Skip to content
This repository was archived by the owner on May 30, 2024. It is now read-only.

Commit b959c6c

Browse files
authored
Merge pull request #238 from launchdarkly/eb/ch76240/benchmarks-4.x
(4.x) implement JMH benchmarks
2 parents 34ecff9 + 0cefb4e commit b959c6c

File tree

11 files changed

+580
-0
lines changed

11 files changed

+580
-0
lines changed

.circleci/config.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ workflows:
3030
- packaging:
3131
requires:
3232
- build-linux
33+
- benchmarks:
34+
requires:
35+
- build-linux
3336
- build-test-windows:
3437
name: Java 11 - Windows - OpenJDK
3538

@@ -131,3 +134,19 @@ jobs:
131134
- run:
132135
name: run packaging tests
133136
command: cd packaging-test && make all
137+
138+
benchmarks:
139+
docker:
140+
- image: circleci/openjdk:11
141+
steps:
142+
- run: java -version
143+
- run: sudo apt-get install make -y -q
144+
- checkout
145+
- attach_workspace:
146+
at: build
147+
- run: cat gradle.properties.example >>gradle.properties
148+
- run:
149+
name: run benchmarks
150+
command: cd benchmarks && make
151+
- store_artifacts:
152+
path: benchmarks/build/reports/jmh

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ out/
1717
classes/
1818

1919
packaging-test/temp/
20+
benchmarks/lib/

CONTRIBUTING.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,7 @@ To build the SDK and run all unit tests:
4242
```
4343

4444
By default, the full unit test suite includes live tests of the Redis integration. Those tests expect you to have Redis running locally. To skip them, set the environment variable `LD_SKIP_DATABASE_TESTS=1` before running the tests.
45+
46+
### Benchmarks
47+
48+
The project in the `benchmarks` subdirectory uses [JMH](https://openjdk.java.net/projects/code-tools/jmh/) to generate performance metrics for the SDK. This is run as a CI job, and can also be run manually by running `make` within `benchmarks` and then inspecting `build/reports/jmh`.

benchmarks/Makefile

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.PHONY: benchmark clean sdk
2+
3+
BASE_DIR:=$(shell pwd)
4+
PROJECT_DIR=$(shell cd .. && pwd)
5+
SDK_VERSION=$(shell grep "version=" $(PROJECT_DIR)/gradle.properties | cut -d '=' -f 2)
6+
7+
BENCHMARK_ALL_JAR=lib/launchdarkly-java-server-sdk-all.jar
8+
BENCHMARK_TEST_JAR=lib/launchdarkly-java-server-sdk-test.jar
9+
SDK_JARS_DIR=$(PROJECT_DIR)/build/libs
10+
SDK_ALL_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-all.jar
11+
SDK_TEST_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-test.jar
12+
13+
benchmark: $(BENCHMARK_ALL_JAR) $(BENCHMARK_TEST_JAR)
14+
rm -rf build/tmp
15+
../gradlew jmh
16+
cat build/reports/jmh/human.txt
17+
../gradlew jmhReport
18+
19+
clean:
20+
rm -rf build lib
21+
22+
sdk: $(BENCHMARK_ALL_JAR) $(BENCHMARK_TEST_JAR)
23+
24+
$(BENCHMARK_ALL_JAR): $(SDK_ALL_JAR)
25+
mkdir -p lib
26+
cp $< $@
27+
28+
$(BENCHMARK_TEST_JAR): $(SDK_TEST_JAR)
29+
mkdir -p lib
30+
cp $< $@
31+
32+
$(SDK_ALL_JAR):
33+
cd .. && ./gradlew shadowJarAll
34+
35+
$(SDK_TEST_JAR):
36+
cd .. && ./gradlew testJar

benchmarks/build.gradle

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
2+
buildscript {
3+
repositories {
4+
jcenter()
5+
mavenCentral()
6+
}
7+
}
8+
9+
plugins {
10+
id "me.champeau.gradle.jmh" version "0.5.0"
11+
id "io.morethan.jmhreport" version "0.9.0"
12+
}
13+
14+
repositories {
15+
mavenCentral()
16+
}
17+
18+
ext.versions = [
19+
"jmh": "1.21",
20+
"guava": "19.0"
21+
]
22+
23+
dependencies {
24+
compile files("lib/launchdarkly-java-server-sdk-all.jar")
25+
compile files("lib/launchdarkly-java-server-sdk-test.jar")
26+
compile "com.google.guava:guava:${versions.guava}" // required by SDK test code
27+
compile "com.squareup.okhttp3:mockwebserver:3.12.10"
28+
compile "org.openjdk.jmh:jmh-core:1.21"
29+
compile "org.openjdk.jmh:jmh-generator-annprocess:${versions.jmh}"
30+
}
31+
32+
jmh {
33+
iterations = 10 // Number of measurement iterations to do.
34+
benchmarkMode = ['avgt'] // "average time" - reports execution time as ns/op and allocations as B/op.
35+
// batchSize = 1 // Batch size: number of benchmark method calls per operation. (some benchmark modes can ignore this setting)
36+
fork = 1 // How many times to forks a single benchmark. Use 0 to disable forking altogether
37+
// failOnError = false // Should JMH fail immediately if any benchmark had experienced the unrecoverable error?
38+
forceGC = true // Should JMH force GC between iterations?
39+
humanOutputFile = project.file("${project.buildDir}/reports/jmh/human.txt") // human-readable output file
40+
// resultsFile = project.file("${project.buildDir}/reports/jmh/results.txt") // results file
41+
operationsPerInvocation = 3 // Operations per invocation.
42+
// benchmarkParameters = [:] // Benchmark parameters.
43+
profilers = [ 'gc' ] // 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]
44+
timeOnIteration = '1s' // Time to spend at each measurement iteration.
45+
resultFormat = 'JSON' // Result format type (one of CSV, JSON, NONE, SCSV, TEXT)
46+
// synchronizeIterations = false // Synchronize iterations?
47+
// threads = 4 // Number of worker threads to run with.
48+
// timeout = '1s' // Timeout for benchmark iteration.
49+
timeUnit = 'ns' // Output time unit. Available time units are: [m, s, ms, us, ns].
50+
verbosity = 'NORMAL' // Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA]
51+
warmup = '1s' // Time to spend at each warmup iteration.
52+
warmupBatchSize = 2 // Warmup batch size: number of benchmark method calls per operation.
53+
warmupIterations = 1 // Number of warmup iterations to do.
54+
// warmupForks = 0 // How many warmup forks to make for a single benchmark. 0 to disable warmup forks.
55+
// warmupMode = 'INDI' // Warmup mode for warming up selected benchmarks. Warmup modes are: [INDI, BULK, BULK_INDI].
56+
57+
jmhVersion = versions.jmh
58+
}
59+
60+
jmhReport {
61+
jmhResultPath = project.file('build/reports/jmh/results.json')
62+
jmhReportOutput = project.file('build/reports/jmh')
63+
}

benchmarks/settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
rootProject.name = 'launchdarkly-java-server-sdk-benchmarks'
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package com.launchdarkly.client;
2+
3+
import java.io.IOException;
4+
5+
// Placed here so we can access package-private SDK methods.
6+
public class EventProcessorInternals {
7+
public static void waitUntilInactive(EventProcessor ep) {
8+
try {
9+
((DefaultEventProcessor)ep).waitUntilInactive();
10+
} catch (IOException e) {
11+
throw new RuntimeException(e);
12+
}
13+
}
14+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.launchdarkly.client;
2+
3+
import com.launchdarkly.client.value.LDValue;
4+
5+
import java.util.ArrayList;
6+
import java.util.Arrays;
7+
import java.util.HashSet;
8+
import java.util.List;
9+
10+
import static com.launchdarkly.client.TestUtil.fallthroughVariation;
11+
import static com.launchdarkly.client.TestUtil.flagWithValue;
12+
import static com.launchdarkly.client.VersionedDataKind.FEATURES;
13+
import static com.launchdarkly.sdk.server.TestValues.BOOLEAN_FLAG_KEY;
14+
import static com.launchdarkly.sdk.server.TestValues.CLAUSE_MATCH_ATTRIBUTE;
15+
import static com.launchdarkly.sdk.server.TestValues.CLAUSE_MATCH_VALUES;
16+
import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_MULTI_VALUE_CLAUSE_KEY;
17+
import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_PREREQ_KEY;
18+
import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_TARGET_LIST_KEY;
19+
import static com.launchdarkly.sdk.server.TestValues.INT_FLAG_KEY;
20+
import static com.launchdarkly.sdk.server.TestValues.JSON_FLAG_KEY;
21+
import static com.launchdarkly.sdk.server.TestValues.STRING_FLAG_KEY;
22+
import static com.launchdarkly.sdk.server.TestValues.TARGETED_USER_KEYS;
23+
24+
// This class must be in com.launchdarkly.client because FeatureFlagBuilder is package-private in the
25+
// SDK, but we are keeping the rest of the benchmark implementation code in com.launchdarkly.sdk.server
26+
// so we can more clearly compare between 4.x and 5.0.
27+
public class FlagData {
28+
public static void loadTestFlags(FeatureStore store) {
29+
for (FeatureFlag flag: FlagData.makeTestFlags()) {
30+
store.upsert(FEATURES, flag);
31+
}
32+
}
33+
34+
public static List<FeatureFlag> makeTestFlags() {
35+
List<FeatureFlag> flags = new ArrayList<>();
36+
37+
flags.add(flagWithValue(BOOLEAN_FLAG_KEY, LDValue.of(true)));
38+
flags.add(flagWithValue(INT_FLAG_KEY, LDValue.of(1)));
39+
flags.add(flagWithValue(STRING_FLAG_KEY, LDValue.of("x")));
40+
flags.add(flagWithValue(JSON_FLAG_KEY, LDValue.buildArray().build()));
41+
42+
FeatureFlag targetsFlag = new FeatureFlagBuilder(FLAG_WITH_TARGET_LIST_KEY)
43+
.on(true)
44+
.targets(Arrays.asList(new Target(new HashSet<String>(TARGETED_USER_KEYS), 1)))
45+
.fallthrough(fallthroughVariation(0))
46+
.offVariation(0)
47+
.variations(LDValue.of(false), LDValue.of(true))
48+
.build();
49+
flags.add(targetsFlag);
50+
51+
FeatureFlag prereqFlag = new FeatureFlagBuilder("prereq-flag")
52+
.on(true)
53+
.fallthrough(fallthroughVariation(1))
54+
.variations(LDValue.of(false), LDValue.of(true))
55+
.build();
56+
flags.add(prereqFlag);
57+
58+
FeatureFlag flagWithPrereq = new FeatureFlagBuilder(FLAG_WITH_PREREQ_KEY)
59+
.on(true)
60+
.prerequisites(Arrays.asList(new Prerequisite("prereq-flag", 1)))
61+
.fallthrough(fallthroughVariation(1))
62+
.offVariation(0)
63+
.variations(LDValue.of(false), LDValue.of(true))
64+
.build();
65+
flags.add(flagWithPrereq);
66+
67+
FeatureFlag flagWithMultiValueClause = new FeatureFlagBuilder(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY)
68+
.on(true)
69+
.fallthrough(fallthroughVariation(0))
70+
.offVariation(0)
71+
.variations(LDValue.of(false), LDValue.of(true))
72+
.rules(Arrays.asList(
73+
new RuleBuilder()
74+
.clauses(new Clause(CLAUSE_MATCH_ATTRIBUTE, Operator.in, CLAUSE_MATCH_VALUES, false))
75+
.build()
76+
))
77+
.build();
78+
flags.add(flagWithMultiValueClause);
79+
80+
return flags;
81+
}
82+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package com.launchdarkly.sdk.server;
2+
3+
import com.launchdarkly.client.Components;
4+
import com.launchdarkly.client.Event;
5+
import com.launchdarkly.client.EventProcessor;
6+
import com.launchdarkly.client.EventProcessorInternals;
7+
import com.launchdarkly.client.LDConfig;
8+
import com.launchdarkly.client.interfaces.EventSender;
9+
import com.launchdarkly.client.interfaces.EventSenderFactory;
10+
import com.launchdarkly.client.interfaces.HttpConfiguration;
11+
import com.launchdarkly.client.value.LDValue;
12+
13+
import org.openjdk.jmh.annotations.Benchmark;
14+
import org.openjdk.jmh.annotations.Scope;
15+
import org.openjdk.jmh.annotations.State;
16+
17+
import java.io.IOException;
18+
import java.net.URI;
19+
import java.util.ArrayList;
20+
import java.util.List;
21+
import java.util.Random;
22+
23+
import static com.launchdarkly.sdk.server.TestValues.BASIC_USER;
24+
import static com.launchdarkly.sdk.server.TestValues.CUSTOM_EVENT;
25+
import static com.launchdarkly.sdk.server.TestValues.TEST_EVENTS_COUNT;
26+
27+
public class EventProcessorBenchmarks {
28+
private static final int EVENT_BUFFER_SIZE = 1000;
29+
private static final int FLAG_COUNT = 10;
30+
private static final int FLAG_VERSIONS = 3;
31+
private static final int FLAG_VARIATIONS = 2;
32+
33+
@State(Scope.Thread)
34+
public static class BenchmarkInputs {
35+
// Initialization of the things in BenchmarkInputs does not count as part of a benchmark.
36+
final EventProcessor eventProcessor;
37+
final EventSender eventSender;
38+
final List<Event.FeatureRequest> featureRequestEventsWithoutTracking = new ArrayList<>();
39+
final List<Event.FeatureRequest> featureRequestEventsWithTracking = new ArrayList<>();
40+
final Random random;
41+
42+
public BenchmarkInputs() {
43+
// MockEventSender does no I/O - it discards every event payload. So we are benchmarking
44+
// all of the event processing steps up to that point, including the formatting of the
45+
// JSON data in the payload.
46+
eventSender = new MockEventSender();
47+
48+
eventProcessor = Components.sendEvents()
49+
.capacity(EVENT_BUFFER_SIZE)
50+
.eventSender(new MockEventSenderFactory())
51+
.createEventProcessor(TestValues.SDK_KEY, new LDConfig.Builder().build());
52+
53+
random = new Random();
54+
55+
for (int i = 0; i < TEST_EVENTS_COUNT; i++) {
56+
String flagKey = "flag" + random.nextInt(FLAG_COUNT);
57+
int version = random.nextInt(FLAG_VERSIONS) + 1;
58+
int variation = random.nextInt(FLAG_VARIATIONS);
59+
for (boolean trackEvents: new boolean[] { false, true }) {
60+
Event.FeatureRequest event = new Event.FeatureRequest(
61+
System.currentTimeMillis(),
62+
flagKey,
63+
BASIC_USER,
64+
version,
65+
variation,
66+
LDValue.of(variation),
67+
LDValue.ofNull(),
68+
null,
69+
null,
70+
trackEvents,
71+
null,
72+
false
73+
);
74+
(trackEvents ? featureRequestEventsWithTracking : featureRequestEventsWithoutTracking).add(event);
75+
}
76+
}
77+
}
78+
79+
public String randomFlagKey() {
80+
return "flag" + random.nextInt(FLAG_COUNT);
81+
}
82+
83+
public int randomFlagVersion() {
84+
return random.nextInt(FLAG_VERSIONS) + 1;
85+
}
86+
87+
public int randomFlagVariation() {
88+
return random.nextInt(FLAG_VARIATIONS);
89+
}
90+
}
91+
92+
@Benchmark
93+
public void summarizeFeatureRequestEvents(BenchmarkInputs inputs) throws Exception {
94+
for (Event.FeatureRequest event: inputs.featureRequestEventsWithoutTracking) {
95+
inputs.eventProcessor.sendEvent(event);
96+
}
97+
inputs.eventProcessor.flush();
98+
EventProcessorInternals.waitUntilInactive(inputs.eventProcessor);
99+
}
100+
101+
@Benchmark
102+
public void featureRequestEventsWithFullTracking(BenchmarkInputs inputs) throws Exception {
103+
for (Event.FeatureRequest event: inputs.featureRequestEventsWithTracking) {
104+
inputs.eventProcessor.sendEvent(event);
105+
}
106+
inputs.eventProcessor.flush();
107+
EventProcessorInternals.waitUntilInactive(inputs.eventProcessor);
108+
}
109+
110+
@Benchmark
111+
public void customEvents(BenchmarkInputs inputs) throws Exception {
112+
for (int i = 0; i < TEST_EVENTS_COUNT; i++) {
113+
inputs.eventProcessor.sendEvent(CUSTOM_EVENT);
114+
}
115+
inputs.eventProcessor.flush();
116+
EventProcessorInternals.waitUntilInactive(inputs.eventProcessor);
117+
}
118+
119+
private static final class MockEventSender implements EventSender {
120+
private static final Result RESULT = new Result(true, false, null);
121+
122+
@Override
123+
public void close() throws IOException {}
124+
125+
@Override
126+
public Result sendEventData(EventDataKind arg0, String arg1, int arg2, URI arg3) {
127+
return RESULT;
128+
}
129+
}
130+
131+
private static final class MockEventSenderFactory implements EventSenderFactory {
132+
@Override
133+
public EventSender createEventSender(String arg0, HttpConfiguration arg1) {
134+
return new MockEventSender();
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)