Skip to content

perf: optimize classic-only histogram observe path#2252

Open
arnabnandy7 wants to merge 12 commits into
prometheus:mainfrom
arnabnandy7:perf/synchronized-histogram
Open

perf: optimize classic-only histogram observe path#2252
arnabnandy7 wants to merge 12 commits into
prometheus:mainfrom
arnabnandy7:perf/synchronized-histogram

Conversation

@arnabnandy7

@arnabnandy7 arnabnandy7 commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

Optimizes Histogram.classicOnly() observations by adding a dedicated classic-only fast path that uses synchronized plain fields instead of the native/hybrid LongAdder/DoubleAdder plus buffer path.

Fixes #1915

Changes

  • Added classic-only storage for bucket counts, total count, and sum in Histogram.DataPoint.
  • Routed observe() and observeWithExemplar() through the classic-only fast path when nativeInitialSchema == CLASSIC_HISTOGRAM.
  • Kept native-only and hybrid histograms on the existing buffer/aduder implementation.
  • Added multithreaded coverage for classic-only observe/collect behavior.

Testing

  • .\mvnw compile -pl prometheus-metrics-core -Dcoverage.skip=true -Dcheckstyle.skip=true
  • git diff --check

Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
@arnabnandy7 arnabnandy7 force-pushed the perf/synchronized-histogram branch from 8f7d299 to 591d6ad Compare June 25, 2026 21:07
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

⚠️ API changes detected — maintainer review required

This PR modifies the published API diff for the following module(s):

  • prometheus-metrics-core

Please review the changes in docs/apidiffs/current_vs_latest/ carefully before approving.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
@zeitlinger zeitlinger force-pushed the perf/synchronized-histogram branch from 2dea810 to e539344 Compare July 2, 2026 14:42
zeitlinger and others added 2 commits July 3, 2026 07:35
zeitlinger added a commit that referenced this pull request Jul 7, 2026
## Summary

Extract the PR benchmark workflows from #2252 into a standalone PR so
they can land on `main` first.

## Why

The benchmark job uploads a Markdown artifact with the PR comment
contents, but the actual PR comment is posted by a separate
`workflow_run` workflow. GitHub only triggers `workflow_run` workflows
when the workflow file already exists on the default branch, so keeping
the reporter in the same PR prevents it from commenting on that PR's
benchmark run.

Splitting these workflows out first lets subsequent benchmark PRs use
the comment-reporting flow as intended.

## Changes

- add `PR Benchmarks` workflow triggered by the `benchmark` label on
pull requests
- add `PR Benchmark Report` workflow triggered by `workflow_run`
- keep the benchmark summary as an uploaded artifact consumed by the
reporter workflow

## Testing

- `mise run lint:fix`
zeitlinger added a commit that referenced this pull request Jul 8, 2026
## Summary

The `PR Benchmark Report` workflow failed to comment on
[#2252](#2252) after a
successful benchmark run ([run
28865892835](https://github.com/prometheus/client_java/actions/runs/28865892835)):

```
gh: Not Found (HTTP 404)
parse "https://api.github.com/repos/prometheus/client_java/issues/comments/{\r": net/url: invalid control character in URL
```

Root cause: `workflow_run.pull_requests` is empty when the triggering
run's head branch is not on the default branch (or the PR is from a
fork). With `PR_NUMBER` empty, the subsequent
`repos/${REPO}/issues//comments` API call 404s and the paginator emits a
malformed URL, so no comment gets posted.

## Changes

- Fall back to `repos/{owner}/{repo}/commits/{sha}/pulls` to resolve the
PR from the head SHA when `workflow_run.pull_requests` is empty.
- Fail fast with an explicit error if the PR still can't be resolved, so
future regressions don't cascade into cryptic URL-parse errors.

## Test plan

- [ ] Re-run the report workflow against run 28861406560 (or a fresh
push to #2252) and confirm a benchmark comment lands on the PR.

Signed-off-by: Gregor Zeitlinger <gregor.zeitlinger@grafana.com>
zeitlinger added a commit that referenced this pull request Jul 9, 2026
## Summary

Fix the `PR Benchmark Report` workflow for forked PR benchmark runs like
#2252, where `workflow_run.pull_requests` and `commits/{sha}/pulls` can
both be empty for the upstream repository.

## Changes

- Resolve the PR number from the uploaded benchmark artifact names
first.
- Fall back to the workflow run head repository owner and branch via
`repos/{owner}/{repo}/pulls`.
- Keep the existing commit-SHA lookup as a final fallback.
- Remove the artifact ZIP download `Accept: application/octet-stream`
header because `gh api` rejected it during local replay.

## Validation

- Replayed the resolution logic against benchmark run `28928458470` and
confirmed it resolves PR #2252 and downloads artifact `8164318312`.
- Ran `mise run lint:fix`.
- Ran `git diff --check`.
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Benchmark results

Benchmark run succeeded for 3667a0e64a8ca957ec1bc989a00cce602f4f778f.

Prometheus Java Client Benchmarks

Run Information

  • Date: 2026-07-10T17:15:46Z
  • Commit: 3667a0e
  • JDK: 25.0.3 (OpenJDK 64-Bit Server VM)
  • Benchmark config: 3 fork(s), 3 warmup, 5 measurement, 4 threads
  • Hardware: AMD EPYC 7763 64-Core Processor, 4 cores, 16 GB RAM
  • OS: Linux 6.17.0-1018-azure

Comparison with base

  • Head: 3667a0e
  • Base: 1bb6c6d
  • Change: positive means the PR is faster than base.
  • Head runner: AMD EPYC 7763 64-Core Processor, 4 cores, 16 GB RAM, Linux 6.17.0-1018-azure
  • Base runner: AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GB RAM, Linux 6.17.0-1018-azure
  • Note: base and head run in parallel jobs, so runner hardware can differ and affect results.
Benchmark PR Base Change Result
CounterBenchmark.codahaleIncNoLabels 49.57K 44.54K +11.3% faster
CounterBenchmark.openTelemetryAdd 3.36K 3.85K -12.7% within noise
CounterBenchmark.openTelemetryInc 3.20K 4.39K -27.1% within noise
CounterBenchmark.openTelemetryIncNoLabels 2.90K 5.34K -45.8% slower
CounterBenchmark.prometheusAdd 51.64K 47.89K +7.8% faster
CounterBenchmark.prometheusInc 66.29K 59.31K +11.8% faster
CounterBenchmark.prometheusNoLabelsInc 56.18K 51.43K +9.2% faster
CounterBenchmark.simpleclientAdd 6.20K 5.82K +6.4% within noise
CounterBenchmark.simpleclientInc 6.59K 6.14K +7.2% faster
CounterBenchmark.simpleclientNoLabelsInc 6.34K 6.14K +3.1% within noise
HistogramBenchmark.openTelemetryClassic 745.31 693.20 +7.5% faster
HistogramBenchmark.openTelemetryExponential 583.88 532.37 +9.7% within noise
HistogramBenchmark.prometheusClassic 1.27K 5.09K -75.0% slower
HistogramBenchmark.prometheusNative 2.75K 2.94K -6.5% within noise
HistogramBenchmark.simpleclient 4.44K 4.52K -1.6% within noise
HistogramTextFormatBenchmark.openMetricsWriteToNull 23.17K 27.54K -15.9% slower
HistogramTextFormatBenchmark.prometheusWriteToNull 23.74K 27.41K -13.4% slower
TextFormatUtilBenchmark.openMetricsWriteToByteArray 476.49K 527.51K -9.7% slower
TextFormatUtilBenchmark.openMetricsWriteToNull 485.06K 530.46K -8.6% slower
TextFormatUtilBenchmark.prometheusWriteToByteArray 507.66K 555.50K -8.6% slower
TextFormatUtilBenchmark.prometheusWriteToNull 512.26K 566.96K -9.6% slower

Results for PR head

CounterBenchmark

Benchmark Score Error Units Within run
prometheusInc 66.29K ± 308.57 ops/s fastest
prometheusNoLabelsInc 56.18K ± 912.82 ops/s 1.2x slower
prometheusAdd 51.64K ± 175.39 ops/s 1.3x slower
codahaleIncNoLabels 49.57K ± 1.94K ops/s 1.3x slower
simpleclientInc 6.59K ± 16.14 ops/s 10x slower
simpleclientNoLabelsInc 6.34K ± 62.04 ops/s 10x slower
simpleclientAdd 6.20K ± 223.79 ops/s 11x slower
openTelemetryAdd 3.36K ± 294.12 ops/s 20x slower
openTelemetryInc 3.20K ± 165.28 ops/s 21x slower
openTelemetryIncNoLabels 2.90K ± 44.63 ops/s 23x slower

HistogramBenchmark

Benchmark Score Error Units Within run
simpleclient 4.44K ± 36.60 ops/s fastest
prometheusNative 2.75K ± 319.16 ops/s 1.6x slower
prometheusClassic 1.27K ± 110.63 ops/s 3.5x slower
openTelemetryClassic 745.31 ± 12.87 ops/s 6.0x slower
openTelemetryExponential 583.88 ± 30.76 ops/s 7.6x slower

HistogramTextFormatBenchmark

Benchmark Score Error Units Within run
prometheusWriteToNull 23.74K ± 702.85 ops/s fastest
openMetricsWriteToNull 23.17K ± 588.99 ops/s 1.0x slower

TextFormatUtilBenchmark

Benchmark Score Error Units Within run
prometheusWriteToNull 512.26K ± 3.67K ops/s fastest
prometheusWriteToByteArray 507.66K ± 6.68K ops/s 1.0x slower
openMetricsWriteToNull 485.06K ± 4.27K ops/s 1.1x slower
openMetricsWriteToByteArray 476.49K ± 5.18K ops/s 1.1x slower

Raw Results

Benchmark                                            Mode  Cnt          Score        Error  Units
CounterBenchmark.codahaleIncNoLabels                thrpt   15      49571.254   ± 1936.403  ops/s
CounterBenchmark.openTelemetryAdd                   thrpt   15       3361.300    ± 294.116  ops/s
CounterBenchmark.openTelemetryInc                   thrpt   15       3199.321    ± 165.282  ops/s
CounterBenchmark.openTelemetryIncNoLabels           thrpt   15       2896.148     ± 44.627  ops/s
CounterBenchmark.prometheusAdd                      thrpt   15      51635.534    ± 175.386  ops/s
CounterBenchmark.prometheusInc                      thrpt   15      66288.247    ± 308.570  ops/s
CounterBenchmark.prometheusNoLabelsInc              thrpt   15      56175.118    ± 912.817  ops/s
CounterBenchmark.simpleclientAdd                    thrpt   15       6196.410    ± 223.792  ops/s
CounterBenchmark.simpleclientInc                    thrpt   15       6585.392     ± 16.142  ops/s
CounterBenchmark.simpleclientNoLabelsInc            thrpt   15       6336.154     ± 62.037  ops/s
HistogramBenchmark.openTelemetryClassic             thrpt   15        745.309     ± 12.871  ops/s
HistogramBenchmark.openTelemetryExponential         thrpt   15        583.878     ± 30.756  ops/s
HistogramBenchmark.prometheusClassic                thrpt   15       1272.779    ± 110.632  ops/s
HistogramBenchmark.prometheusNative                 thrpt   15       2747.476    ± 319.161  ops/s
HistogramBenchmark.simpleclient                     thrpt   15       4443.076     ± 36.600  ops/s
HistogramTextFormatBenchmark.openMetricsWriteToNull  thrpt   15      23166.549    ± 588.995  ops/s
HistogramTextFormatBenchmark.prometheusWriteToNull  thrpt   15      23735.767    ± 702.851  ops/s
TextFormatUtilBenchmark.openMetricsWriteToByteArray  thrpt   15     476492.808   ± 5175.613  ops/s
TextFormatUtilBenchmark.openMetricsWriteToNull      thrpt   15     485064.441   ± 4265.006  ops/s
TextFormatUtilBenchmark.prometheusWriteToByteArray  thrpt   15     507655.194   ± 6684.238  ops/s
TextFormatUtilBenchmark.prometheusWriteToNull       thrpt   15     512257.686   ± 3665.166  ops/s

Notes

  • Score = Throughput in operations per second (higher is better)
  • Error = 99.9% confidence interval
  • Comparison with base uses JMH confidence intervals when available; overlapping intervals are marked "within noise".
  • Within run compares benchmarks in the same result set, not against the base commit.

Benchmark Descriptions

Benchmark Description
CounterBenchmark Counter increment performance: Prometheus, OpenTelemetry, simpleclient, Codahale
HistogramBenchmark Histogram observation performance (classic vs native/exponential)
TextFormatUtilBenchmark Metric exposition format writing speed

zeitlinger added a commit that referenced this pull request Jul 10, 2026
## Summary

Make PR benchmark comments answer whether the PR is faster or slower
than its base commit, while keeping wall-clock time close to a single
benchmark run.

## Changes

- Run base and head benchmarks in parallel matrix jobs.
- Add a lightweight summary job that downloads both raw JMH artifacts
and generates the PR comment.
- Add a `Comparison with base` table to the generated `README.md` / PR
comment.
- Mark each benchmark as `faster`, `slower`, or `within noise` using JMH
confidence intervals when available.
- Capture and show runner hardware for both base and head jobs,
including a note that parallel jobs may run on different hardware.
- Rename the existing per-run relative column to `Within run` so it is
clear that it is not the base comparison.
- Store `baseline-results.json` in the uploaded benchmark artifact for
inspection.

## Validation

- Ran `mise run lint:fix`.
- Ran `python3 -m py_compile .mise/tasks/generate_benchmark_summary.py`.
- Ran `git diff --check`.
- Replayed summary generation with the latest #2252 benchmark artifact
and the published benchmark baseline; the output now shows
`HistogramBenchmark.prometheusClassic` as `-78.6%` / `slower`.
- Verified no-baseline summary generation still omits the comparison
section.

## Tradeoff

Parallel base/head jobs are faster wall-clock than measuring both
commits sequentially on one runner, but runner hardware can differ. The
comment now prints both runner descriptions and calls out that caveat.
@zeitlinger

Copy link
Copy Markdown
Member

the bench is not comparing to main - I'll fix that

@arnabnandy7

Copy link
Copy Markdown
Contributor Author

the bench is not comparing to main - I'll fix that

thanks

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.

Classic-only histogram: consider synchronized block instead of multi-LongAdder for observe() hot path

2 participants