Skip to content

perf: optimize classic-only histogram observe path#2252

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

perf: optimize classic-only histogram observe path#2252
arnabnandy7 wants to merge 13 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 1c1f76907c1318938f88d4a207dfeb27ac30c971.

Prometheus Java Client Benchmarks

Run Information

  • Date: 2026-07-20T13:53:43Z
  • Commit: 1c1f769
  • 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-1020-azure

Comparison with base

  • Head: 1c1f769
  • Base: 0a91771
  • Change: positive means the PR is faster than base.
  • Note: Base and head are compared on the same runner within each topic.
Benchmark PR Base Change Result
CounterBenchmark.codahaleIncNoLabels 49.66K 47.08K +5.5% within noise
CounterBenchmark.openTelemetryAdd 3.50K 3.56K -1.6% within noise
CounterBenchmark.openTelemetryInc 3.12K 3.09K +0.9% within noise
CounterBenchmark.openTelemetryIncNoLabels 3.20K 3.16K +1.2% within noise
CounterBenchmark.prometheusAdd 51.11K 51.32K -0.4% within noise
CounterBenchmark.prometheusInc 66.09K 66.46K -0.6% within noise
CounterBenchmark.prometheusNoLabelsInc 56.79K 56.95K -0.3% within noise
CounterBenchmark.simpleclientAdd 6.11K 6.39K -4.4% within noise
CounterBenchmark.simpleclientInc 6.55K 6.55K -0.1% within noise
CounterBenchmark.simpleclientNoLabelsInc 6.34K 6.33K +0.3% within noise
HistogramBenchmark.openTelemetryClassic 707.83 712.84 -0.7% within noise
HistogramBenchmark.openTelemetryExponential 524.92 556.18 -5.6% slower
HistogramBenchmark.prometheusClassic 935.75 7.89K -88.1% slower
HistogramBenchmark.prometheusClassicPerThread 19.20K 14.48K +32.6% faster
HistogramBenchmark.prometheusClassicSingleThread 9.39K 5.94K +58.1% faster
HistogramBenchmark.prometheusNative 2.97K 2.80K +5.9% within noise
HistogramBenchmark.simpleclient 4.62K 4.51K +2.3% within noise
HistogramTextFormatBenchmark.openMetricsWriteToNull 23.51K 23.69K -0.8% within noise
HistogramTextFormatBenchmark.prometheusWriteToNull 23.70K 24.05K -1.4% within noise
TextFormatUtilBenchmark.openMetricsWriteToByteArray 487.19K 482.95K +0.9% within noise
TextFormatUtilBenchmark.openMetricsWriteToNull 491.70K 493.02K -0.3% within noise
TextFormatUtilBenchmark.prometheusWriteToByteArray 505.02K 509.99K -1.0% within noise
TextFormatUtilBenchmark.prometheusWriteToNull 513.34K 512.86K +0.1% within noise

Results for PR head

CounterBenchmark

Benchmark Score Error Units Within run
prometheusInc 66.09K ± 537.02 ops/s fastest
prometheusNoLabelsInc 56.79K ± 299.70 ops/s 1.2x slower
prometheusAdd 51.11K ± 269.88 ops/s 1.3x slower
codahaleIncNoLabels 49.66K ± 886.22 ops/s 1.3x slower
simpleclientInc 6.55K ± 147.47 ops/s 10x slower
simpleclientNoLabelsInc 6.34K ± 9.84 ops/s 10x slower
simpleclientAdd 6.11K ± 306.29 ops/s 11x slower
openTelemetryAdd 3.50K ± 166.71 ops/s 19x slower
openTelemetryIncNoLabels 3.20K ± 243.16 ops/s 21x slower
openTelemetryInc 3.12K ± 339.34 ops/s 21x slower

HistogramBenchmark

Benchmark Score Error Units Within run
prometheusClassicPerThread 19.20K ± 11.10 ops/s fastest
prometheusClassicSingleThread 9.39K ± 43.52 ops/s 2.0x slower
simpleclient 4.62K ± 91.85 ops/s 4.2x slower
prometheusNative 2.97K ± 228.32 ops/s 6.5x slower
prometheusClassic 935.75 ± 108.72 ops/s 21x slower
openTelemetryClassic 707.83 ± 9.35 ops/s 27x slower
openTelemetryExponential 524.92 ± 8.95 ops/s 37x slower

HistogramTextFormatBenchmark

Benchmark Score Error Units Within run
prometheusWriteToNull 23.70K ± 603.04 ops/s fastest
openMetricsWriteToNull 23.51K ± 606.26 ops/s 1.0x slower

TextFormatUtilBenchmark

Benchmark Score Error Units Within run
prometheusWriteToNull 513.34K ± 3.50K ops/s fastest
prometheusWriteToByteArray 505.02K ± 8.91K ops/s 1.0x slower
openMetricsWriteToNull 491.70K ± 2.39K ops/s 1.0x slower
openMetricsWriteToByteArray 487.19K ± 1.41K ops/s 1.1x slower

Raw Results

Benchmark                                            Mode  Cnt          Score        Error  Units
CounterBenchmark.codahaleIncNoLabels                thrpt   15      49657.370    ± 886.225  ops/s
CounterBenchmark.openTelemetryAdd                   thrpt   15       3499.269    ± 166.707  ops/s
CounterBenchmark.openTelemetryInc                   thrpt   15       3119.853    ± 339.336  ops/s
CounterBenchmark.openTelemetryIncNoLabels           thrpt   15       3200.141    ± 243.162  ops/s
CounterBenchmark.prometheusAdd                      thrpt   15      51107.850    ± 269.875  ops/s
CounterBenchmark.prometheusInc                      thrpt   15      66087.397    ± 537.025  ops/s
CounterBenchmark.prometheusNoLabelsInc              thrpt   15      56785.283    ± 299.705  ops/s
CounterBenchmark.simpleclientAdd                    thrpt   15       6110.421    ± 306.287  ops/s
CounterBenchmark.simpleclientInc                    thrpt   15       6546.736    ± 147.473  ops/s
CounterBenchmark.simpleclientNoLabelsInc            thrpt   15       6342.087      ± 9.837  ops/s
HistogramBenchmark.openTelemetryClassic             thrpt   15        707.826      ± 9.351  ops/s
HistogramBenchmark.openTelemetryExponential         thrpt   15        524.921      ± 8.946  ops/s
HistogramBenchmark.prometheusClassic                thrpt   15        935.747    ± 108.715  ops/s
HistogramBenchmark.prometheusClassicPerThread       thrpt   15      19195.302     ± 11.102  ops/s
HistogramBenchmark.prometheusClassicSingleThread    thrpt   15       9386.316     ± 43.521  ops/s
HistogramBenchmark.prometheusNative                 thrpt   15       2968.843    ± 228.324  ops/s
HistogramBenchmark.simpleclient                     thrpt   15       4616.714     ± 91.854  ops/s
HistogramTextFormatBenchmark.openMetricsWriteToNull  thrpt   15      23510.366    ± 606.262  ops/s
HistogramTextFormatBenchmark.prometheusWriteToNull  thrpt   15      23699.025    ± 603.043  ops/s
TextFormatUtilBenchmark.openMetricsWriteToByteArray  thrpt   15     487186.586   ± 1405.248  ops/s
TextFormatUtilBenchmark.openMetricsWriteToNull      thrpt   15     491698.956   ± 2390.927  ops/s
TextFormatUtilBenchmark.prometheusWriteToByteArray  thrpt   15     505020.646   ± 8905.491  ops/s
TextFormatUtilBenchmark.prometheusWriteToNull       thrpt   15     513341.626   ± 3496.829  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

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

Replace the parallel base/head PR benchmark comparison with topic-split
benchmark jobs. Each topic compares base and head sequentially on the
same runner, while topics still run in parallel for better wall-clock
time.

## Changes

- Split PR benchmarks into topic jobs: counter, histogram, and
exposition.
- Run base and head sequentially inside each topic job so each
comparison uses the same runner.
- Merge per-topic JMH JSON results in the summary job before generating
the PR comment.
- Add a comparison note clarifying that base/head are compared on the
same runner within each topic.
- Add histogram benchmark variants for classic-only histograms:
  - shared contended 4-thread benchmark remains as `prometheusClassic`
  - single-thread benchmark: `prometheusClassicSingleThread`
  - per-thread 4-thread benchmark: `prometheusClassicPerThread`

## Why

Running base and head in separate jobs was faster but not reliable
enough because GitHub Actions can schedule them on different CPU models.
Topic splitting keeps the reliable same-runner comparison within each
topic while still allowing unrelated benchmark topics to run in
parallel.

## Validation

- Ran `mise run lint:fix`.
- Ran `mise run build`.
- Ran `python3 -m py_compile .mise/tasks/generate_benchmark_summary.py`.
- Ran `git diff --check`.
- Verified the JMH include patterns select the intended benchmarks with
`java -jar benchmarks/target/benchmarks.jar -l ...`.
- Smoke-tested the topic result merge and summary generation using the
latest #2252 benchmark artifact.
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