From acd352d2eb60665e7fc82dd93bca3966bec07d12 Mon Sep 17 00:00:00 2001 From: James Yuzawa Date: Tue, 26 Aug 2025 17:21:24 -0400 Subject: [PATCH 1/3] optimize EvaluatorBucketing --- .../sdk/server/EvaluatorBucketing.java | 38 ++++++++++++------- .../sdk/server/EvaluatorBucketingTest.java | 14 +++++++ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java index 2a96663..5b55e24 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java @@ -47,30 +47,42 @@ static float computeBucketValue( } } - String idHash = getBucketableStringValue(contextValue); - if (idHash == null) { + StringBuilder keyBuilder = new StringBuilder(); + if (seed != null) { + keyBuilder.append(seed.intValue()); + } else { + keyBuilder.append(flagOrSegmentKey).append('.').append(salt); + } + keyBuilder.append('.'); + if (!getBucketableStringValue(keyBuilder, contextValue)) { return 0; } - String prefix; - if (seed != null) { - prefix = seed.toString(); - } else { - prefix = flagOrSegmentKey + "." + salt; + // turn the first 15 hex digits of this into a long + byte[] hash = DigestUtils.sha1(keyBuilder.toString()); + long longVal = 0; + for (int i = 0; i < 7; i++) { + longVal <<= 8; + longVal |= (hash[i] & 0xff); } - String hash = DigestUtils.sha1Hex(prefix + "." + idHash).substring(0, 15); - long longVal = Long.parseLong(hash, 16); + longVal <<= 4; + longVal |= ((hash[7] >> 4) & 0xf); return (float) longVal / LONG_SCALE; } - private static String getBucketableStringValue(LDValue userValue) { + private static boolean getBucketableStringValue(StringBuilder keyBuilder, LDValue userValue) { switch (userValue.getType()) { case STRING: - return userValue.stringValue(); + keyBuilder.append(userValue.stringValue()); + return true; case NUMBER: - return userValue.isInt() ? String.valueOf(userValue.intValue()) : null; + if (userValue.isInt()) { + keyBuilder.append(userValue.intValue()); + return true; + } + return false; default: - return null; + return false; } } } diff --git a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java index 6e0a164..72e82c4 100644 --- a/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java +++ b/lib/sdk/server/src/test/java/com/launchdarkly/sdk/server/EvaluatorBucketingTest.java @@ -8,6 +8,8 @@ import com.launchdarkly.sdk.server.DataModel.RolloutKind; import com.launchdarkly.sdk.server.DataModel.WeightedVariation; +import org.apache.commons.codec.digest.DigestUtils; + import org.junit.Test; import java.util.Arrays; @@ -132,6 +134,18 @@ public void cannotBucketByBooleanAttribute() { assertEquals(0f, result, Float.MIN_VALUE); } + @Test + public void optimizedHashText() { + LDContext context = LDContext.builder("key") + .set("stringattr", "33333") + .build(); + float result = computeBucketValue(false, noSeed, context, null, "key", AttributeRef.fromLiteral("stringattr"), "salt"); + String hash = DigestUtils.sha1Hex("key.salt.33333").substring(0, 15); + long longVal = Long.parseLong(hash, 16); + float expectedResult = longVal / (float) 0xFFFFFFFFFFFFFFFL; + assertEquals(expectedResult, result, Float.MIN_VALUE); + } + private static void assertVariationIndexFromRollout( int expectedVariation, Rollout rollout, From 59247ee9ed77571ec553ad165ed6a3e3dfb52267 Mon Sep 17 00:00:00 2001 From: James Yuzawa Date: Tue, 26 Aug 2025 18:05:14 -0400 Subject: [PATCH 2/3] avoid intermediate copy from StringBuilder --- .../com/launchdarkly/sdk/server/EvaluatorBucketing.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java index 5b55e24..925b408 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java @@ -7,6 +7,10 @@ import org.apache.commons.codec.digest.DigestUtils; +import java.nio.charset.StandardCharsets; +import java.nio.CharBuffer; +import java.security.MessageDigest; + /** * Encapsulates the logic for percentage rollouts. */ @@ -59,7 +63,9 @@ static float computeBucketValue( } // turn the first 15 hex digits of this into a long - byte[] hash = DigestUtils.sha1(keyBuilder.toString()); + MessageDigest digest = DigestUtils.getSha1Digest(); + digest.update(StandardCharsets.UTF_8.encode(CharBuffer.wrap(keyBuilder))); + byte[] hash = digest.digest(); long longVal = 0; for (int i = 0; i < 7; i++) { longVal <<= 8; From 21716ebe3c7a4f8a6194ac7e8785883c6a0a49d0 Mon Sep 17 00:00:00 2001 From: James Yuzawa Date: Wed, 27 Aug 2025 13:11:54 -0400 Subject: [PATCH 3/3] Revert "avoid intermediate copy from StringBuilder" This reverts commit 59247ee9ed77571ec553ad165ed6a3e3dfb52267. --- .../com/launchdarkly/sdk/server/EvaluatorBucketing.java | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java index 925b408..5b55e24 100644 --- a/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java +++ b/lib/sdk/server/src/main/java/com/launchdarkly/sdk/server/EvaluatorBucketing.java @@ -7,10 +7,6 @@ import org.apache.commons.codec.digest.DigestUtils; -import java.nio.charset.StandardCharsets; -import java.nio.CharBuffer; -import java.security.MessageDigest; - /** * Encapsulates the logic for percentage rollouts. */ @@ -63,9 +59,7 @@ static float computeBucketValue( } // turn the first 15 hex digits of this into a long - MessageDigest digest = DigestUtils.getSha1Digest(); - digest.update(StandardCharsets.UTF_8.encode(CharBuffer.wrap(keyBuilder))); - byte[] hash = digest.digest(); + byte[] hash = DigestUtils.sha1(keyBuilder.toString()); long longVal = 0; for (int i = 0; i < 7; i++) { longVal <<= 8;