Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ buildscript {
classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.83.0'
classpath 'com.palantir.suppressible-error-prone:gradle-suppressible-error-prone:2.26.0'
classpath 'org.revapi:gradle-revapi:1.8.0'
classpath 'me.champeau.jmh:jmh-gradle-plugin:0.7.3'
}
}

Expand All @@ -48,7 +49,7 @@ apply plugin: 'com.palantir.jdks.latest'

javaVersions {
libraryTarget = 17
runtime = 25
runtime = 21
Comment on lines -51 to +52
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why, but I can't seem to run in IntelliJ with runtime = 25

}

allprojects {
Expand Down
21 changes: 21 additions & 0 deletions conjure-jmh/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apply plugin: 'me.champeau.jmh'
apply plugin: 'org.revapi.revapi-gradle-plugin'

jmh {
// Use profilers to collect additional data. Supported profilers:
// [cl, comp, gc, stack, perf, perfnorm, perfasm, xperf, xperfasm, hs_cl, hs_comp, hs_gc, hs_rt, hs_thr]
profilers = ['gc']
}

dependencies {
jmh 'org.openjdk.jmh:jmh-core'

jmh 'com.fasterxml.jackson.core:jackson-annotations'
jmh 'com.fasterxml.jackson.core:jackson-databind'
jmh 'com.google.code.findbugs:jsr305'
jmh 'com.google.guava:guava'
jmh 'com.palantir.conjure.java.runtime:conjure-java-jackson-serialization'
jmh 'com.palantir.safe-logging:preconditions'

jmhAnnotationProcessor 'org.openjdk.jmh:jmh-generator-annprocess'
}
297 changes: 297 additions & 0 deletions conjure-jmh/src/jmh/java/com/palantir/conjure/ConjureBenchmarks.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
/*
* (c) Copyright 2025 Palantir Technologies Inc. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.palantir.conjure;

import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.google.common.collect.ImmutableMap;
import com.google.errorprone.annotations.CheckReturnValue;
import com.palantir.conjure.java.serialization.ObjectMappers;
import com.palantir.logsafe.Preconditions;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import javax.annotation.Nullable;
import org.jspecify.annotations.NullMarked;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.openjdk.jmh.profile.GCProfiler;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(1)
@SuppressWarnings({"NullAway", "designforextension", "checkstyle:RegexpSinglelineJava", "checkstyle:VisibilityModifier"
})
public class ConjureBenchmarks {

private static final ObjectMapper mapper = ObjectMappers.newClientJsonMapper();

@Setup
public void before() {
// Ensure we have enough retained capacity to avoid measuring resizing costs.
// Each operation takes at least ~100-200ns, so 10 million ensures we have enough for all benchmark iterations.
MemoryProfiler.ensureRetainedCapacity(10_000_000);
}

@SuppressWarnings("ImmutableEnumChecker")
public enum RawJson {
NO_MAP("{}"),
EMPTY(generateMap(0)),
SINGLETON(generateMap(1)),
THREE(generateMap(3)),
SIX(generateMap(6)),
TEN(generateMap(10)),
HUNDRED(generateMap(100)),
THOUSAND(generateMap(1000)),
;

private static String generateMap(int count) {
StringBuilder sb = new StringBuilder();
sb.append("{\"map\":{");
for (int i = 1; i <= count; i++) {
sb.append("\"key").append(i).append("\":\"value").append(i).append("\"");
if (i < count) {
sb.append(",");
}
}
sb.append("}}");
return sb.toString();
}

private final byte[] json;

RawJson(String jsonString) {
this.json = jsonString.getBytes(StandardCharsets.UTF_8);
}
}

public enum MapImplementation {
NORMAL(NormalMap.class),
SINGLETON(SingletonMap.class),
GUAVA_IMMUTABLE(GuavaImMap.class);

private final Class<?> clazz;

MapImplementation(Class<?> clazz) {
this.clazz = clazz;
}
}

@Param
public RawJson json;

@Param
public MapImplementation mapImpl;

@Benchmark
public void testAllocatingBenchmark() throws IOException {
MemoryProfiler.addRetained(mapper.readValue(json.json, mapImpl.clazz));
}

@JsonDeserialize(builder = NormalMap.Builder.class)
public static final class NormalMap {
private final Map<String, String> map;

private NormalMap(Map<String, String> map) {
this.map = Collections.unmodifiableMap(map);
}

public Map<String, String> getMap() {
return map;
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private boolean buildInvoked;

private Map<String, String> map = new LinkedHashMap<>();

private Builder() {}

@JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL)
public Builder map(Map<String, String> newMap) {
checkNotBuilt();
this.map = new LinkedHashMap<>(Preconditions.checkNotNull(newMap, "map cannot be null"));
return this;
}

@CheckReturnValue
public NormalMap build() {
checkNotBuilt();
this.buildInvoked = true;
return new NormalMap(map);
}

private void checkNotBuilt() {
Preconditions.checkState(!buildInvoked, "Build has already been called");
}
}
}

private static <K, V> Map<K, V> specialUnmodifiableMap(Map<K, V> in) {
if (in.isEmpty()) {
return Map.of();
}
if (in.size() == 1) {
Iterator<Entry<K, V>> itr = in.entrySet().iterator();
if (itr.hasNext()) {
Entry<K, V> entry = itr.next();
if (!itr.hasNext()) {
return Map.of(entry.getKey(), entry.getValue());
}
}
}
return Collections.unmodifiableMap(in);
}

@JsonDeserialize(builder = SingletonMap.Builder.class)
public static final class SingletonMap {
private final Map<String, String> map;

private SingletonMap(Map<String, String> map) {
this.map = specialUnmodifiableMap(map);
}

public Map<String, String> getMap() {
return map;
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private boolean buildInvoked;

private Map<String, String> map = new LinkedHashMap<>();

private Builder() {}

@JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL)
public Builder map(Map<String, String> newMap) {
checkNotBuilt();
this.map = new LinkedHashMap<>(Preconditions.checkNotNull(newMap, "map cannot be null"));
return this;
}

@CheckReturnValue
public SingletonMap build() {
checkNotBuilt();
this.buildInvoked = true;
return new SingletonMap(map);
}

private void checkNotBuilt() {
Preconditions.checkState(!buildInvoked, "Build has already been called");
}
}
}

@JsonDeserialize(builder = GuavaImMap.Builder.class)
@NullMarked
public static final class GuavaImMap {
private final Map<String, String> map;

private GuavaImMap(ImmutableMap<String, String> map) {
this.map = map;
}

public Map<String, String> getMap() {
return map;
}

public static Builder builder() {
return new Builder();
}

public static final class Builder {
private boolean buildInvoked;

@Nullable
private Object map;

private Builder() {}

@JsonSetter(value = "map", nulls = Nulls.SKIP, contentNulls = Nulls.FAIL)
@JsonDeserialize(as = ImmutableMap.class)
public Builder map(Map<String, String> newMap) {
checkNotBuilt();
this.map = newMap instanceof ImmutableMap<String, String> im
? im
: ImmutableMap.<String, String>builder().putAll(newMap);
return this;
}

@SuppressWarnings({"rawtypes", "unchecked"})
@CheckReturnValue
public GuavaImMap build() {
checkNotBuilt();
this.buildInvoked = true;
ImmutableMap<String, String> finalMap;
if (map == null) {
finalMap = ImmutableMap.of();
} else if (map instanceof ImmutableMap im) {
finalMap = im;
} else if (map instanceof ImmutableMap.Builder builder) {
finalMap = builder.buildOrThrow();
} else {
throw new IllegalStateException("Unexpected map type: " + map.getClass());
}
return new GuavaImMap(finalMap);
}

private void checkNotBuilt() {
Preconditions.checkState(!buildInvoked, "Build has already been called");
}
}
}

public static void main(String[] _args) throws Exception {
Options opt = new OptionsBuilder()
.include(ConjureBenchmarks.class.getSimpleName())
.addProfiler(GCProfiler.class)
.addProfiler(MemoryProfiler.class)
.build();

new Runner(opt).run();
}
}
Loading