Skip to content

Gate any metric of any grid point, and let every matching threshold fire - #1018

Draft
epompeii wants to merge 1 commit into
u/ep/parameters-api/alertsfrom
u/ep/parameters-api/thresholds
Draft

Gate any metric of any grid point, and let every matching threshold fire#1018
epompeii wants to merge 1 commit into
u/ep/parameters-api/alertsfrom
u/ep/parameters-api/thresholds

Conversation

@epompeii

@epompeii epompeii commented Aug 26, 2026

Copy link
Copy Markdown
Member

A threshold gated one thing: the value scalar of every grid point of its measure.
A benchmark now reports as many grid points as it has parameter sets and as many
named scalars as its harness measured, and neither was addressable. A project that
wanted p99 watched on the one configuration where it matters had nowhere to say
so.

The model

A threshold gains two optional fields.

metric is the name it gates. A threshold that names none gates the conventional
value name: a threshold always gates exactly one name, and a bare one never gates
all of them.

parameters is a filter over grid points: a list of parameter sets, an OR across
the list and a subset match within each set. A filter names only the keys it cares
about, so [{"size": 512}] matches a grid point that also pins threads. A
threshold with no filter gates every grid point.

Both defaults are what every threshold already does, so no existing row moves and no
project's alert volume changes.

Every matching threshold fires

There is no winner: not in gating, not in display. A grid point that a bare
threshold and a filtered threshold both match earns a boundary from each and, on a
regression, an alert from each. Two thresholds asked for that row to be watched, so
two of them are told about it. This is deliberate and it is pinned by a test rather
than smoothed over.

Identity, and the null semantics

Identity is the three dimensions plus the two new fields, under the null semantics
they carry. SQLite treats nulls as distinct in a unique index, so two bare
thresholds on one branch, testbed, and measure would no longer collide under a plain
unique key over the five columns, and an explicit value would sit beside an absent
one.

The key is declared over the effective values instead:

CREATE UNIQUE INDEX index_threshold_dimensions ON threshold(
    branch_id,
    testbed_id,
    measure_id,
    COALESCE(metric, 'value'),
    COALESCE(parameters, x'')
);

x'' is a value no stored filter can take, because a filter that matches everything
is stored as NULL.

The wire canonicalizes into those stored values. An explicit value and an absent
name are one threshold. An empty filter, a filter holding the empty set, and an
absent filter are one threshold, because the empty set is a subset of every grid
point. A filter has one spelling, because its sets sort by their RFC 8785 canonical
bytes and duplicates collapse, so [{"a": 1}, {"a": 1.0}] is one set. The wire
accepts any order and any spelling; the response returns the canonical form.

The sample keys on the name

The historical query behind detection filters on the threshold's metric name, so a
threshold on p99 is tested against p99 rows and never against the value rows
beside them. The per grid point separation is unchanged: a threshold still samples
one grid point's history and never the benchmark's grid pooled.

The boundary key

boundary keys on (metric_id, threshold_id) rather than on metric_id alone,
because a metric row may now carry a boundary per threshold that gated it. Both
threshold and boundary are rebuilt, each with its unique keys built after the
copy rather than declared on the table, which is what keeps the rebuild at the cost
of the scan. down.sql restores both old shapes.

Responses

JsonAlert gains value, the scalar the alert fired on, and its metric triple
becomes optional. The triple is a convention over the value name, so an alert on
any other name has none; every alert a threshold could raise before this carries the
triple exactly as it did, so no existing response changes. The gated name is
readable at alert.threshold.metric.

The base of this branch carried a debug_assert_eq! in into_json_for_report
holding that a gated row is always a point estimate. That is the invariant this
branch retires, so the assertion is removed by design rather than by accident, and
the comment in its place says so.

The deprecated singular threshold, boundary, and alert fields carry the bare
threshold's gate and no other's, wherever they appear. That is precisely what a
caller from before named gating has always been shown: a row that only a named or
filtered threshold gates reports no gate in them at all. Where a list of boundaries
is returned, it is ordered by threshold creation time, oldest first, with the UUID
breaking a tie. Nothing about that order is a ranking.

The in-report thresholds.models map is unchanged and still addresses the bare
threshold. A map that names a measure and a model says nothing about a name or a
grid point, so it neither creates nor resets anything narrower. A threshold that
gates a name or a corner of the grid is created through the thresholds endpoint.

@epompeii
epompeii force-pushed the u/ep/parameters-api/thresholds branch 2 times, most recently from abd44e8 to 70525c7 Compare August 26, 2026 10:19
@github-actions

github-actions Bot commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

ProjectBencher
Branchu/ep/parameters-api/thresholds
Testbedintel-v1
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
microseconds (µs)
(Result Δ%)
Upper Boundary
microseconds (µs)
(Limit %)
Adapter::Json📈 view plot
🚷 view threshold
5.26 µs
(+10.33%)Baseline: 4.77 µs
5.60 µs
(93.82%)
Adapter::Magic (JSON)📈 view plot
🚷 view threshold
5.07 µs
(+9.54%)Baseline: 4.62 µs
5.38 µs
(94.12%)
Adapter::Magic (Rust)📈 view plot
🚷 view threshold
27.45 µs
(+5.08%)Baseline: 26.12 µs
28.60 µs
(95.97%)
Adapter::Rust📈 view plot
🚷 view threshold
4.65 µs
(+25.54%)Baseline: 3.71 µs
5.39 µs
(86.27%)
Adapter::RustBench📈 view plot
🚷 view threshold
4.65 µs
(+25.69%)Baseline: 3.70 µs
5.37 µs
(86.63%)
🐰 View full continuous benchmarking report in Bencher

@epompeii
epompeii force-pushed the u/ep/parameters-api/thresholds branch from 70525c7 to 5d16149 Compare August 26, 2026 12:18
@epompeii
epompeii force-pushed the u/ep/parameters-api/thresholds branch from 5d16149 to 1a3e5a0 Compare August 27, 2026 03:46
A threshold gated one thing: the `value` scalar of every grid point of its
measure. A benchmark now reports as many grid points as it has parameter sets and
as many named scalars as its harness measured, and neither was addressable. A
project that wanted `p99` watched on the one configuration where it matters had
nowhere to say so.

A threshold gains two optional fields. `metric` is the name it gates, and a
threshold that names none gates the conventional `value` name: a threshold always
gates exactly one name and a bare one never gates all of them. `parameters` is a
filter over grid points, a list of parameter sets that is an OR across the list
and a subset match within each set, and a threshold with no filter gates every
grid point. Both defaults are what every existing threshold already does, so no
existing row moves and no project's alert volume changes.

Every threshold that matches a metric row runs. There is no winner: not in
gating, not in display. A grid point that a bare threshold and a filtered
threshold both match earns a boundary from each and, on a regression, an alert
from each. That is the design and it is pinned by a test, because a row that two
people asked to be watched is a row two people hear about.

Identity is the three dimensions plus the two new fields, under the null
semantics they carry. SQLite treats nulls as distinct in a unique index, so two
bare thresholds on one branch, testbed, and measure would no longer collide under
a plain unique key over the five columns. The key is declared over the effective
values instead, `COALESCE(metric, 'value')` and `COALESCE(parameters, x'')`, and
the wire canonicalizes into them: an explicit `value` and an absent name are one
threshold, an empty filter and an absent one are one threshold, and a filter has
one spelling because its sets sort by their RFC 8785 canonical bytes and
duplicates collapse.

A threshold gates the sample it names. The historical query behind detection
filters on the threshold's metric name, so a threshold on `p99` is tested against
`p99` rows and never against the `value` rows beside them, and the per grid point
separation stays exactly as it was.

`boundary` keys on `(metric_id, threshold_id)` rather than on `metric_id` alone,
because a metric row may now carry a boundary per threshold that gated it. Both
tables are rebuilt with their unique keys built after the copy, which is what
keeps the rebuild at the cost of the scan.

`JsonAlert` gains `value`, the scalar the alert fired on, and its `metric` triple
becomes optional: the triple is a convention over the `value` name, so an alert
on any other name has none. Every alert that a threshold could raise before this
carries the triple exactly as it did. The gated name is readable at
`alert.threshold.metric`.

The deprecated singular `threshold`, `boundary`, and `alert` fields carry the
bare threshold's gate and no other's, everywhere they appear. That is precisely
what a caller from before named gating has always been shown: a row that only a
named or filtered threshold gates reports no gate in them at all. Where a list of
boundaries is returned, it is ordered by threshold creation time, oldest first,
with the UUID breaking a tie.

The in-report `thresholds.models` map is unchanged and still addresses the bare
threshold: a map that names a measure and a model says nothing about a name or a
grid point, so it neither creates nor resets anything narrower.
@epompeii
epompeii force-pushed the u/ep/parameters-api/thresholds branch from 1a3e5a0 to 6b139dc Compare August 27, 2026 05:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant