Skip to content

Batch metrics: compact captures#19265

Open
yaauie wants to merge 2 commits into
elastic:mainfrom
yaauie:batch-metrics-compact-captures
Open

Batch metrics: compact captures#19265
yaauie wants to merge 2 commits into
elastic:mainfrom
yaauie:batch-metrics-compact-captures

Conversation

@yaauie

@yaauie yaauie commented Jul 1, 2026

Copy link
Copy Markdown
Member

Release notes

  • reduces memory overhead of queue batch metrics

What does this PR do?

Uses PackedHistogram to store histogram captures in a significantly more compact manner, which is safe because the captured histograms are NOT modified once captured.

  • ConcurrentHistogram carries double overhead so that it can isolate "active" and "inactive" sets; we don't need this since our captures are never updated.
  • PackedHistogram is very compact and recommended for sparse values, with a note that the resize and re-pack operations can be slower; this doesn't affect us because we never update the values once created.

Why is it important/What is the impact to the user?

Significantly reduces the memory overhead of storing batch metrics.

Checklist

  • My code follows the style guidelines of this project
  • I have commented my code, particularly in hard-to-understand areas
  • [ ] I have made corresponding changes to the documentation
  • [ ] I have made corresponding change to the default configuration files (and/or docker env variables)
  • I have added tests that prove my fix is effective or that my feature works

Author's Checklist

  • [ ]

How to test this PR locally

  1. Run Logstash from main with a generator:
    bin/logstash -w24 -b10000 -e 'input { java_generator { threads => 1 } } output { sink {} }'
    
  2. Wait 5+ minutes
  3. Take a heap dump
  4. Search the heap for instances of org.logstash.instrument.metrics.HistogramCapture
  5. Observe the retained heap
  6. repeat steps 1-5 with this patch
  7. Compare

In my local testing where batches were consistently full with 10,000 events (truly optimal), the size per capture was 848b when packed vs 82,500b without this patch. Since we need to keep hundreds of captures for each metric on each pipeline to satisfy the retention windows, this space savings is multiplied significantly in the real world.

When the values are less sparse, the savings will still be at least 50% since we're not carrying the concurrent update overhead.

Related issues

Relates: #18243

Use cases

On a host with many pipelines, batch metrics can occupy a significant amount of memory. By using a more compact representation for unmodified captures, we can reduce the overall memory footprint.

yaauie added 2 commits July 1, 2026 20:32
By default, HdrHistogram's `Recorder` uses ConcurrentHistogram internally to
ensure concurrency-safety with regard to updates, but that means that the
values produced by `Recorder#getIntervalHistogram` carry all of the
concurrency-safety overhead of that implementation.

We already make copies of these histograms for storage in captures, and we
do NOT update these objects once they are created; we can safely use the
lower overhead basic `Histogram` implementation.
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

🤖 GitHub comments

Just comment with:

  • run docs-build : Re-trigger the docs validation. (use unformatted text in the comment!)
  • run exhaustive tests : Run the exhaustive tests Buildkite pipeline.

@mergify

mergify Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

This pull request does not have a backport label. Could you fix it @yaauie? 🙏
To fixup this pull request, you need to add the backport labels for the needed
branches, such as:

  • backport-8./d is the label to automatically backport to the 8./d branch. /d is the digit.
  • If no backport is necessary, please add the backport-skip label

@yaauie yaauie added the backport-active-9 Automated backport with mergify to all the active 9.[0-9]+ branches label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

TL;DR

Java unit tests are failing because BatchStructureMetricTest.givenInstantiatedStructureMetricInstanceThenVerifyOccupationEstimation still expects batch metric footprint estimates to be based on Recorder#getIntervalHistogram()'s unpacked/concurrent histogram size, but this PR changes LifetimeHistogramMetric to report the much smaller packed snapshot size.

Remediation

  • Update logstash-core/src/test/java/org/logstash/instrument/metrics/BatchStructureMetricTest.java:178-194 so the expected footprint is derived from the same representation now used by LifetimeHistogramMetric.estimateSingleHistogramFootprintInBytes() instead of a raw new Recorder(3).getIntervalHistogram() sample.
  • Validate with ./gradlew :logstash-core:javaTests --tests org.logstash.instrument.metrics.BatchStructureMetricTest --tests org.logstash.instrument.metrics.histogram.LifetimeHistogramMetricTest.
Investigation details

Root Cause

The PR changes LifetimeHistogramMetric so lifetimeSnapshot is initialized and maintained as a packed copy, and estimateSingleHistogramFootprintInBytes() reports this.lifetimeSnapshot.getEstimatedFootprintInBytes():

  • logstash-core/src/main/java/org/logstash/instrument/metrics/histogram/LifetimeHistogramMetric.java: constructor now uses compactCopy(recorder.getIntervalHistogram()).
  • logstash-core/src/main/java/org/logstash/instrument/metrics/histogram/LifetimeHistogramMetric.java: compactCopy creates new PackedHistogram(source).

But BatchStructureMetricTest still computes the expected value from a Recorder interval histogram:

Histogram sample = new Recorder(3).getIntervalHistogram();
int sampleOccupation = sample.getEstimatedFootprintInBytes();
...
int expectedOccupation = sampleOccupation * totalDatapoints;
assertEquals(expectedOccupation, sut.estimateBatchMetricsFootprintInBytes());

That expectation no longer matches the implementation, because sut.estimateBatchMetricsFootprintInBytes() delegates to LifetimeHistogramMetric.estimateSingleHistogramFootprintInBytes(), which now reflects the compact packed snapshot.

Evidence

BatchStructureMetricTest > givenInstantiatedStructureMetricInstanceThenVerifyOccupationEstimation FAILED
    java.lang.AssertionError: expected:<2496000> but was:<24000>
        at org.logstash.instrument.metrics.BatchStructureMetricTest.givenInstantiatedStructureMetricInstanceThenVerifyOccupationEstimation(BatchStructureMetricTest.java:194)

Verification

  • ./gradlew :logstash-core:javaTests --tests org.logstash.instrument.metrics.histogram.LifetimeHistogramMetricTest --no-daemon --console=plain --quiet passes: 4 tests, 0 failures.
  • ./gradlew :logstash-core:javaTests --tests org.logstash.instrument.metrics.BatchStructureMetricTest --no-daemon --console=plain reproduces the failure above: 1 failure in givenInstantiatedStructureMetricInstanceThenVerifyOccupationEstimation.

Follow-up

The prefetched Buildkite log for this job only contained the artifact-upload tail, so the assertion excerpt above is from a targeted local reproduction of the PR patch.


What is this? | From workflow: PR Buildkite Detective

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@yaauie

yaauie commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

Both test failures are around calculating the effective total storage size. I'll follow up with fixes to both:

  • Ruby Unit tests: ./logstash-core/spec/logstash/java_pipeline_spec.rb:1280
  • Java Unit tests: org.logstash.instrument.metrics.BatchStructureMetricTest.givenInstantiatedStructureMetricInstanceThenVerifyOccupationEstimation(BatchStructureMetricTest.java:194)

@jsvd

jsvd commented Jul 2, 2026

Copy link
Copy Markdown
Member

I think the production formula itself is now wrong: 3 * estimateSingleHistogramFootprintInBytes was written to approximate the recorder's 2 live histograms + 1 snapshot and that only worked because all three used to be the same (unpacked) size class. Now the snapshot shrank ~100x while the other two stayed full-size.

This will likely lead to incorrect sizes, which is what the failing tests are signaling.

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

Labels

backport-active-9 Automated backport with mergify to all the active 9.[0-9]+ branches

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants