1111 mean sampled latency per op; it's our best available proxy for CPU
1212 work since no dedicated CPU profiler is configured in
1313 `BenchmarkRunner`.
14- * `Alloc/op` — `secondaryMetrics["· gc.alloc.rate.norm"]`, populated by
14+ * `Alloc/op` — `secondaryMetrics["gc.alloc.rate.norm"]`, populated by
1515 JMH's `GCProfiler`. This is bytes allocated per benchmark op and is
16- the standard, low-noise JMH memory metric.
16+ the standard, low-noise JMH memory metric. (Note: JMH 1.37 stores the
17+ key with no prefix; some visualizer tools display it as
18+ `·gc.alloc.rate.norm` but the raw JSON does not contain that dot.)
1719
1820Both metrics are "lower is better", so a positive delta indicates the
1921PR is worse than the baseline. A run is considered failed when **any**
4244# ---------------------------------------------------------------------------
4345
4446# JMH's `GCProfiler` reports allocation rate normalised per op under this
45- # secondary metric key (the leading char is U+00B7 MIDDLE DOT, not a regular
46- # dot — that's JMH's convention for profiler-emitted metrics).
47- ALLOC_NORM_KEY = "\u00b7 gc.alloc.rate.norm"
47+ # secondary metric key. Verified against jmh-core 1.37 sources: the key
48+ # is the literal `gc.alloc.rate.norm` with no prefix. Some visualizers
49+ # render it as `·gc.alloc.rate.norm`, which is a display convention, not
50+ # what JMH writes to JSON.
51+ ALLOC_NORM_KEY = "gc.alloc.rate.norm"
52+
53+ # Tolerated prefix-bearing variants we'll fall back to if the canonical
54+ # key ever moves. Lets a future JMH (or a non-Oracle JVM profiler that
55+ # decorates the label) keep working without us being aware.
56+ ALLOC_NORM_VARIANTS = (
57+ ALLOC_NORM_KEY ,
58+ "\u00b7 gc.alloc.rate.norm" , # pre-emptive middle-dot variant
59+ "+gc.alloc.rate.norm" ,
60+ )
61+
62+ # Set to True the first time we fail to find any allocation data; used
63+ # to emit a one-time diagnostic listing the secondary keys present so
64+ # the cause is obvious in workflow logs.
65+ _alloc_warn_printed = False
4866
4967
5068@dataclass (frozen = True )
@@ -70,13 +88,35 @@ def _secondary(record: Dict[str, Any], key: str) -> Optional[Dict[str, Any]]:
7088 return val if isinstance (val , dict ) else None
7189
7290
91+ def _alloc (record : Dict [str , Any ]) -> Optional [Dict [str , Any ]]:
92+ """Pull the GC allocation-per-op metric, tolerating prefix variants.
93+
94+ Falls back to a suffix match so even an unrecognised prefix (e.g. a
95+ third-party JMH profiler that decorates the label) still works."""
96+ global _alloc_warn_printed
97+ sm = record .get ("secondaryMetrics" ) or {}
98+ for k in ALLOC_NORM_VARIANTS :
99+ v = sm .get (k )
100+ if isinstance (v , dict ):
101+ return v
102+ # Last-resort suffix match.
103+ for k , v in sm .items ():
104+ if isinstance (v , dict ) and k .lower ().endswith ("gc.alloc.rate.norm" ):
105+ return v
106+ if sm and not _alloc_warn_printed :
107+ print (
108+ "warn: no `gc.alloc.rate.norm` (or prefix-variant) found in "
109+ "secondaryMetrics. Available keys: "
110+ + ", " .join (sorted (sm .keys ())),
111+ file = sys .stderr ,
112+ )
113+ _alloc_warn_printed = True
114+ return None
115+
116+
73117METRICS : List [Metric ] = [
74118 Metric (id = "time" , label = "Time" , extract = _primary ),
75- Metric (
76- id = "alloc" ,
77- label = "Alloc/op" ,
78- extract = lambda r : _secondary (r , ALLOC_NORM_KEY ),
79- ),
119+ Metric (id = "alloc" , label = "Alloc/op" , extract = _alloc ),
80120]
81121
82122
@@ -391,7 +431,7 @@ def bucket(r: Row) -> int:
391431 bench , params = k
392432 rec = current [k ]
393433 time_d = _primary (rec ) or {}
394- alloc_d = _secondary (rec , ALLOC_NORM_KEY ) or {}
434+ alloc_d = _alloc (rec ) or {}
395435 out .append (
396436 f"- `{ short_bench (bench )} ` ({ params or '—' } ): "
397437 f"time={ fmt_score (_float (time_d , 'score' ), _float (time_d , 'scoreError' ), time_d .get ('scoreUnit' , '' ))} , "
0 commit comments