Skip to content

Replace sampled old-key eviction with generational object store - #101

Draft
mcculls wants to merge 1 commit into
mainfrom
mcculls/generation-objectstore
Draft

Replace sampled old-key eviction with generational object store#101
mcculls wants to merge 1 commit into
mainfrom
mcculls/generation-objectstore

Conversation

@mcculls

@mcculls mcculls commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

What Does This Do

Previously we sampled a small set of "old" keys to approximate LRU-ish eviction. This degraded under sustained load and didn't scale well with GLOBAL_HARD_LIMIT. We're replacing it with a young/old generational design: writes land in a young concurrent map that ages into the old map at a fixed threshold. Both periodic and inline paths trim the old generation.

Motivation

Works better with DataDog/dd-trace-java#10479

Contributor Checklist

Jira ticket: [PROJ-IDENT]

@datadog-prod-us1-5

This comment has been minimized.

@mcculls
mcculls force-pushed the mcculls/generation-objectstore branch from b2d627a to abf0d61 Compare July 31, 2026 01:39
@mcculls
mcculls requested a review from Copilot July 31, 2026 01:41
@DataDog DataDog deleted a comment from chatgpt-codex-connector Bot Jul 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR replaces the previous sampled “old key” eviction approach in the field-injection global object store with a two-generation (young/old) design intended to behave more predictably under sustained load and large capacity limits.

Changes:

  • Introduces a young/old generational ConcurrentHashMap design in GlobalObjectStore, including ageing and inline/periodic old-generation trimming.
  • Updates lookup/write paths to read from young first, then old; writes land in young with capacity enforcement hooks.
  • Adjusts the JMH benchmark allocator to simulate non-trivial allocation cost by filling allocated buffers with random bytes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
field-inject/src/main/java/datadog/instrument/fieldinject/GlobalObjectStore.java Replaces sampled-key eviction with a young/old generational store and new capacity enforcement logic.
field-inject/src/jmh/java/datadog/instrument/fieldinject/ObjectStoreBenchmark.java Makes the benchmark allocator more realistic by adding random-byte initialization.
Comments suppressed due to low confidence (1)

field-inject/src/main/java/datadog/instrument/fieldinject/GlobalObjectStore.java:209

  • Inline stale eviction also removes stale keys only from the captured g snapshot. For the same reason as in removeStaleEntries(), if a StoreKey from a newer generation is polled, it can be dropped without removing the corresponding map entry.
    int attempts = MAX_INLINE_EVICTION_ATTEMPTS;
    while (attempts-- > 0 && (staleKey = StoreKey.pollStaleKeys()) != null) {
      if (removeEntry(g, staleKey) != null) {
        return;
      }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@mcculls
mcculls force-pushed the mcculls/generation-objectstore branch from abf0d61 to ad7fa7c Compare July 31, 2026 02:01
@mcculls
mcculls requested a review from Copilot July 31, 2026 02:02

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (1)

field-inject/src/main/java/datadog/instrument/fieldinject/GlobalObjectStore.java:201

  • The new generational capacity/eviction behavior (inline stale eviction, ageing, hard-limit old eviction) is not currently covered by unit tests. Existing tests cover basic CRUD/GC cleanup via ObjectStoreTest, but none assert behavior near GLOBAL_SOFT_LIMIT/GLOBAL_HARD_LIMIT or the ageing path.

Add a focused test that drives the store above the thresholds (e.g., by inserting many distinct keys and triggering removeStaleEntries() / inline enforcement) and asserts that size is bounded and entries are evicted as expected.

  private static void enforceCapacity() {
    Generations g = generations;
    int youngSize = g.young.size();
    int totalSize = youngSize + g.old.size();
    if (totalSize < INLINE_CLEANUP_THRESHOLD) {

Previously we sampled a small set of "old" keys to approximate LRU-ish eviction.
This degraded under sustained load and didn't scale well with GLOBAL_HARD_LIMIT.
We're replacing it with a young/old generational design: writes land in a young
concurrent map that ages into the old map at a fixed threshold. Both periodic
and inline paths trim the old generation.
@mcculls
mcculls force-pushed the mcculls/generation-objectstore branch from ad7fa7c to a8ce760 Compare July 31, 2026 02:25
@mcculls
mcculls requested a review from Copilot July 31, 2026 02:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.

Suppressed comments (3)

field-inject/src/main/java/datadog/instrument/fieldinject/GlobalObjectStore.java:84

  • In removeStaleEntries(), the soft-limit trim decrements estimatedSize unconditionally after Iterator.remove(). Under concurrent modifications, the iterator removal may be a no-op (or race with another removal), which can cause the loop to stop early and report/leave the store above GLOBAL_SOFT_LIMIT. Prefer removing by key and only decrementing when a mapping was actually removed.
      while (estimatedSize >= GLOBAL_SOFT_LIMIT && itr.hasNext()) {
        itr.next();
        itr.remove();
        estimatedSize--;
      }

field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreTest.java:238

  • This test allocates and retains 240k distinct keys, which is a relatively expensive unit test (time + heap) for exercising the generational thresholds. The same scenario (age at least once and end above the soft limit) can be covered with fewer inserts while still validating bounded growth.
    int totalInserts = (AGEING_THRESHOLD * 4) + 40_000;

field-inject/src/test/java/datadog/instrument/fieldinject/ObjectStoreTest.java:259

  • This test leaves up to ~GLOBAL_SOFT_LIMIT live entries in the static GlobalObjectStore after it completes (the @BeforeEach only drains stale entries). That can make later tests in the same JVM observe a pre-filled store and can change eviction behavior. Consider explicitly removing the entries created by this test before returning.
    int finalSize = ObjectStore.removeStaleEntries();
    assertTrue(
        finalSize < GLOBAL_HARD_LIMIT,
        "Sustained insertion should have triggered eviction rather than unbounded growth");
    assertTrue(

@mcculls
mcculls marked this pull request as ready for review July 31, 2026 02:33
@mcculls
mcculls requested a review from a team as a code owner July 31, 2026 02:33
@mcculls
mcculls requested a review from ygree July 31, 2026 02:33
@mcculls
mcculls marked this pull request as draft July 31, 2026 10:08
@mcculls
mcculls removed the request for review from ygree July 31, 2026 10:08
@mcculls

mcculls commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator Author

Current state works well in DataDog/dd-trace-java#12119 but too close to release to merge (also needs a feature flag to toggle between the two approaches)

Moving back to draft for now - when I get back I'll look at optimizing the get path to improve inlinability and reduce volatile reads to a minimum.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants