Skip to content

feat: Add opt-in release of in-clause value lists - #238

Closed
joker23 wants to merge 4 commits into
mainfrom
skz/sdk-3232/list-set-optimization
Closed

joker23 wants to merge 4 commits into
mainfrom
skz/sdk-3232/list-set-optimization

Conversation

@joker23

@joker23 joker23 commented Oct 15, 2026 •

Copy link
Copy Markdown

Summary

An in clause with more than one value gets an ImmutableSet during preprocessing, and the clause also keeps its value list. The SDK then holds every value twice. An external report against the Go SDK measured 31.6 MB of clause value lists, about 30% of one service's live heap (launchdarkly/go-server-sdk-evaluation#69). This PR is the Java port of the Go fix (launchdarkly/go-server-sdk-evaluation#72).

Two findings and one change:

  • Threshold: Java keeps 2. A JMH benchmark of the real match path (EvaluatorHelpers.matchClauseWithoutSegments) shows the set lookup is faster than a list scan at every size of two or more values, hit and miss, hot and cold cache. Go raised its threshold to 8 because a Go map lookup costs about 30 ns; Java's ImmutableSet.contains costs 4-5 ns at every size, because String caches its hash code. So the existing build-at-2+ rule is already right for Java, and this PR does not change it. Results (Apple Silicon, Zulu JDK 17, ns per match):

    values list hit list miss set hit set miss
    2 7.7 8.2 4.6 4.0
    4 11.3 14.2 4.5 4.3
    8 19.0 26.0 5.1 4.0
    16 31.2 52.1 4.5 3.8
    64 108 201 4.8 5.0
    1024 1607 3232 5.2 5.7
  • Opt-in release of the duplicate list. With the system property com.launchdarkly.sdk.server.releaseClauseValues=true, the preprocessor drops the value list of each in clause that has a value set, so memory holds one copy instead of two. Evaluation and JSON serialization then read the values from the set. ImmutableSet keeps first-appearance order, so a released clause serializes its values in the original order, but a duplicate value is dropped.

  • The default is unchanged. Without the property, the list is kept and the serialized form of a clause stays byte-for-byte identical. The release is opt-in because re-serialization (for example, to a persistent data store) would silently drop duplicate values. Other operators always keep their list: their evaluation reads values by position, and segmentMatch reads the list as segment keys.

The switch is a system property because afterDeserialized() is a static Gson hook with no access to LDConfig. Per-client config plumbing can be follow-up work, as it was for Go.

🤖 Generated with Claude Code


Note

Overview
Adds an opt-in memory optimization for in clauses: when com.launchdarkly.sdk.server.releaseClauseValues=true, preprocessing replaces each multi-value clause’s duplicate value list with the preprocessed ImmutableSet’s list view so only one copy of the values is retained.

DataModel.Clause now allows that swap via releaseValues() (the values field is no longer final). Default behavior is unchanged—without the property, lists and serialized JSON stay as received. The release applies only to in clauses that get a value set (still at 2+ values); other operators and single-value in clauses are untouched because evaluation or serialization depends on the original list.

Tests cover default vs enabled release, segment rules, serialization order/deduping, and that matching still works (including segmentMatch).

Reviewed by Cursor Bugbot for commit 25d8e2d. Bugbot is set up for automated code reviews on this repo. Configure here.


Devin Review

@joker23
joker23 marked this pull request as ready for review October 15, 2026 20:31
@joker23
joker23 requested a review from a team as a code owner October 15, 2026 20:31
devin-ai-integration[bot]

This comment was marked as resolved.

* The SDK reads the property once, at class load. The default is off. With the default, a
* clause serializes its values exactly as received.
*/
static final String RELEASE_CLAUSE_VALUES_PROPERTY = "com.launchdarkly.sdk.server.releaseClauseValues";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Is there a reason we are making this opt-in?

@joker23 joker23 Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I followed the decision in launchdarkly/go-server-sdk-evaluation#72 , though opting in with system props might not be as safe as it is in go?

UPDATE: I just surrounded the system property with a try catch and made it fallback to false.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I also think it is fine if we break from go precedence and just change the behavior outright.

@joker23

joker23 commented Oct 16, 2026

Copy link
Copy Markdown
Author

Closing as Java's collections backing is just a list of pointers so this optimization is much less impactful as the go optimization.

@joker23 joker23 closed this Oct 16, 2026
@joker23
joker23 deleted the skz/sdk-3232/list-set-optimization branch October 16, 2026 22:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants