diff --git a/.circleci/config.yml b/.circleci/config.yml index 554291282..9d09ffa0d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -30,6 +30,9 @@ workflows: - packaging: requires: - build-linux + - benchmarks: + requires: + - build-linux - build-test-windows: name: Java 11 - Windows - OpenJDK @@ -131,3 +134,19 @@ jobs: - run: name: run packaging tests command: cd packaging-test && make all + + benchmarks: + docker: + - image: circleci/openjdk:11 + steps: + - run: java -version + - run: sudo apt-get install make -y -q + - checkout + - attach_workspace: + at: build + - run: cat gradle.properties.example >>gradle.properties + - run: + name: run benchmarks + command: cd benchmarks && make + - store_artifacts: + path: benchmarks/build/reports/jmh diff --git a/.gitignore b/.gitignore index de40ed7a7..b127c5f09 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ out/ classes/ packaging-test/temp/ +benchmarks/lib/ diff --git a/.ldrelease/config.yml b/.ldrelease/config.yml index 09d702867..601d8b378 100644 --- a/.ldrelease/config.yml +++ b/.ldrelease/config.yml @@ -13,6 +13,11 @@ template: env: LD_SKIP_DATABASE_TESTS: 1 +releasableBranches: + - name: master + description: 5.x + - name: 4.x + documentation: githubPages: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3e5c76bf0..13daea7e0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,3 +42,7 @@ To build the SDK and run all unit tests: ``` 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. + +### Benchmarks + +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`. diff --git a/benchmarks/Makefile b/benchmarks/Makefile new file mode 100644 index 000000000..d39fff5d9 --- /dev/null +++ b/benchmarks/Makefile @@ -0,0 +1,36 @@ +.PHONY: benchmark clean sdk + +BASE_DIR:=$(shell pwd) +PROJECT_DIR=$(shell cd .. && pwd) +SDK_VERSION=$(shell grep "version=" $(PROJECT_DIR)/gradle.properties | cut -d '=' -f 2) + +BENCHMARK_ALL_JAR=lib/launchdarkly-java-server-sdk-all.jar +BENCHMARK_TEST_JAR=lib/launchdarkly-java-server-sdk-test.jar +SDK_JARS_DIR=$(PROJECT_DIR)/build/libs +SDK_ALL_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-all.jar +SDK_TEST_JAR=$(SDK_JARS_DIR)/launchdarkly-java-server-sdk-$(SDK_VERSION)-test.jar + +benchmark: $(BENCHMARK_ALL_JAR) $(BENCHMARK_TEST_JAR) + rm -rf build/tmp + ../gradlew jmh + cat build/reports/jmh/human.txt + ../gradlew jmhReport + +clean: + rm -rf build lib + +sdk: $(BENCHMARK_ALL_JAR) $(BENCHMARK_TEST_JAR) + +$(BENCHMARK_ALL_JAR): $(SDK_ALL_JAR) + mkdir -p lib + cp $< $@ + +$(BENCHMARK_TEST_JAR): $(SDK_TEST_JAR) + mkdir -p lib + cp $< $@ + +$(SDK_ALL_JAR): + cd .. && ./gradlew shadowJarAll + +$(SDK_TEST_JAR): + cd .. && ./gradlew testJar diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle new file mode 100644 index 000000000..b63e654e7 --- /dev/null +++ b/benchmarks/build.gradle @@ -0,0 +1,63 @@ + +buildscript { + repositories { + jcenter() + mavenCentral() + } +} + +plugins { + id "me.champeau.gradle.jmh" version "0.5.0" + id "io.morethan.jmhreport" version "0.9.0" +} + +repositories { + mavenCentral() +} + +ext.versions = [ + "jmh": "1.21", + "guava": "19.0" +] + +dependencies { + compile files("lib/launchdarkly-java-server-sdk-all.jar") + compile files("lib/launchdarkly-java-server-sdk-test.jar") + compile "com.google.guava:guava:${versions.guava}" // required by SDK test code + compile "com.squareup.okhttp3:mockwebserver:3.12.10" + compile "org.openjdk.jmh:jmh-core:1.21" + compile "org.openjdk.jmh:jmh-generator-annprocess:${versions.jmh}" +} + +jmh { + iterations = 10 // Number of measurement iterations to do. + benchmarkMode = ['avgt'] // "average time" - reports execution time as ns/op and allocations as B/op. + // batchSize = 1 // Batch size: number of benchmark method calls per operation. (some benchmark modes can ignore this setting) + fork = 1 // How many times to forks a single benchmark. Use 0 to disable forking altogether + // failOnError = false // Should JMH fail immediately if any benchmark had experienced the unrecoverable error? + forceGC = true // Should JMH force GC between iterations? + humanOutputFile = project.file("${project.buildDir}/reports/jmh/human.txt") // human-readable output file + // resultsFile = project.file("${project.buildDir}/reports/jmh/results.txt") // results file + operationsPerInvocation = 3 // Operations per invocation. + // benchmarkParameters = [:] // Benchmark parameters. + 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] + timeOnIteration = '1s' // Time to spend at each measurement iteration. + resultFormat = 'JSON' // Result format type (one of CSV, JSON, NONE, SCSV, TEXT) + // synchronizeIterations = false // Synchronize iterations? + // threads = 4 // Number of worker threads to run with. + // timeout = '1s' // Timeout for benchmark iteration. + timeUnit = 'ns' // Output time unit. Available time units are: [m, s, ms, us, ns]. + verbosity = 'NORMAL' // Verbosity mode. Available modes are: [SILENT, NORMAL, EXTRA] + warmup = '1s' // Time to spend at each warmup iteration. + warmupBatchSize = 2 // Warmup batch size: number of benchmark method calls per operation. + warmupIterations = 1 // Number of warmup iterations to do. + // warmupForks = 0 // How many warmup forks to make for a single benchmark. 0 to disable warmup forks. + // warmupMode = 'INDI' // Warmup mode for warming up selected benchmarks. Warmup modes are: [INDI, BULK, BULK_INDI]. + + jmhVersion = versions.jmh +} + +jmhReport { + jmhResultPath = project.file('build/reports/jmh/results.json') + jmhReportOutput = project.file('build/reports/jmh') +} diff --git a/benchmarks/settings.gradle b/benchmarks/settings.gradle new file mode 100644 index 000000000..81d1c11c8 --- /dev/null +++ b/benchmarks/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'launchdarkly-java-server-sdk-benchmarks' diff --git a/benchmarks/src/jmh/java/com/launchdarkly/client/EventProcessorInternals.java b/benchmarks/src/jmh/java/com/launchdarkly/client/EventProcessorInternals.java new file mode 100644 index 000000000..416ff2d44 --- /dev/null +++ b/benchmarks/src/jmh/java/com/launchdarkly/client/EventProcessorInternals.java @@ -0,0 +1,14 @@ +package com.launchdarkly.client; + +import java.io.IOException; + +// Placed here so we can access package-private SDK methods. +public class EventProcessorInternals { + public static void waitUntilInactive(EventProcessor ep) { + try { + ((DefaultEventProcessor)ep).waitUntilInactive(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/benchmarks/src/jmh/java/com/launchdarkly/client/FlagData.java b/benchmarks/src/jmh/java/com/launchdarkly/client/FlagData.java new file mode 100644 index 000000000..f908e80c3 --- /dev/null +++ b/benchmarks/src/jmh/java/com/launchdarkly/client/FlagData.java @@ -0,0 +1,82 @@ +package com.launchdarkly.client; + +import com.launchdarkly.client.value.LDValue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; + +import static com.launchdarkly.client.TestUtil.fallthroughVariation; +import static com.launchdarkly.client.TestUtil.flagWithValue; +import static com.launchdarkly.client.VersionedDataKind.FEATURES; +import static com.launchdarkly.sdk.server.TestValues.BOOLEAN_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.CLAUSE_MATCH_ATTRIBUTE; +import static com.launchdarkly.sdk.server.TestValues.CLAUSE_MATCH_VALUES; +import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_MULTI_VALUE_CLAUSE_KEY; +import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_PREREQ_KEY; +import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_TARGET_LIST_KEY; +import static com.launchdarkly.sdk.server.TestValues.INT_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.JSON_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.STRING_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.TARGETED_USER_KEYS; + +// This class must be in com.launchdarkly.client because FeatureFlagBuilder is package-private in the +// SDK, but we are keeping the rest of the benchmark implementation code in com.launchdarkly.sdk.server +// so we can more clearly compare between 4.x and 5.0. +public class FlagData { + public static void loadTestFlags(FeatureStore store) { + for (FeatureFlag flag: FlagData.makeTestFlags()) { + store.upsert(FEATURES, flag); + } + } + + public static List makeTestFlags() { + List flags = new ArrayList<>(); + + flags.add(flagWithValue(BOOLEAN_FLAG_KEY, LDValue.of(true))); + flags.add(flagWithValue(INT_FLAG_KEY, LDValue.of(1))); + flags.add(flagWithValue(STRING_FLAG_KEY, LDValue.of("x"))); + flags.add(flagWithValue(JSON_FLAG_KEY, LDValue.buildArray().build())); + + FeatureFlag targetsFlag = new FeatureFlagBuilder(FLAG_WITH_TARGET_LIST_KEY) + .on(true) + .targets(Arrays.asList(new Target(new HashSet(TARGETED_USER_KEYS), 1))) + .fallthrough(fallthroughVariation(0)) + .offVariation(0) + .variations(LDValue.of(false), LDValue.of(true)) + .build(); + flags.add(targetsFlag); + + FeatureFlag prereqFlag = new FeatureFlagBuilder("prereq-flag") + .on(true) + .fallthrough(fallthroughVariation(1)) + .variations(LDValue.of(false), LDValue.of(true)) + .build(); + flags.add(prereqFlag); + + FeatureFlag flagWithPrereq = new FeatureFlagBuilder(FLAG_WITH_PREREQ_KEY) + .on(true) + .prerequisites(Arrays.asList(new Prerequisite("prereq-flag", 1))) + .fallthrough(fallthroughVariation(1)) + .offVariation(0) + .variations(LDValue.of(false), LDValue.of(true)) + .build(); + flags.add(flagWithPrereq); + + FeatureFlag flagWithMultiValueClause = new FeatureFlagBuilder(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY) + .on(true) + .fallthrough(fallthroughVariation(0)) + .offVariation(0) + .variations(LDValue.of(false), LDValue.of(true)) + .rules(Arrays.asList( + new RuleBuilder() + .clauses(new Clause(CLAUSE_MATCH_ATTRIBUTE, Operator.in, CLAUSE_MATCH_VALUES, false)) + .build() + )) + .build(); + flags.add(flagWithMultiValueClause); + + return flags; + } +} diff --git a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/EventProcessorBenchmarks.java b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/EventProcessorBenchmarks.java new file mode 100644 index 000000000..a033b1eaf --- /dev/null +++ b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/EventProcessorBenchmarks.java @@ -0,0 +1,137 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.client.Components; +import com.launchdarkly.client.Event; +import com.launchdarkly.client.EventProcessor; +import com.launchdarkly.client.EventProcessorInternals; +import com.launchdarkly.client.LDConfig; +import com.launchdarkly.client.interfaces.EventSender; +import com.launchdarkly.client.interfaces.EventSenderFactory; +import com.launchdarkly.client.interfaces.HttpConfiguration; +import com.launchdarkly.client.value.LDValue; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static com.launchdarkly.sdk.server.TestValues.BASIC_USER; +import static com.launchdarkly.sdk.server.TestValues.CUSTOM_EVENT; +import static com.launchdarkly.sdk.server.TestValues.TEST_EVENTS_COUNT; + +public class EventProcessorBenchmarks { + private static final int EVENT_BUFFER_SIZE = 1000; + private static final int FLAG_COUNT = 10; + private static final int FLAG_VERSIONS = 3; + private static final int FLAG_VARIATIONS = 2; + + @State(Scope.Thread) + public static class BenchmarkInputs { + // Initialization of the things in BenchmarkInputs does not count as part of a benchmark. + final EventProcessor eventProcessor; + final EventSender eventSender; + final List featureRequestEventsWithoutTracking = new ArrayList<>(); + final List featureRequestEventsWithTracking = new ArrayList<>(); + final Random random; + + public BenchmarkInputs() { + // MockEventSender does no I/O - it discards every event payload. So we are benchmarking + // all of the event processing steps up to that point, including the formatting of the + // JSON data in the payload. + eventSender = new MockEventSender(); + + eventProcessor = Components.sendEvents() + .capacity(EVENT_BUFFER_SIZE) + .eventSender(new MockEventSenderFactory()) + .createEventProcessor(TestValues.SDK_KEY, new LDConfig.Builder().build()); + + random = new Random(); + + for (int i = 0; i < TEST_EVENTS_COUNT; i++) { + String flagKey = "flag" + random.nextInt(FLAG_COUNT); + int version = random.nextInt(FLAG_VERSIONS) + 1; + int variation = random.nextInt(FLAG_VARIATIONS); + for (boolean trackEvents: new boolean[] { false, true }) { + Event.FeatureRequest event = new Event.FeatureRequest( + System.currentTimeMillis(), + flagKey, + BASIC_USER, + version, + variation, + LDValue.of(variation), + LDValue.ofNull(), + null, + null, + trackEvents, + null, + false + ); + (trackEvents ? featureRequestEventsWithTracking : featureRequestEventsWithoutTracking).add(event); + } + } + } + + public String randomFlagKey() { + return "flag" + random.nextInt(FLAG_COUNT); + } + + public int randomFlagVersion() { + return random.nextInt(FLAG_VERSIONS) + 1; + } + + public int randomFlagVariation() { + return random.nextInt(FLAG_VARIATIONS); + } + } + + @Benchmark + public void summarizeFeatureRequestEvents(BenchmarkInputs inputs) throws Exception { + for (Event.FeatureRequest event: inputs.featureRequestEventsWithoutTracking) { + inputs.eventProcessor.sendEvent(event); + } + inputs.eventProcessor.flush(); + EventProcessorInternals.waitUntilInactive(inputs.eventProcessor); + } + + @Benchmark + public void featureRequestEventsWithFullTracking(BenchmarkInputs inputs) throws Exception { + for (Event.FeatureRequest event: inputs.featureRequestEventsWithTracking) { + inputs.eventProcessor.sendEvent(event); + } + inputs.eventProcessor.flush(); + EventProcessorInternals.waitUntilInactive(inputs.eventProcessor); + } + + @Benchmark + public void customEvents(BenchmarkInputs inputs) throws Exception { + for (int i = 0; i < TEST_EVENTS_COUNT; i++) { + inputs.eventProcessor.sendEvent(CUSTOM_EVENT); + } + inputs.eventProcessor.flush(); + EventProcessorInternals.waitUntilInactive(inputs.eventProcessor); + } + + private static final class MockEventSender implements EventSender { + private static final Result RESULT = new Result(true, false, null); + + @Override + public void close() throws IOException {} + + @Override + public Result sendEventData(EventDataKind arg0, String arg1, int arg2, URI arg3) { + return RESULT; + } + } + + private static final class MockEventSenderFactory implements EventSenderFactory { + @Override + public EventSender createEventSender(String arg0, HttpConfiguration arg1) { + return new MockEventSender(); + } + } +} diff --git a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java new file mode 100644 index 000000000..2caa89a2e --- /dev/null +++ b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/LDClientEvaluationBenchmarks.java @@ -0,0 +1,156 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.client.Components; +import com.launchdarkly.client.FeatureStore; +import com.launchdarkly.client.FlagData; +import com.launchdarkly.client.LDClient; +import com.launchdarkly.client.LDClientInterface; +import com.launchdarkly.client.LDConfig; +import com.launchdarkly.client.LDUser; +import com.launchdarkly.client.TestUtil; +import com.launchdarkly.client.value.LDValue; + +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; + +import java.util.Random; + +import static com.launchdarkly.sdk.server.TestValues.BOOLEAN_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.CLAUSE_MATCH_VALUE_COUNT; +import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_MULTI_VALUE_CLAUSE_KEY; +import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_PREREQ_KEY; +import static com.launchdarkly.sdk.server.TestValues.FLAG_WITH_TARGET_LIST_KEY; +import static com.launchdarkly.sdk.server.TestValues.INT_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.JSON_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.NOT_MATCHED_VALUE_USER; +import static com.launchdarkly.sdk.server.TestValues.NOT_TARGETED_USER_KEY; +import static com.launchdarkly.sdk.server.TestValues.SDK_KEY; +import static com.launchdarkly.sdk.server.TestValues.STRING_FLAG_KEY; +import static com.launchdarkly.sdk.server.TestValues.TARGETED_USER_KEYS; +import static com.launchdarkly.sdk.server.TestValues.UNKNOWN_FLAG_KEY; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +/** + * These benchmarks cover just the evaluation logic itself (and, by necessity, the overhead of getting the + * flag to be evaluated out of the in-memory store). + */ +public class LDClientEvaluationBenchmarks { + @State(Scope.Thread) + public static class BenchmarkInputs { + // Initialization of the things in BenchmarkInputs does not count as part of a benchmark. + final LDClientInterface client; + final LDUser basicUser; + final Random random; + + public BenchmarkInputs() { + FeatureStore featureStore = TestUtil.initedFeatureStore(); + FlagData.loadTestFlags(featureStore); + + LDConfig config = new LDConfig.Builder() + .dataStore(TestUtil.specificFeatureStore(featureStore)) + .events(Components.noEvents()) + .dataSource(Components.externalUpdatesOnly()) + .build(); + client = new LDClient(SDK_KEY, config); + + basicUser = new LDUser("userkey"); + + random = new Random(); + } + } + + @Benchmark + public void boolVariationForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.boolVariation(BOOLEAN_FLAG_KEY, inputs.basicUser, false); + } + + @Benchmark + public void boolVariationDetailForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.boolVariationDetail(BOOLEAN_FLAG_KEY, inputs.basicUser, false); + } + + @Benchmark + public void boolVariationForUnknownFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.boolVariation(UNKNOWN_FLAG_KEY, inputs.basicUser, false); + } + + @Benchmark + public void intVariationForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.intVariation(INT_FLAG_KEY, inputs.basicUser, 0); + } + + @Benchmark + public void intVariationDetailForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.intVariationDetail(INT_FLAG_KEY, inputs.basicUser, 0); + } + + @Benchmark + public void intVariationForUnknownFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.intVariation(UNKNOWN_FLAG_KEY, inputs.basicUser, 0); + } + + @Benchmark + public void stringVariationForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.stringVariation(STRING_FLAG_KEY, inputs.basicUser, ""); + } + + @Benchmark + public void stringVariationDetailForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.stringVariationDetail(STRING_FLAG_KEY, inputs.basicUser, ""); + } + + @Benchmark + public void stringVariationForUnknownFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.stringVariation(UNKNOWN_FLAG_KEY, inputs.basicUser, ""); + } + + @Benchmark + public void jsonVariationForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.jsonValueVariation(JSON_FLAG_KEY, inputs.basicUser, LDValue.ofNull()); + } + + @Benchmark + public void jsonVariationDetailForSimpleFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.jsonValueVariationDetail(JSON_FLAG_KEY, inputs.basicUser, LDValue.ofNull()); + } + + @Benchmark + public void jsonVariationForUnknownFlag(BenchmarkInputs inputs) throws Exception { + inputs.client.jsonValueVariation(UNKNOWN_FLAG_KEY, inputs.basicUser, LDValue.ofNull()); + } + + @Benchmark + public void userFoundInTargetList(BenchmarkInputs inputs) throws Exception { + String userKey = TARGETED_USER_KEYS.get(inputs.random.nextInt(TARGETED_USER_KEYS.size())); + boolean result = inputs.client.boolVariation(FLAG_WITH_TARGET_LIST_KEY, new LDUser(userKey), false); + assertTrue(result); + } + + @Benchmark + public void userNotFoundInTargetList(BenchmarkInputs inputs) throws Exception { + boolean result = inputs.client.boolVariation(FLAG_WITH_TARGET_LIST_KEY, new LDUser(NOT_TARGETED_USER_KEY), false); + assertFalse(result); + } + + @Benchmark + public void flagWithPrerequisite(BenchmarkInputs inputs) throws Exception { + boolean result = inputs.client.boolVariation(FLAG_WITH_PREREQ_KEY, inputs.basicUser, false); + assertTrue(result); + } + + @Benchmark + public void userValueFoundInClauseList(BenchmarkInputs inputs) throws Exception { + int i = inputs.random.nextInt(CLAUSE_MATCH_VALUE_COUNT); + LDUser user = TestValues.CLAUSE_MATCH_VALUE_USERS.get(i); + boolean result = inputs.client.boolVariation(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY, user, false); + assertTrue(result); + } + + @Benchmark + public void userValueNotFoundInClauseList(BenchmarkInputs inputs) throws Exception { + boolean result = inputs.client.boolVariation(FLAG_WITH_MULTI_VALUE_CLAUSE_KEY, NOT_MATCHED_VALUE_USER, false); + assertFalse(result); + } +} diff --git a/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java new file mode 100644 index 000000000..fa69c41e1 --- /dev/null +++ b/benchmarks/src/jmh/java/com/launchdarkly/sdk/server/TestValues.java @@ -0,0 +1,67 @@ +package com.launchdarkly.sdk.server; + +import com.launchdarkly.client.Event; +import com.launchdarkly.client.LDUser; +import com.launchdarkly.client.value.LDValue; + +import java.util.ArrayList; +import java.util.List; + +public abstract class TestValues { + private TestValues() {} + + public static final String SDK_KEY = "sdk-key"; + + public static final LDUser BASIC_USER = new LDUser("userkey"); + + public static final String BOOLEAN_FLAG_KEY = "flag-bool"; + public static final String INT_FLAG_KEY = "flag-int"; + public static final String STRING_FLAG_KEY = "flag-string"; + public static final String JSON_FLAG_KEY = "flag-json"; + public static final String FLAG_WITH_TARGET_LIST_KEY = "flag-with-targets"; + public static final String FLAG_WITH_PREREQ_KEY = "flag-with-prereq"; + public static final String FLAG_WITH_MULTI_VALUE_CLAUSE_KEY = "flag-with-multi-value-clause"; + public static final String UNKNOWN_FLAG_KEY = "no-such-flag"; + + public static final List TARGETED_USER_KEYS; + static { + TARGETED_USER_KEYS = new ArrayList<>(); + for (int i = 0; i < 1000; i++) { + TARGETED_USER_KEYS.add("user-" + i); + } + } + public static final String NOT_TARGETED_USER_KEY = "no-match"; + + public static final String CLAUSE_MATCH_ATTRIBUTE = "clause-match-attr"; + public static final int CLAUSE_MATCH_VALUE_COUNT = 1000; + public static final List CLAUSE_MATCH_VALUES; + public static final List CLAUSE_MATCH_VALUE_USERS; + static { + // pre-generate all these values and matching users so this work doesn't count in the evaluation benchmark performance + CLAUSE_MATCH_VALUES = new ArrayList<>(CLAUSE_MATCH_VALUE_COUNT); + CLAUSE_MATCH_VALUE_USERS = new ArrayList<>(CLAUSE_MATCH_VALUE_COUNT); + for (int i = 0; i < 1000; i++) { + LDValue value = LDValue.of("value-" + i); + LDUser user = new LDUser.Builder("key").custom(CLAUSE_MATCH_ATTRIBUTE, value).build(); + CLAUSE_MATCH_VALUES.add(value); + CLAUSE_MATCH_VALUE_USERS.add(user); + } + } + public static final LDValue NOT_MATCHED_VALUE = LDValue.of("no-match"); + public static final LDUser NOT_MATCHED_VALUE_USER = + new LDUser.Builder("key").custom(CLAUSE_MATCH_ATTRIBUTE, NOT_MATCHED_VALUE).build(); + + public static final String EMPTY_JSON_DATA = "{\"flags\":{},\"segments\":{}}"; + + public static final int TEST_EVENTS_COUNT = 1000; + + public static final LDValue CUSTOM_EVENT_DATA = LDValue.of("data"); + + public static final Event.Custom CUSTOM_EVENT = new Event.Custom( + System.currentTimeMillis(), + "event-key", + BASIC_USER, + CUSTOM_EVENT_DATA, + null + ); +} diff --git a/src/main/java/com/launchdarkly/client/LDUser.java b/src/main/java/com/launchdarkly/client/LDUser.java index 47d938b1d..0c0fdeb65 100644 --- a/src/main/java/com/launchdarkly/client/LDUser.java +++ b/src/main/java/com/launchdarkly/client/LDUser.java @@ -98,49 +98,49 @@ protected LDValue getValueForEvaluation(String attribute) { } LDValue getKey() { - return key; + return LDValue.normalize(key); } String getKeyAsString() { - return key.stringValue(); + return key == null ? null : key.stringValue(); } // All of the LDValue getters are guaranteed not to return null (although the LDValue may *be* a JSON null). LDValue getIp() { - return ip; + return LDValue.normalize(ip); } LDValue getCountry() { - return country; + return LDValue.normalize(country); } LDValue getSecondary() { - return secondary; + return LDValue.normalize(secondary); } LDValue getName() { - return name; + return LDValue.normalize(name); } LDValue getFirstName() { - return firstName; + return LDValue.normalize(firstName); } LDValue getLastName() { - return lastName; + return LDValue.normalize(lastName); } LDValue getEmail() { - return email; + return LDValue.normalize(email); } LDValue getAvatar() { - return avatar; + return LDValue.normalize(avatar); } LDValue getAnonymous() { - return anonymous; + return LDValue.normalize(anonymous); } LDValue getCustom(String key) { @@ -157,23 +157,24 @@ public boolean equals(Object o) { LDUser ldUser = (LDUser) o; - return Objects.equals(key, ldUser.key) && - Objects.equals(secondary, ldUser.secondary) && - Objects.equals(ip, ldUser.ip) && - Objects.equals(email, ldUser.email) && - Objects.equals(name, ldUser.name) && - Objects.equals(avatar, ldUser.avatar) && - Objects.equals(firstName, ldUser.firstName) && - Objects.equals(lastName, ldUser.lastName) && - Objects.equals(anonymous, ldUser.anonymous) && - Objects.equals(country, ldUser.country) && + return Objects.equals(getKey(), ldUser.getKey()) && + Objects.equals(getSecondary(), ldUser.getSecondary()) && + Objects.equals(getIp(), ldUser.getIp()) && + Objects.equals(getEmail(), ldUser.getEmail()) && + Objects.equals(getName(), ldUser.getName()) && + Objects.equals(getAvatar(), ldUser.getAvatar()) && + Objects.equals(getFirstName(), ldUser.getFirstName()) && + Objects.equals(getLastName(), ldUser.getLastName()) && + Objects.equals(getAnonymous(), ldUser.getAnonymous()) && + Objects.equals(getCountry(), ldUser.getCountry()) && Objects.equals(custom, ldUser.custom) && Objects.equals(privateAttributeNames, ldUser.privateAttributeNames); } @Override public int hashCode() { - return Objects.hash(key, secondary, ip, email, name, avatar, firstName, lastName, anonymous, country, custom, privateAttributeNames); + return Objects.hash(getKey(), getSecondary(), getIp(), getEmail(), getName(), getAvatar(), getFirstName(), + getLastName(), getAnonymous(), getCountry(), custom, privateAttributeNames); } // Used internally when including users in analytics events, to ensure that private attributes are stripped out. diff --git a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java index 7c9fa62d1..55d0c1b05 100644 --- a/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java +++ b/src/test/java/com/launchdarkly/client/LDClientEndToEndTest.java @@ -174,10 +174,10 @@ public void clientSendsDiagnosticEvent() throws Exception { try (LDClient client = new LDClient(sdkKey, config)) { assertTrue(client.initialized()); - } - - RecordedRequest req = server.takeRequest(); - assertEquals("/diagnostic", req.getPath()); + + RecordedRequest req = server.takeRequest(); + assertEquals("/diagnostic", req.getPath()); + } } } diff --git a/src/test/java/com/launchdarkly/client/LDUserTest.java b/src/test/java/com/launchdarkly/client/LDUserTest.java index fd66966f6..49216608b 100644 --- a/src/test/java/com/launchdarkly/client/LDUserTest.java +++ b/src/test/java/com/launchdarkly/client/LDUserTest.java @@ -494,6 +494,14 @@ public void canAddCustomAttrWithListOfMixedValues() { assertEquals(expectedAttr, jo.get("custom")); } + @Test + public void gsonDeserialization() { + for (Map.Entry e: getUserPropertiesJsonMap().entrySet()) { + LDUser user = TEST_GSON_INSTANCE.fromJson(e.getValue(), LDUser.class); + assertEquals(e.getKey(), user); + } + } + private JsonElement makeCustomAttrWithListOfValues(String name, JsonElement... values) { JsonObject ret = new JsonObject(); JsonArray a = new JsonArray();