Key Takeaways
- A benchmark does not become trustworthy when the load test ends. It becomes more trustworthy when raw artifacts are normalized, invalid runs stay visible, repeated runs are aggregated consistently, and uncertainty stays attached to the published comparison.
- This article defines the analysis contract for this benchmark series. Later articles use the same units, reporting rules, and chart constraints instead of inventing logic after seeing the numbers.
- We do not treat throughput, tail latency, CPU, memory, runtime diagnostics, startup, and warmup as one scoreboard. Each metric family answers a different engineering question, so we report them separately.
- The results article may compare variants within defined scenario and variant cells, but it may not declare an overall language winner. Exclusions, instability, and uncertainty remain visible wherever we make a comparison.
If you have ever read a benchmark post and thought, the chart is clean, but I still do not trust it, this is probably the missing layer.
Most benchmark arguments sound like methodology arguments, but a lot of the real trust problem shows up one step later, in analysis. The load test finishes. A directory fills up with raw outputs, logs, host metrics, and maybe runtime telemetry. Then someone has to decide what a “run” means, what gets excluded, how repeats get summarized, which latency numbers matter, and what kind of chart makes the comparison easiest to understand.
That is where a benchmark can drift from careful measurement into selective storytelling.
This article fixes the analysis contract before we publish results. Earlier entries in the series defined the run protocol, invalid-run logic, execution artifacts, and reproduction path. This article answers a narrower question: how do repeated benchmark runs become a published comparison that a skeptical engineer can inspect without guessing what got smoothed away?
The answer is disciplined rather than complicated. We commit now to the unit of analysis, the canonical dataset, the aggregation rules, the reporting rules, and the allowed plot types. We do that before the results article and before the interpretation article.
That matters because benchmark trust is not won by having more telemetry. It is won by making the path from raw artifacts to public claims easy to inspect.

We turn raw benchmark artifacts into reported comparisons through a fixed path: normalize each run, validate acceptance rules, aggregate by cell, surface exclusions and uncertainty, and only then choose allowed plots.
Why benchmark analysis methodology determines trust
It is easy to treat methodology as the part that happens before measurement: define the service, define the workload, validate the load generator, run the matrix, keep the host stable. All of that matters, and we have already spent several articles making it explicit.
But once the measurement phase is over, the benchmark still is not done. At that point, you have evidence, not a result. The result only appears after you answer analysis questions that are easy to wave away and surprisingly easy to bias.
What exactly is the unit we compare? One run, one average, one percentile, one scenario, one runtime variant, or one bundle of unlike metrics?
What happens when one repeat fails startup, another has incomplete telemetry, and six more look fine?
Do we show the best repeat, the mean of accepted repeats, the median, a tail-focused summary, or some mashup of all three?
Do we keep startup and steady state separate, or quietly blend them into one performance story?
Do we show throughput with uncertainty signals and latency as a selected p99, or do we carry uncertainty through both?
Those are not cosmetic questions. They determine how much trust the reader should place in the claim.
That is why we are fixing the analysis rules now instead of letting the results article discover them on the fly. Later in the series, if a chart appears, it should be possible to ask three immediate questions and get concrete answers:
- What exact cell does this chart summarize?
- How many repeats were attempted and accepted?
- What uncertainty or instability should stop me from overreading the difference?
If a chart cannot answer those questions, it does not belong in this series.
Benchmark terms we will use consistently
Before we talk about schema or aggregation, we need stable terms. These are series terms, not universal benchmark law, but we will use them consistently from here on.
A scenario is one workload definition. It captures payload size, concurrency target, workload mode, and the other settings that define what kind of test this is.
A variant is one runtime shape under test. Examples in this series include a Go service built on the standard-library net/http package and a Java service built with Helidon. These are benchmarked implementation shapes, not exhaustive representatives of either ecosystem.
A run is one executed measurement attempt for one scenario and one variant.
A repeat is one numbered run within a set of repeated attempts for the same scenario and variant combination. In practice, a repeat is usually just a run viewed in the context of its sibling attempts.
A cell is the combination of one scenario and one variant. This is the main unit of aggregation. We do not compare a loose pile of runs. We compare cells built from repeated runs under one fixed scenario and one fixed variant.
An accepted run is a run that passed the predeclared validation rules and is eligible for aggregation.
An invalid run is a run that failed those rules. Invalid does not mean invisible. It means excluded from the aggregated summaries and still reported with a reason class.
An unstable cell is a cell that meets the minimum bar for reporting but still triggers one or more warning conditions, such as higher-than-allowed repeat spread, missing metric slices, or incomplete evidence for one metric family.
Warmup is the interval where the service and runtime are getting into shape but we are not yet treating the behavior as steady measurement. For Java that can include JIT and class-loading effects; for Go it can still include allocator, scheduler, and cache stabilization. Either way, warmup is not the same thing as the main measurement window.
Steady state is the defined analysis window where service behavior is treated as the main comparable runtime state for throughput, latency, CPU, memory, and related metrics.
That vocabulary is intentionally boring. If those terms slide around between articles, the results slide around with them.
What the canonical benchmark dataset contains
A result chart should never be a pile of hand-entered summary numbers with the interesting mess stripped away. Our analysis starts from a canonical dataset that links the raw evidence to the derived metrics we actually publish.
The top-level shape is runtime-neutral. Go and Java both need to fit the same record model. Java-specific telemetry such as GC logs or Java Flight Recorder recordings can appear when captured, but they are additive fields, not proof that the schema belongs only to the JVM side.
At minimum, one normalized run record needs to connect scenario metadata, variant metadata, environment details, raw artifact references, analysis windows, derived metrics, and validation state.
Here is an intentionally schematic JSON example that shows the contract. Treat the field names, metric names, file paths, and IDs as illustrative only, not as the finalized benchmark schema.
{ "scenario_id": "<scenario>", "variant_id": "<variant>", "run_id": "<timestamp>-<variant>-r03", "repeat_index": 3, "environment": { "host_id": "<bench-host>", "os": "linux" }, "runtime": { "language": "<language>", "framework": "<framework>", "build_mode": "<mode>" }, "load_profile": { "connections": 64, "target_rps": 40000, "warmup_s": 60, "measurement_s": 180 }, "artifacts": { "loadgen_output": "artifacts/<scenario>/<variant>/<repeat>/loadgen-output.json", "service_log": "artifacts/<scenario>/<variant>/<repeat>/service.log", "runtime_log": "artifacts/<scenario>/<variant>/<repeat>/runtime.log", "runtime_trace": "artifacts/<scenario>/<variant>/<repeat>/runtime-trace.dat" }, "windows": { "startup": { "readiness_ms": 842 }, "warmup": { "start_s": 0, "end_s": 60 }, "steady_state": { "start_s": 60, "end_s": 240 } }, "derived_metrics": { "throughput_rps": 39520.4, "latency_p50_ms": 1.9, "latency_p95_ms": 4.8, "latency_p99_ms": 9.7, "error_rate_pct": 0.0, "cpu_utilization_pct": 87.4, "rss_mb": 612, "runtime_pause_total_ms": 128.0 }, "validation": { "accepted": true, "exclusion_reasons": [] }}
The important thing here is not the exact spelling of the fields. It is the separation of concerns.
The record points to raw files instead of pretending the summaries appeared by magic. It separates startup, warmup, and steady-state windows so we do not flatten them together later, and those windows come from the predefined run protocol rather than being tuned after the numbers are in. It keeps derived metrics distinct from artifact references. And it carries validation state inside the same normalized object instead of hiding exclusions in a spreadsheet note.
That structure is especially useful on the Java side. The Java implementation in this series is one specific framework and runtime shape, not “Java” in the abstract. On HotSpot-based runs, the artifact set may also include service logs, GC logs, and Java Flight Recorder data where configured. Those artifacts are useful, but only if they are tied to a run ID and normalized into the same record model as the Go-side evidence rather than used as a separate storytelling layer.
The example record uses Linux as the host OS label, but the actual results package should identify the tested host environment with more precision. These are single-machine results, and kernel, distribution, CPU settings, and other host details can materially affect behavior.
Our analysis contract requires a manifest-style mapping from archived files to normalized inputs so the path from source artifact to reported metric stays inspectable.
A compact directory sketch can be as simple as this:
artifacts/ <scenario>/ <variant>/ <repeat>/ manifest.json loadgen-output.json service.log runtime-log-or-trace.dat
And a matching manifest might look like this at a conceptual level. The keys and filenames below are illustrative placeholders, not verified repository literals:
{ "run_id": "<timestamp>-<variant>-r03", "scenario_id": "<scenario>", "variant_id": "<variant>", "raw_artifacts": { "loadgen_output": "loadgen-output.json", "service_log": "service.log", "runtime_log": "runtime.log", "runtime_trace": "runtime-trace.dat" }, "normalization_inputs": { "throughput_source": "loadgen_output", "latency_source": "loadgen_output", "startup_source": "service_log", "runtime_diagnostics_source": "runtime_trace" }}
This example shows the minimum traceability we want: raw artifacts are archived, the mapping to normalized metrics is explicit, and optional Java telemetry is supported without making the whole dataset JVM-specific.

Our canonical dataset keeps one repeated run inspectable end to end by linking scenario and variant metadata, environment details, raw artifacts, analysis windows, derived metrics, and validation status in one normalized record.
How repeated runs become reported comparisons
This is the point where many benchmark posts quietly switch from measurement to rhetoric.
Our aggregation path is strict:
run -> cell -> scenario and variant comparison
That means one run never becomes a headline by itself. A run belongs to a repeat set. Accepted repeats are aggregated into a cell. Cells are compared only within the same scenario boundary.
So if you are looking at one workload scenario, the comparison is between variants within that scenario. We are not allowed to blend unlike scenarios into one runtime score. That would answer no coherent engineering question.
We also summarize metric families separately.
Startup and readiness stay separate because they answer, “How quickly can this process become usable?” In this series, startup and readiness are only comparable when they come from the same per-run lifecycle defined in the benchmark protocol. We do not mix fresh-process startup measurements with reused-process steady-state repeats and call them one startup story.
Warmup behavior stays separate because it answers, “How quickly does this runtime shape settle?” Steady-state throughput stays separate because it answers, “How much work is delivered once the run reaches its main measurement phase?” Latency percentiles stay separate because tail behavior is not interchangeable with throughput. CPU efficiency and memory footprint are separate again because resource cost is part of the runtime shape, not background noise. Runtime diagnostics such as GC logs or Java Flight Recorder traces stay contextual and explanatory rather than turning into a hidden deciding factor.
That leads to one rule worth stating plainly:
We do not treat throughput, tail latency, CPU, memory, runtime diagnostics, and startup as one scoreboard.
In practice, that means later figures and captions should compare like with like: throughput against throughput, latency against latency, startup against startup, and resource-cost metrics in their own views.
Why be so strict? Because composite benchmark scoring hides tradeoffs the reader actually cares about. A variant can start faster and still have weaker steady-state throughput. Another can deliver stronger throughput and still consume more CPU per unit of work. A third can look strong on average and still have a wider p99 spread. Those are not contradictions. They are different dimensions of runtime behavior.
For the Java side, this matters even more if multiple Java runtime shapes appear. The Helidon-based benchmark variant is one Java framework shape, not “Java” as a whole. If the series later includes another JDK-based workflow with different startup or warmup behavior, that remains its own variant. The analysis contract forbids us from pooling unlike runtime modes into one Java bucket and then talking as if the combined number represents the platform fairly.
In practical terms, a reported comparison in the results article might say that one variant shows higher steady-state throughput in a given scenario while another reaches readiness sooner or stabilizes earlier in warmup. That is allowed. Saying one of them “wins overall” is not.
How we handle invalid runs, excluded repeats, and unstable cells
A benchmark result is not just a number. It is also an accounting of how much evidence survived long enough to support that number.
Every reported cell summary needs to make four things visible:
- total repeats attempted
- repeats accepted
- repeats excluded by reason class
- a central value plus a visible uncertainty or instability signal for the reported metrics
If a cell is unstable, that needs to be visible too. If telemetry is incomplete for one metric family, that should show up as a missing slice, not vanish into silence. That does not always mean the whole run is invalid. Missing optional telemetry may leave a run usable for core throughput and latency reporting while making it unavailable for a diagnostic metric family. Missing required comparison artifacts is a different case and can invalidate the run entirely under the series acceptance rules.
Here is an illustrative cell-summary example showing the reporting shape we expect. The interval fields here show reporting shape only; they do not prescribe a specific statistical method by themselves.
{ "cell_id": "<scenario>/<variant>", "runs_attempted": 8, "runs_accepted": 6, "runs_excluded": 2, "exclusion_reason_counts": { "startup_failure": 1, "artifact_incomplete": 1 }, "reported_metrics": { "throughput_rps_median": 39480.0, "throughput_rps_interval": [38990.0, 39910.0], "latency_p99_ms_median": 9.9, "latency_p99_ms_interval": [9.1, 11.4], "rss_mb_median": 608, "rss_mb_interval": [601, 620] }, "stability_flags": [ "moderate_tail_variance" ], "notes": [ "steady-state metrics computed only from accepted runs", "excluded runs remain visible in the published summary" ]}
This is a small example, but it carries the full contract.
First, it tells you how many times we tried. That matters because “median of six accepted runs” means something different when it came from six attempts versus eight attempts with two exclusions.
Second, it tells you what got excluded and why. A startup failure and an incomplete artifact are not the same problem, and they should not disappear behind a cleaned-up summary.
Third, it reports uncertainty signals alongside central values. We are not going to publish a tiny difference in throughput or p99 and pretend the reader should treat that as settled truth without any variation context. Repeated benchmark runs vary, and the summary needs to make that visible.
Fourth, it can flag instability even when the cell is still worth showing. An unstable cell is not automatically worthless. It is a sign that the claim attached to it needs to stay narrow.
Here is how we want a human reader to interpret that example:
Attempted eight repeats. Accepted six. Excluded two, and the reasons are visible. The reported throughput and p99 come only from accepted runs. Tail behavior still varies enough that we should not use this cell as the basis for sweeping claims.
There is also a small-sample rule underneath all of this. This article fixes the reporting shape, not the full statistical appendix, but later results still need to meet a minimum bar: if a metric is compared, the chart or adjacent caption must show accepted-repeat counts and some visible uncertainty or instability signal for that metric family. If accepted evidence is too thin for a normal cell summary, we mark the cell as insufficient evidence instead of presenting it as a clean head-to-head result. If a cell does not meet the series minimum for comparability, it may still be shown as incomplete evidence, but not as a claimed winner.
This article keeps invalid runs, accepted counts, and uncertainty visible, but load-generator validity questions such as coordinated-omission handling must be documented separately in the series methodology and tool configuration.

A reported cell summary shows how many repeats we attempted, how many were accepted, why any were excluded, which metrics are summarized, and whether instability limits strong claims.
Which uncertainty signals we report and why
Uncertainty is part of the result, not a footnote.
A point estimate by itself is easy to quote and easy to overread. That is exactly why later articles need more than a single median, average, or percentile value in isolation. Repeated runs vary even when the benchmark protocol is stable, so the published comparison has to carry some visible signal of that variation.
Our reporting rule is simple:
- each compared metric family needs a central summary value
- each compared metric family also needs a visible uncertainty or instability signal
- accepted-repeat counts must stay close to the reported value
- excluded-repeat counts and reasons remain visible at the cell level
That does not mean every figure turns into a statistics lecture. It means the reader should be able to tell whether a small gap is supported by stable repeated evidence or sits inside a noisy range that weakens the claim.
It also means we are careful with what interval language implies. An interval estimate is not proof. It is a compact way to show that repeated measurements vary and that narrow or wide spread changes how strongly we should read the comparison.
This article fixes the reporting shape, not the exact interval formula. The results article needs to name one series-wide method and apply it consistently across compared cells. The same applies to instability thresholds: they need one published rule for the series, not case-by-case judgment after the numbers are visible.
The practical effect is straightforward: later result claims can be stronger when accepted repeats are stable and uncertainty is tight, and narrower when the cell remains noisy or partially incomplete.
Which plots are allowed for each metric family
Plot choice is part of the method. It is not decoration added after the data already “speaks for itself.”
Different metric families need different chart shapes because they answer different questions.
Throughput comparisons work well as bars or points with visible uncertainty because the reader needs a direct side-by-side view and a clear sense of whether small gaps are likely meaningful.
Latency tails need tail-aware views. Percentile lines or tail-focused point comparisons are appropriate because p95 and p99 behavior should stay visible instead of being flattened into one average-latency story.
Startup and readiness need timeline or phase-duration views. A startup chart should not sit on the same visual axis as steady-state throughput because those are unlike quantities answering unlike questions.
Warmup behavior needs time-series views. That is the honest way to show convergence, early instability, or time-to-peak differences.
CPU efficiency is better served by scatter or normalized views that pair delivered work with CPU cost. Raw CPU percentages alone are often too context-poor.
Memory footprint works as bars or points with intervals, as long as it is not mixed into a composite throughput-latency-resource scoreboard.
Just as important, some chart patterns are off-limits in this series because they mislead more than they clarify.
We will not use stacked bars that imply throughput, latency, CPU, and memory add up to one thing. They do not.
We will not use mixed-axis mashups that visually dramatize one metric by scaling another into the background.
We will not publish a single scoreboard that combines startup, p99 latency, throughput, CPU, memory, and runtime diagnostics into one rank.
And we will not hide exclusions or uncertainty in figure notes while putting the cleanest-looking number in the chart title.

We choose plot types by metric family so readers can inspect throughput, tail latency, startup, warmup, CPU efficiency, and memory behavior without collapsing unlike measures into one score.
What later articles will and will not be allowed to claim
By this point, the contract should feel narrow enough to be useful.
The results article may compare variants within defined scenario and cell boundaries. It may say that one variant showed higher steady-state throughput for a given scenario. It may say another reached readiness sooner. It may say a variant’s p99 interval was tighter or that a cell remained unstable enough to weaken the claim. It may use Java runtime telemetry as explanatory context when the artifact trail supports it. For example, GC logs or Java Flight Recorder data can help explain whether tail disturbances align with runtime events instead of leaving the reader with a vague runtime story.
But later articles may not do several tempting things.
They may not declare an overall language winner.
They may not merge startup, throughput, tail latency, CPU, and memory into one ranking.
They may not hide invalid or excluded runs just because the accepted set tells a cleaner story.
They may not generalize from the benchmarked Java framework variant to all Java frameworks, or from a Go service built on net/http to Go as a whole.
They may not treat separately benchmarked JDK distributions as interchangeable just because they are closely related upstream. Even when two JDK distributions are derived from OpenJDK source, separately benchmarked distributions remain separate variants for reporting.
They may not treat a JVM run, a native image, a checkpoint-and-restore workflow, or any other materially different runtime path as the same thing just because they come from related tooling or source bases.
A short way to remember the rule is this:
Allowed claim: “Within this scenario, variant A shows higher steady-state throughput, while variant B reaches readiness sooner and shows a tighter p99 interval.”
Disallowed claim: “Variant A wins overall.”
That line may sound strict, but it is the difference between a benchmark that helps engineering decisions and one that mostly fuels argument.
What changes after this article
Up to now, the series established the benchmark scope, the service contract, the implementation shapes, the load-generation logic, the run protocol, and the reproduction path. What changes here is that the analysis rules are now fixed.
That matters because once results appear, readers should not have to reverse-engineer the logic behind each figure. You should already know what a cell is, what the canonical dataset carries, how accepted runs are aggregated, what exclusions look like, why startup and steady state stay separate, and why no one score is coming to rescue a complicated comparison.
You should also know what this article does not settle. Keeping exclusions and uncertainty visible improves the honesty of the reporting, but it is not a substitute for tool-specific load-generator behavior, runtime-specific diagnostics, or careful host-environment disclosure. Those still need to appear where the benchmark evidence is published.
If you are the kind of reader who distrusts benchmark posts by default, this is the article that should make the next one easier to read. Not because it proves any runtime is better, but because it limits what we are allowed to claim when the numbers arrive.
So when you read the results article, read it with this contract in mind. Ask whether every chart stays inside a scenario boundary. Ask whether exclusions and accepted counts are visible. Ask whether uncertainty is shown where the article wants you to care about a difference. And ask whether the interpretation respects the fact that startup, warmup, throughput, latency, CPU, memory, and runtime telemetry are related signals, not one giant score.
That is the discipline this series needs if the conclusions are going to be worth anything.
Conclusion
Benchmark trust is easy to lose because the final chart compresses a lot of judgment into one visual. This article makes those judgments explicit before the results article gets a chance to hide them.
Our analysis contract is simple to state even if it takes work to follow. We normalize each run into a canonical record. We keep raw artifacts linked to derived metrics. We validate runs and keep excluded ones visible by reason class. We aggregate accepted repeats into scenario and variant cells. We report metric families separately. We show uncertainty and instability instead of pretending they are background noise. And we forbid composite-score storytelling.
That does not make the later results automatically correct. It does make them much harder to overclaim.
In the next article, we apply this contract to the actual benchmark outputs. The important part is that the rules are already on the page.
References
- Gil Tene, How NOT to Measure Latency and related coordinated-omission materials from HdrHistogram: https://hdrhistogram.github.io/HdrHistogram/
- HdrHistogram project: https://github.com/HdrHistogram/HdrHistogram
- Georges, Buytaert, and Eeckhout, Statistically Rigorous Java Performance Evaluation, USENIX OOPSLA: https://www.usenix.org/conference/oopsla13/statistically-rigorous-java-performance-evaluation
- Neil J. Gunther, Performance Matters, ACM Queue: https://queue.acm.org/detail.cfm?id=2855183
- Java Flight Recorder documentation: https://docs.oracle.com/en/java/javase/25/troubleshoot/troubleshoot-performance-issues-using-jfr.html
- Helidon project home: https://helidon.io/
- Go
net/httppackage documentation: https://pkg.go.dev/net/http - Oracle JDK FAQ: https://www.oracle.com/java/technologies/javase/jdk-faqs.html
- GraalVM Native Image documentation: https://www.graalvm.org/latest/reference-manual/native-image/



































You must be logged in to post a comment.