Skip to content

Commit ec241c8

Browse files
committed
Make minor timestamp optimizations
Now that we cache `getFormat` in the smithy TimestampFormatTrait, we can simplify lookups and remove the cache. This gives a modest improvement but also remove the CHM (~1080ns/iter to ~920ns/iter in the new benchmark).
1 parent 65001fb commit ec241c8

File tree

7 files changed

+168
-37
lines changed

7 files changed

+168
-37
lines changed

codecs/json-codec/build.gradle.kts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
2-
31
plugins {
42
id("smithy-java.module-conventions")
53
id("smithy-java.fuzz-test")
4+
id("me.champeau.jmh") version "0.7.3"
65
alias(libs.plugins.shadow)
76
}
87

@@ -55,3 +54,11 @@ afterEvaluate {
5554
mapToMavenScope("runtime")
5655
}
5756
}
57+
58+
jmh {
59+
warmupIterations = 3
60+
iterations = 3
61+
fork = 3
62+
// profilers.add("async:output=flamegraph")
63+
// profilers.add("gc")
64+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.java.json;
7+
8+
import java.nio.ByteBuffer;
9+
import java.time.Instant;
10+
import java.util.concurrent.TimeUnit;
11+
import org.openjdk.jmh.annotations.Benchmark;
12+
import org.openjdk.jmh.annotations.BenchmarkMode;
13+
import org.openjdk.jmh.annotations.Fork;
14+
import org.openjdk.jmh.annotations.Mode;
15+
import org.openjdk.jmh.annotations.OutputTimeUnit;
16+
import org.openjdk.jmh.annotations.Scope;
17+
import org.openjdk.jmh.annotations.Setup;
18+
import org.openjdk.jmh.annotations.State;
19+
import org.openjdk.jmh.infra.Blackhole;
20+
import org.openjdk.jmh.util.NullOutputStream;
21+
import software.amazon.smithy.java.core.schema.PreludeSchemas;
22+
import software.amazon.smithy.java.core.schema.Schema;
23+
import software.amazon.smithy.java.core.schema.SerializableStruct;
24+
import software.amazon.smithy.java.core.serde.ShapeSerializer;
25+
import software.amazon.smithy.model.shapes.ShapeId;
26+
import software.amazon.smithy.model.traits.TimestampFormatTrait;
27+
28+
@State(Scope.Benchmark)
29+
@OutputTimeUnit(TimeUnit.NANOSECONDS)
30+
@BenchmarkMode(Mode.AverageTime)
31+
@Fork(1)
32+
public class JsonBench {
33+
JsonCodec codec;
34+
ByteBuffer buff;
35+
Schema mainSchema;
36+
SerializableStruct struct;
37+
ShapeSerializer serializer;
38+
39+
@Setup
40+
public void setup() throws Exception {
41+
buff = ByteBuffer.allocate(4096);
42+
codec = JsonCodec.builder()
43+
.defaultNamespace("foo")
44+
.useTimestampFormat(true)
45+
.build();
46+
var tsId = PreludeSchemas.TIMESTAMP.id();
47+
mainSchema = Schema.structureBuilder(ShapeId.from("foo#Bar"))
48+
.putMember("a", Schema.createTimestamp(tsId))
49+
.putMember("b", Schema.createTimestamp(tsId, new TimestampFormatTrait("epoch-seconds")))
50+
.putMember("c", Schema.createTimestamp(tsId))
51+
.putMember("d", Schema.createTimestamp(tsId))
52+
.putMember("e", Schema.createTimestamp(tsId))
53+
.putMember("f", Schema.createTimestamp(tsId))
54+
.putMember("g", Schema.createTimestamp(tsId))
55+
.putMember("h", Schema.createTimestamp(tsId))
56+
.putMember("i", Schema.createTimestamp(tsId))
57+
.build();
58+
var instant = Instant.now();
59+
struct = new TestStruct(mainSchema, instant);
60+
serializer = codec.createSerializer(new NullOutputStream());
61+
}
62+
63+
private static final class TestStruct implements SerializableStruct {
64+
65+
private final Instant instant;
66+
private final Schema schema;
67+
private final Schema amember;
68+
private final Schema bmember;
69+
private final Schema cmember;
70+
private final Schema dmember;
71+
private final Schema emember;
72+
private final Schema fmember;
73+
private final Schema gmember;
74+
private final Schema hmember;
75+
private final Schema imember;
76+
77+
TestStruct(Schema schema, Instant instant) {
78+
this.instant = instant;
79+
this.schema = schema;
80+
amember = schema.member("a");
81+
bmember = schema.member("b");
82+
cmember = schema.member("c");
83+
dmember = schema.member("d");
84+
emember = schema.member("e");
85+
fmember = schema.member("f");
86+
gmember = schema.member("g");
87+
hmember = schema.member("h");
88+
imember = schema.member("i");
89+
}
90+
91+
@Override
92+
public Schema schema() {
93+
return schema;
94+
}
95+
96+
@Override
97+
public void serializeMembers(ShapeSerializer serializer) {
98+
serializer.writeTimestamp(amember, instant);
99+
serializer.writeTimestamp(bmember, instant);
100+
serializer.writeTimestamp(cmember, instant);
101+
serializer.writeTimestamp(dmember, instant);
102+
serializer.writeTimestamp(emember, instant);
103+
serializer.writeTimestamp(fmember, instant);
104+
serializer.writeTimestamp(gmember, instant);
105+
serializer.writeTimestamp(hmember, instant);
106+
serializer.writeTimestamp(imember, instant);
107+
}
108+
109+
@Override
110+
public <T> T getMemberValue(Schema member) {
111+
throw new UnsupportedOperationException();
112+
}
113+
}
114+
115+
@Benchmark
116+
public void serialize(Blackhole bh) {
117+
serializer.writeStruct(struct.schema(), struct);
118+
bh.consume(buff);
119+
}
120+
}

codecs/json-codec/src/main/java/software/amazon/smithy/java/json/TimestampResolver.java

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import java.time.Instant;
99
import java.util.Objects;
10-
import java.util.concurrent.ConcurrentHashMap;
1110
import software.amazon.smithy.java.core.schema.Schema;
1211
import software.amazon.smithy.java.core.schema.TraitKey;
1312
import software.amazon.smithy.java.core.serde.SerializationException;
@@ -75,7 +74,6 @@ public String toString() {
7574
* Uses the timestampFormat trait if present, otherwise uses a configurable default format.
7675
*/
7776
final class UseTimestampFormatTrait implements TimestampResolver {
78-
private final ConcurrentHashMap<Schema, TimestampFormatter> cache = new ConcurrentHashMap<>();
7977
private final TimestampFormatter defaultFormat;
8078

8179
UseTimestampFormatTrait(TimestampFormatter defaultFormat) {
@@ -89,14 +87,14 @@ public TimestampFormatter defaultFormat() {
8987

9088
@Override
9189
public TimestampFormatter resolve(Schema schema) {
92-
var result = cache.get(schema);
93-
if (result == null) {
94-
var trait = schema.getTrait(TraitKey.TIMESTAMP_FORMAT_TRAIT);
95-
var fresh = trait != null ? TimestampFormatter.of(trait) : defaultFormat;
96-
var previous = cache.putIfAbsent(schema, fresh);
97-
result = previous == null ? fresh : previous;
90+
var trait = schema.getTrait(TraitKey.TIMESTAMP_FORMAT_TRAIT);
91+
if (trait != null) {
92+
var formatter = TimestampFormatter.match(trait.getFormat());
93+
if (formatter != null) {
94+
return formatter;
95+
}
9896
}
99-
return result;
97+
return defaultFormat;
10098
}
10199

102100
@Override

codecs/json-codec/src/main/java/software/amazon/smithy/java/json/jackson/JacksonJsonDeserializer.java

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import software.amazon.smithy.java.core.serde.document.Document;
2727
import software.amazon.smithy.java.json.JsonDocuments;
2828
import software.amazon.smithy.java.json.JsonSettings;
29-
import software.amazon.smithy.java.json.TimestampResolver;
3029
import software.amazon.smithy.model.shapes.ShapeType;
3130

3231
final class JacksonJsonDeserializer implements ShapeDeserializer {
@@ -232,14 +231,11 @@ private String describeToken() {
232231
public Instant readTimestamp(Schema schema) {
233232
try {
234233
var format = settings.timestampResolver().resolve(schema);
235-
if (parser.getCurrentToken() == JsonToken.VALUE_NUMBER_FLOAT
236-
|| parser.getCurrentToken() == JsonToken.VALUE_NUMBER_INT) {
237-
return TimestampResolver.readTimestamp(parser.getNumberValue(), format);
238-
} else if (parser.getCurrentToken() == JsonToken.VALUE_STRING) {
239-
return TimestampResolver.readTimestamp(parser.getText(), format);
240-
} else {
241-
throw new SerializationException("Expected a timestamp, but found " + describeToken());
242-
}
234+
return switch (parser.getCurrentToken()) {
235+
case VALUE_NUMBER_FLOAT, VALUE_NUMBER_INT -> format.readFromNumber(parser.getNumberValue());
236+
case VALUE_STRING -> format.readFromString(parser.getText(), true);
237+
default -> throw new SerializationException("Expected a timestamp, but found " + describeToken());
238+
};
243239
} catch (Exception e) {
244240
throw new SerializationException(e);
245241
}

codecs/json-codec/src/main/java/software/amazon/smithy/java/json/jackson/JacksonJsonSerdeProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.fasterxml.jackson.core.JsonFactory;
99
import com.fasterxml.jackson.core.JsonFactoryBuilder;
1010
import com.fasterxml.jackson.core.StreamReadFeature;
11+
import com.fasterxml.jackson.core.StreamWriteFeature;
1112
import java.io.IOException;
1213
import java.io.OutputStream;
1314
import java.nio.ByteBuffer;
@@ -27,6 +28,7 @@ public class JacksonJsonSerdeProvider implements JsonSerdeProvider {
2728
static {
2829
var serBuilder = new JsonFactoryBuilder();
2930
serBuilder.disable(JsonFactory.Feature.INTERN_FIELD_NAMES);
31+
serBuilder.enable(StreamWriteFeature.USE_FAST_DOUBLE_WRITER);
3032
serBuilder.enable(StreamReadFeature.USE_FAST_DOUBLE_PARSER);
3133
serBuilder.enable(StreamReadFeature.USE_FAST_BIG_NUMBER_PARSER);
3234
FACTORY = serBuilder.build();

codecs/json-codec/src/main/java/software/amazon/smithy/java/json/jackson/JacksonJsonSerializer.java

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -135,18 +135,15 @@ public void writeFloat(Schema schema, float value) {
135135
try {
136136
if (Float.isFinite(value)) {
137137
int intValue = (int) value;
138-
if (value - intValue != 0) {
139-
// Avoid writing 1.0 and instead write 1.
140-
generator.writeNumber(value);
141-
} else {
138+
if (value == (float) intValue) {
142139
generator.writeNumber(intValue);
140+
} else {
141+
generator.writeNumber(value);
143142
}
144143
} else if (Float.isNaN(value)) {
145144
generator.writeString("NaN");
146-
} else if (Float.POSITIVE_INFINITY == value) {
147-
generator.writeString("Infinity");
148145
} else {
149-
generator.writeString("-Infinity");
146+
generator.writeString(value > 0 ? "Infinity" : "-Infinity");
150147
}
151148
} catch (Exception e) {
152149
throw new SerializationException(e);
@@ -157,19 +154,17 @@ public void writeFloat(Schema schema, float value) {
157154
public void writeDouble(Schema schema, double value) {
158155
try {
159156
if (Double.isFinite(value)) {
157+
// Avoid writing 1.0 and instead write 1.
160158
long longValue = (long) value;
161-
if (value - longValue != 0) {
162-
// Avoid writing 1.0 and instead write 1.
163-
generator.writeNumber(value);
164-
} else {
159+
if (value == (double) longValue) {
165160
generator.writeNumber(longValue);
161+
} else {
162+
generator.writeNumber(value);
166163
}
167164
} else if (Double.isNaN(value)) {
168165
generator.writeString("NaN");
169-
} else if (Double.POSITIVE_INFINITY == value) {
170-
generator.writeString("Infinity");
171166
} else {
172-
generator.writeString("-Infinity");
167+
generator.writeString(value > 0 ? "Infinity" : "-Infinity");
173168
}
174169
} catch (Exception e) {
175170
throw new SerializationException(e);

core/src/main/java/software/amazon/smithy/java/core/serde/TimestampFormatter.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,25 @@ static TimestampFormatter of(TimestampFormatTrait trait) {
6868
* @return the created formatter.
6969
*/
7070
static TimestampFormatter of(TimestampFormatTrait.Format format) {
71+
var result = match(format);
72+
if (result == null) {
73+
throw new SerializationException("Unknown timestamp format: " + format);
74+
}
75+
return result;
76+
}
77+
78+
/**
79+
* Attempts to create a TimestampFormatter from the given format, or returns null if unknown.
80+
*
81+
* @param format Format to create.
82+
* @return the created formatter.
83+
*/
84+
static TimestampFormatter match(TimestampFormatTrait.Format format) {
7185
return switch (format) {
7286
case DATE_TIME -> Prelude.DATE_TIME;
7387
case EPOCH_SECONDS -> Prelude.EPOCH_SECONDS;
7488
case HTTP_DATE -> Prelude.HTTP_DATE;
75-
default -> throw new SerializationException("Unknown timestamp format: " + format);
89+
case null, default -> null;
7690
};
7791
}
7892

@@ -174,8 +188,7 @@ public Instant readFromNumber(Number value) {
174188

175189
@Override
176190
public void writeToSerializer(Schema schema, Instant instant, ShapeSerializer serializer) {
177-
double value = ((double) instant.toEpochMilli()) / 1000;
178-
serializer.writeDouble(schema, value);
191+
serializer.writeDouble(schema, instant.toEpochMilli() / 1000.0);
179192
}
180193
},
181194

0 commit comments

Comments
 (0)