Skip to content

perf: optimize classic-only histogram observe path#2252

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

perf: optimize classic-only histogram observe path#2252
arnabnandy7 wants to merge 11 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 1926f054597a849a1fbda0aa23817a1e696edd98.

Prometheus Java Client Benchmarks

Run Information

  • Date: 2026-07-09T12:03:07Z
  • Commit: 1926f05
  • JDK: 25.0.3 (OpenJDK 64-Bit Server VM)
  • Benchmark config: 3 fork(s), 3 warmup, 5 measurement, 4 threads
  • Hardware: AMD EPYC 9V74 80-Core Processor, 4 cores, 16 GB RAM
  • OS: Linux 6.17.0-1018-azure

Results

CounterBenchmark

Benchmark Score Error Units
prometheusInc 74.97K ± 2.84K ops/s fastest
prometheusNoLabelsInc 66.54K ± 750.84 ops/s 1.1x slower
prometheusAdd 64.04K ± 409.67 ops/s 1.2x slower
codahaleIncNoLabels 57.08K ± 250.80 ops/s 1.3x slower
simpleclientAdd 7.78K ± 107.82 ops/s 9.6x slower
simpleclientInc 7.75K ± 172.32 ops/s 9.7x slower
simpleclientNoLabelsInc 7.57K ± 54.65 ops/s 9.9x slower
openTelemetryInc 5.91K ± 1.83K ops/s 13x slower
openTelemetryAdd 5.90K ± 986.98 ops/s 13x slower
openTelemetryIncNoLabels 5.83K ± 1.90K ops/s 13x slower

HistogramBenchmark

Benchmark Score Error Units
simpleclient 5.66K ± 45.09 ops/s fastest
prometheusNative 4.16K ± 31.26 ops/s 1.4x slower
prometheusClassic 1.19K ± 151.08 ops/s 4.8x slower
openTelemetryClassic 923.72 ± 16.65 ops/s 6.1x slower
openTelemetryExponential 727.89 ± 21.06 ops/s 7.8x slower

HistogramTextFormatBenchmark

Benchmark Score Error Units
openMetricsWriteToNull 35.28K ± 143.39 ops/s fastest
prometheusWriteToNull 35.18K ± 407.47 ops/s 1.0x slower

TextFormatUtilBenchmark

Benchmark Score Error Units
prometheusWriteToNull 707.56K ± 10.56K ops/s fastest
prometheusWriteToByteArray 695.09K ± 8.30K ops/s 1.0x slower
openMetricsWriteToNull 658.03K ± 6.48K ops/s 1.1x slower
openMetricsWriteToByteArray 636.54K ± 8.64K ops/s 1.1x slower

Raw Results

Benchmark                                            Mode  Cnt          Score        Error  Units
CounterBenchmark.codahaleIncNoLabels                thrpt   15      57082.929    ± 250.804  ops/s
CounterBenchmark.openTelemetryAdd                   thrpt   15       5895.730    ± 986.980  ops/s
CounterBenchmark.openTelemetryInc                   thrpt   15       5914.569   ± 1829.951  ops/s
CounterBenchmark.openTelemetryIncNoLabels           thrpt   15       5825.048   ± 1897.047  ops/s
CounterBenchmark.prometheusAdd                      thrpt   15      64039.124    ± 409.675  ops/s
CounterBenchmark.prometheusInc                      thrpt   15      74968.713   ± 2838.660  ops/s
CounterBenchmark.prometheusNoLabelsInc              thrpt   15      66544.432    ± 750.836  ops/s
CounterBenchmark.simpleclientAdd                    thrpt   15       7783.229    ± 107.817  ops/s
CounterBenchmark.simpleclientInc                    thrpt   15       7749.058    ± 172.317  ops/s
CounterBenchmark.simpleclientNoLabelsInc            thrpt   15       7569.699     ± 54.649  ops/s
HistogramBenchmark.openTelemetryClassic             thrpt   15        923.716     ± 16.650  ops/s
HistogramBenchmark.openTelemetryExponential         thrpt   15        727.891     ± 21.064  ops/s
HistogramBenchmark.prometheusClassic                thrpt   15       1185.772    ± 151.078  ops/s
HistogramBenchmark.prometheusNative                 thrpt   15       4161.661     ± 31.257  ops/s
HistogramBenchmark.simpleclient                     thrpt   15       5659.296     ± 45.093  ops/s
HistogramTextFormatBenchmark.openMetricsWriteToNull  thrpt   15      35276.944    ± 143.390  ops/s
HistogramTextFormatBenchmark.prometheusWriteToNull  thrpt   15      35182.598    ± 407.469  ops/s
TextFormatUtilBenchmark.openMetricsWriteToByteArray  thrpt   15     636544.872   ± 8642.117  ops/s
TextFormatUtilBenchmark.openMetricsWriteToNull      thrpt   15     658028.484   ± 6477.641  ops/s
TextFormatUtilBenchmark.prometheusWriteToByteArray  thrpt   15     695086.780   ± 8301.245  ops/s
TextFormatUtilBenchmark.prometheusWriteToNull       thrpt   15     707556.698  ± 10556.911  ops/s

Notes

  • Score = Throughput in operations per second (higher is better)
  • Error = 99.9% confidence interval

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

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