This pipeline builds the datasets used for performance test selection and for the performance regression predictor. It snapshots Perfherder alerts and the pushes they refer to, applies one labeling policy, and publishes versioned databases like the rest of the bugbug data pipeline.
Everything is produced by the bugbug-data-perf-alerts command, defined in
scripts/perf_alerts_retriever.py, with the logic in the bugbug.perf
package. One more command, bugbug-data-perf-commit-risk in
scripts/perf_commit_risk_scorer.py, scores every commit of the pushes
snapshot with the perf regression predictor, so evaluations of scheduling
strategies that depend on risk scores never have to call the prediction
endpoint. The simulator that replays those strategies over these datasets is
described in perf-scheduling-simulation.md.
| Module | Role |
|---|---|
bugbug/perf/label_policy.py |
every hand-maintained table that decides labels, and POLICY_VERSION |
bugbug/perf/task_labels.py |
pure functions: label parsing, grouping hierarchy, exclusions |
bugbug/perf/extraction.py |
query builders and fetch/parse for Treeherder, fxci and Bugzilla |
bugbug/perf/sql/ |
the SQL templates, sharing the perf_task_filter.sql predicate that selects perf tasks |
bugbug/perf/datasets.py |
dataset paths, registrations, readers and the archive loader |
bugbug/perf/regression_labeling.py |
PushIndex, culprit pinning, labeled regression records |
bugbug/perf/push_status.py |
per-push label state and settled flag; the shared source of negatives |
bugbug/perf/selection_pairs.py |
test selection pairs and the labeled pair stream |
bugbug/perf/regression_commits.py |
commit-level dataset for the regression predictor |
bugbug/perf/commit_risk.py |
the predictor's ignore rules and per-patch scoring, shared with the HTTP service and the training set export, and the batch scorer over a clone |
bugbug/perf/calibration.py |
the mapping from raw risk scores to culprit rates, fitted by calibrate, applied by the HTTP service and the simulator |
bugbug/perf/task_costs.py |
joining pushes to task costs |
Dependencies point one way: label_policy imports nothing from the package,
task_labels reads its tables, extraction and regression_labeling build on
both, push_status builds on regression_labeling, and selection_pairs and
regression_commits both take their negatives from push_status.
commit_risk reads pushes through regression_labeling and the clone through
bugbug.repository; the HTTP service imports it, never the reverse.
| Source | Access | What it provides |
|---|---|---|
| Treeherder Postgres replica | STMO (Redash) data source 89, SQL | Perfherder alerts, summaries and their statuses, signatures, pushes, commits, performance jobs |
BigQuery fxci dataset |
STMO (Redash) data source 63, SQL | Worker pool, runs and machine time of every performance task |
| Bugzilla | REST API, optional token | The regression bug of each summary and the canonical bugs its duplicates point at, with regressed_by to pin the culprit commit and the resolution to decide the outcome |
Mercurial clone (mozilla-central) |
hg export |
Commit messages and diffs for the regression predictor dataset, and the commits scored by the commit risk scorer |
| hg.mozilla.org pushlog | json-pushes JSON API |
The pushlog id of each push, which Treeherder does not store and the taskgraph's push-count rules are defined on |
| Perf regression predictor | published checkpoint | The risk score of every commit, computed by the commit risk scorer |
The pipeline only handles autoland (REPOSITORY in label_policy.py).
Sheriffs attribute regressions to autoland pushes, and a mozilla-central push
is a merge whose commit list repeats every autoland commit it merged, so
including it produced no positives and turned culprit commits into negatives
through their merge push. With autoland alone, each commit lands in exactly one
push, which the labeling relies on.
The Redash key is read from REDASH_API_KEY, or from the REDASH_API_KEY
entry of the Taskcluster secret when running in the pipeline. An optional
BUGBUG_BUGZILLA_TOKEN lifts Bugzilla rate limits.
The alert-to-task mapping is done in SQL by joining each alert's signature to the datapoints on the summary's push and from there to the job and its type. Treeherder deletes jobs after 120 days, so the source can only map alerts from the last four months, and it currently holds alert summaries for about fourteen months. The alerts snapshot therefore acts as an archive: every run refetches whatever the source still has and merges it into the previous snapshot, keeping rows the source no longer returns and carrying forward task labels captured while the job still existed. Mapped history grows by a fortnight per run; only alerts already older than four months on the first run stay unmapped.
The alerts, pushes and task costs snapshots are archives: each run merges into
the previous snapshot, and they are the only copy of what the sources have
deleted, so they cannot be rebuilt. load_previous_snapshot in
bugbug/perf/datasets.py guards them. A run starts a new archive only when the
Taskcluster index has never published one. If it was published but cannot be
fetched, the run fails and publishes nothing, so the next run starts from the
intact snapshot. Locally, an existing copy in ./data/ is used instead.
Their format changes by these rules, also written above their registrations:
- Store what the source returned. Fields computed from bugbug's rules, such as normalized task labels or a commit's bug id and backout flag, are derived when reading, so editing a rule applies to archived rows too and never changes the format.
- Adding a field needs no version bump. Readers must treat it as optional, because archived rows that the source no longer returns never get it.
- Renaming or removing a field, or changing what it means, needs a version bump and a converter from the previous version. Until one is written, the run fails on the version mismatch instead of starting over.
retrieve snapshots the sources into four raw databases:
data/perf_alerts.json: one row per alert, attached to the summary it currently belongs to (reassigned alerts followrelated_summary_id), with summary, signature, platform, push and task label fields. Refetched whole and merged into the previous snapshot, see Sources.data/perf_pushes.json: one row per autoland push, with its commits (revision and first line; the bug id and backout flag are derived when reading, so the current rules apply to archived pushes) and the performance jobs that ran on it (label, tier, total duration, and one entry per run with task id, retry id, result and duration). Jobs are picked by harness name markers; the harnesses' own unit tests and toolchain fetches that share those markers are filtered out in the query. Pushes are retrieved incrementally in short windows and start three weeks before the alerts window, since Perfherder raises alerts up to two weeks after the culprit push; those lead pushes can be culprits but are never negatives, because their own alerts predate the snapshot. The most recent--reretrieve-daysare refreshed on every run because jobs keep completing; a refetched push keeps its stored jobs when the source returns fewer, since Treeherder deletes jobs after 120 days. Every push without apushlog_id, archived ones included, then gets it from the hg.mozilla.org pushlog, queried by push date and joined on the head changeset; pushes the pushlog does not list keepnulland are looked up again on the next run.data/perf_alert_bugs.json: the regression bugs referenced by summaries, plus the canonical bugs reached by followingdupe_ofup to three hops.data/perf_task_costs.json: one row per performance task fromfxci, with the worker pool (task_queue_id), run count and machine time. Machine time per pool is the cost unit: hardware pools such asreleng-hardware/*andproj-autophone/*run almost every performance test, andfxci's cloud billing figures are confidential, so the query never reads them and nothing in the snapshot is priced in money. Rows keep only the source columns; derive test identities withget_runnable_identity(label). Joins to the pushes snapshot on the task id of each job run. Like pushes, costs are retrieved incrementally: rows older than--reretrieve-daysare kept and the recent window is refetched. Expect roughly 300 bytes per task, so a full history is a few gigabytes uncompressed; read it withload_task_costs(task_ids)restricted to the pushes you need.
generate applies the labeling policy and writes
data/perf_regressions.json: one record per confirmed regression summary with
the culprit push and revisions, its landings, the pinned culprit commits, and
the regressed runnables in both signature and normalized task label form.
Summaries still untriaged get a record too, with the pending outcome, so
their push is not mistaken for a clean one.
generate also writes the thin test selection datasets, at commit
granularity so that landing-level and push-level views can be derived by
grouping (the reverse is impossible):
data/perf_scheduling_pairs.json: one row per culprit commit and test identity (the platform-independent test name), merging every platform and summary that regressed it. Each row carriespush_id,landing_id(the commit's bug within its push) andlanding_size, plusfamily,category,framework,application,platform_families,platform_labels,summary_ids,pinned_byandattribution. These are the positives. Pinned regressions label their culprit commits only; unpinned ones label every commit of the push withpinned_bynull so consumers can filter them. A pair named by several summaries keeps the strongestpinned_by, andattribution(commit,landingorpush) says how precisely the label points at the commit. Likewise,labels_from_signaturemarks rows whose task labels were all copied from other alerts of the same signature.data/perf_push_status.json: one row per push withsettled, itscommits(node, bug id, landing id, size and backout flag), and the test identities and platform families that ran. Negatives are not stored: every candidate identity on every commit of a settled push that is not a culprit push is a clean negative, andperf_alerts.iter_labeled_pairs()yields positives and inferred negatives together. Model features are deliberately not part of these files.
export reads the labeled regressions and the push status rows, joins them to
the Mercurial clone and writes
data/perf_regression_commits.json: one row per commit with commit_message,
diff, label, landing_id, landing_size, pinned_by, summary_ids and a
time based split. Negatives are sampled at --negative-ratio times the number of
positives from the clean settled commits in perf_push_status.json, the same
rule the selection pairs use, never a commit that is a culprit elsewhere.
Commits the predictor never scores are dropped, under the rules shared with the
HTTP service in bugbug/perf/commit_risk.py: merges, automated wptsync
imports and commits marked ignore-this-changeset. Backouts are kept, since a
revert regresses like any other change. Commits whose diff is empty are
skipped too.
bugbug-data-perf-commit-risk scores every commit of every push in
perf_pushes.json with the perf regression predictor and writes
data/perf_commit_risk.json: one record per commit and model version with
the commit's risk_score, or the ignored_reason the predictor skips it for
(merges, wptsync imports, ignore-this-changeset). It applies exactly the
rules and scoring of the perfregressionpredictor endpoint, which it shares
through bugbug/perf/commit_risk.py, with the commit metadata and patches
read from the clone instead of hg.mozilla.org. The dataset is an archive: a
run scores only the commits that have no current record for the model
version, so a run stopped by its --time-budget is resumed by the next one
and a new model version rescores the history while the old scores stay. A
record skipped under a rule that no longer exists, such as the backout rule
the predictor dropped, is stale: its commit is scored again and the new record
replaces it. Commits the clone does not have yet are skipped and counted, and
--until defaults to two days ago so that autoland commits have reached
mozilla-central. The model version is the checkpoint's own when its
bugbug_model.json declares one, otherwise the name of the published
artifact, such as perf-regression-predictor-v1; --model-version
overrides it for a local checkpoint.
calibrate turns those raw scores into probabilities. The predictor's
risk_score ranks commits but is not a probability of a regression: the model
was trained on far more positives than autoland produces, so a raw 0.6 is a
risky commit, not a 60 percent chance. The taskgraph's risk-aware batching
adds commit scores up as hazards, -log(1 - score), which is the probability
that at least one pending commit regressed only when each score is a
probability, and a threshold tuned on raw scores drifts as their distribution
moves. For each model version in perf_commit_risk.json, calibrate pairs the
scored commits with the labels (the pinned culprit commits of positive
regressions against the commits of settled clean pushes; the other commits of
culprit pushes, unpinned regressions and pending or rejected pushes are
unknown and left out), fits an isotonic regression of the culprit rate on
the raw score over equal-count bins of 50 commits, so that no step of the
mapping rests on the one or two top-scoring commits (the weighted fit is
bugbug/model_calibration.py's, shared with test selection), and writes it as
breakpoints to data/perf_risk_calibration.json, one record per model
version with at least --min-positives (50) culprits. The mapping never
reorders commits; it changes what a threshold means. The record
also reports how a mapping fitted without the newest --holdout-days (60)
does on those days: log loss, Brier score and expected calibration error of
the raw and calibrated scores, and the precision and recall of the top 1, 2
and 5 percent of commits, which calibration cannot change. The HTTP service
applies the published mapping to the risk_score it returns and the
simulator fits its own on its optimize window; the scorer keeps storing raw
scores, so a refit never means rescoring the archive.
Flag defaults are the DEFAULT_* constants at the top of
scripts/perf_alerts_retriever.py; the pipeline tasks pass no flags, so those
are the production values. generate and export share the window flags.
--settle-days (default 21) drops pushes younger than that, since their alerts
may not have fired or been triaged yet. --since DATE and --until DATE
restrict the dataset to pushes in that range, so a narrower or reproducible
dataset needs no new retrieval; --until never extends past the settle cutoff.
Without --since, culprits start with the first fetched alert and negatives
with the first alert summary in the snapshot, since earlier pushes may have
alerts the snapshot lacks. export takes settled from the push status rows,
so it can narrow the window generate used but not widen it.
All datasets are newline-delimited JSON, one record per line, compressed as
.zst when published. The raw query columns are documented in the headers of
the SQL files under bugbug/perf/sql/; the tables below describe the records
as stored, after the reshaping done in bugbug/perf/extraction.py. Types use
? for nullable fields and ts for ISO 8601 timestamps.
Every column of perf_alerts.sql, with the pipe-joined columns split into
lists. An alert belongs to the summary it is currently attached to, so a
reassigned alert carries its new summary's fields. The decoded status names
and normalized task labels are not stored: derive_alert_fields adds
summary_status_name, alert_status_name and normalized_labels (task_labels
after platform renames, deduplicated and sorted) when the alerts are labeled,
so edits to those tables reach archived alerts too.
| Field | Type | Meaning |
|---|---|---|
alert_id |
int | Perfherder alert id |
alert_status |
int | 0 untriaged, 1 downstream, 2 reassigned, 3 invalid, 4 acknowledged, 5 infra |
is_regression |
bool | direction of the shift; false is an improvement |
amount_pct, amount_abs |
float | size of the shift, relative and absolute |
t_value |
float | Student t statistic the detector used to raise the alert |
prev_value, new_value |
float | series value before and after the shift |
noise_profile |
str | OK, OUTLIERS, SKEWED, MODAL or N/A; how noisy the series is |
alert_manually_created |
bool | created by a sheriff rather than the detector |
alert_sheriffed |
bool | the series is part of the sheriffed set |
alert_summary_id, related_summary_id |
int, int? | summary the alert was created under, and the one it was reassigned to |
alert_created |
ts | when the detector raised it |
summary_id |
int | the summary it currently belongs to: related_summary_id when set, else alert_summary_id |
summary_status |
int | 0 untriaged, 1 downstream, 2 reassigned, 3 invalid, 4 improvement, 5 investigating, 6 wontfix, 7 fixed, 8 backedout, 9 infra |
summary_created, first_triaged, last_updated |
ts, ts?, ts | summary opened, first sheriff action, last change |
bug_number, bug_status, bug_updated |
int?, int?, ts? | linked bug, Perfherder's cached Bugzilla status code for it, and when it was linked; Treeherder renamed its bug_status column bug_resolution on 2026-10-26, the stored name is kept |
summary_manually_created, summary_sheriffed |
bool | created by hand; false for automated monitor summaries |
notes |
str? | free-text sheriff notes |
push_id, prev_push_id |
int | culprit push and the last push before it with a datapoint |
original_push_id, original_prev_push_id |
int | the same pair as first detected, before any sheriff edit |
revision, push_time |
str, ts | culprit push as changeset hash and timestamp |
prev_push_revision, prev_push_time |
str?, ts? | previous push; null for a series' first datapoint |
repository |
str | always autoland |
framework |
str | browsertime, talos, awsy, mozperftest, build_metrics, ... |
signature_id, signature_hash |
int, str | Perfherder series id and its stable identity hash |
suite, test |
str | e.g. amazon and fcp; test is empty for a suite summary |
extra_options |
str | space-separated options such as cold fission webrender |
application |
str | firefox, chrome, fenix, ...; empty for some frameworks |
lower_is_better |
bool | direction in which the metric improves |
should_alert |
bool? | whether the series is configured to alert at all |
alert_severity |
str | critical, subcritical or normal, as set in-tree |
measurement_unit |
str? | ms, score, bytes, ... |
signature_tags |
str | space-separated tags on the series |
platform |
str | machine platform, e.g. linux2404-64-shippable |
summary_tags |
list[str] | sheriff tags on the summary, e.g. harness or infra, split from the pipe-joined column |
task_labels, task_ids |
list[str] | task labels and Taskcluster task ids of the datapoints that triggered the alert, split from the pipe-joined columns |
| Field | Type | Meaning |
|---|---|---|
push_id |
int | Treeherder push id |
pushlog_id |
int? | hg.mozilla.org pushlog id; null until the pushlog lists the push, and absent from rows archived before it was added |
repository |
str | always autoland |
revision |
str | head changeset hash |
time |
ts | when the push landed |
author |
str? | who pushed |
commits[] |
list | in push order: revision, desc (first line); readers add bug_id (int?) and backout (bool), derived from desc by the current rules |
perf_jobs[] |
list | one entry per perf task label that ran: label, tier (int?), duration (total seconds), runs[] |
perf_jobs[].runs[] |
list | one entry per run: task_id (str?), retry_id (int?), result (str), duration (int?) |
Bugzilla REST fields, stored unchanged. Bugs linked on improvement summaries
are included, and those often have an empty regressed_by because the linked
bug is the change itself. Bugs reached by following dupe_of are included too.
Restricted bugs, those in any Bugzilla group, are dropped because this file is
published, and their dupe_of is not followed.
| Field | Type | Meaning |
|---|---|---|
id |
int | bug number |
summary |
str | bug title |
status, resolution |
str | live Bugzilla state, e.g. RESOLVED and WONTFIX; resolution is empty while open |
product, component |
str | where the bug is filed |
keywords |
list[str] | e.g. perf-alert, regression |
creation_time, last_change_time |
ts | when filed and last modified |
regressed_by |
list[int] | bugs the developer named as the cause; the first source of truth for culprit pinning |
regressions |
list[int] | bugs this one is recorded as having caused |
dupe_of |
int? | the bug this one was closed as a duplicate of |
depends_on, blocks |
list[int] | dependency links, kept for reference |
The columns of perf_task_costs.sql restricted to COST_FIELDS; the query's
test_platform and test_suite tags are dropped since label carries the
same information. Derive identities with get_runnable_identity(label) when
reading.
| Field | Type | Meaning |
|---|---|---|
task_id |
str | Taskcluster task id; joins to task_ids in the alerts and to perf_jobs[].runs[].task_id in the pushes |
label |
str | task label, e.g. test-linux2404-64-shippable/opt-talos-g1 |
project |
str | always autoland |
task_queue_id |
str | worker pool, e.g. releng-hardware/gecko-t-linux-talos-2404; also names the machine type |
submission_date |
str | day the task was created, YYYY-MM-DD |
runs |
int? | runs that started and finished, retries included |
duration |
int? | total machine seconds across those runs |
| Field | Type | Meaning |
|---|---|---|
summary_id |
int | the Perfherder summary |
repository, framework |
str | where and which harness |
status |
str | sheriff's summary status: investigating, wontfix, fixed or backedout |
resolved |
bool | true for the three terminal summary statuses |
summary_tags |
list[str] | sheriff tags, e.g. harness |
outcome |
str | concluded, attributed, pending or rejected; only the first two are positives, see the labeling policy |
created |
ts | summary creation |
bug_number, bug_status |
int?, int? | linked bug and Perfherder's cached status code |
canonical_bug |
int? | the bug reached by following dupe_of when the linked bug is a duplicate |
bug_state, bug_resolution |
str?, str? | live Bugzilla status and resolution of the canonical bug, e.g. RESOLVED and WONTFIX |
push_id, revision, push_time |
int, str, ts | the culprit push, taken as accurate |
reassigned |
bool | a sheriff moved the summary from its original push |
revisions |
list[str] | all commits of the culprit push, in order |
landings[] |
list | commits grouped by bug: landing_id, bug_id, backout, revisions, culprit; backouts are included and flagged, since a revert can be the culprit |
culprit_commits |
list[str] | commits pinned as the cause; empty when unpinned |
pinned_by |
str? | bug (via regressed_by), summary_bug (the summary links the culprit bug), single_landing, or null |
regressed_by_agrees |
bool? | whether the linked bug's regressed_by landed in the culprit push; null when there is nothing to compare |
runnables[] |
list | one entry per regressed test series, see below |
policy_version |
int | rules version that produced the record |
Each runnables[] entry is one regressed test series, deduplicated per
signature and task label:
| Field | Type | Meaning |
|---|---|---|
alert_id, alert_status |
int, str | the alert and its decoded status |
signature_id, signature_hash |
int, str | the Perfherder series |
framework, suite, test |
str | harness, page or benchmark, and metric, as in perf_alerts.json |
platform, extra_options, application |
str | where it ran, its options, and the browser under test |
amount_pct, t_value, noise_profile |
float, float, str | size and confidence of the shift, and series noisiness |
label |
str? | normalized task label that produced the datapoints; null when unmapped |
label_from_signature |
bool | the alert had no label of its own and label comes from other alerts of the same signature |
test_name, family, category |
str? | grouping levels from the label; null when unmapped |
categories |
list[str] | every category the test measures, primary first; empty when unmapped |
platform_family |
str | OS family, from the label when mapped, else from platform |
| Field | Type | Meaning |
|---|---|---|
repository, push_id, revision, push_time |
the culprit push (revision is its head) |
|
node, bug_id, backout |
str, int?, bool | the commit, the bug parsed from its message, and whether it is a backout or revert |
landing_id, landing_size |
str, int | push_id:bug_id and how many commits share it; group on it for landing rows |
pinned_by |
str? | how the commit was pinned, the strongest across merged summaries; null when the whole push was labeled |
attribution |
str | commit (a pinned single-commit landing), landing (a pinned multi-commit landing) or push (unpinned, or a negative) |
test_name |
str | platform-independent test identity, e.g. browsertime-tp6-firefox-amazon |
family, category |
str | grouping levels, e.g. browsertime-tp6 and page-load |
categories |
list[str] | every category of the test, primary first |
framework, application |
str | harness and browser under test |
label |
int | always 1; negatives are inferred, see perf_push_status.json |
platform_families |
list[str] | platform families that regressed, e.g. ["linux", "windows"] |
platform_labels |
list[str] | the normalized task labels that regressed |
labels_from_signature |
bool | every label in platform_labels was taken from other alerts of its signature |
summary_ids |
list[int] | summaries merged into this pair |
policy_version |
int | rules version |
| Field | Type | Meaning |
|---|---|---|
repository, push_id, revision, push_time |
the push | |
label_state |
str | culprit (has a positive regression), pending, rejected, or clean; only settled clean pushes are negatives |
settled |
bool | inside the labeled window: older than the settle window and not before the first alert summary in the snapshot, whose alerts would otherwise be missing |
commits[] |
list | all commits: node, bug_id, backout, landing_id, landing_size |
identities_ran |
list[str] | test identities of in-scope perf tasks that ran |
platform_families_ran |
list[str] | platform families those tasks ran on |
policy_version |
int | rules version |
A settled push whose label_state is clean is a negative for every
candidate identity, and so is each of its commits, backouts included;
iter_labeled_pairs() materializes those negatives on the fly. Pending and
rejected pushes yield neither positives nor negatives. At prediction time, commit scores
are aggregated to the push by taking the maximum per test.
| Field | Type | Meaning |
|---|---|---|
node |
str | changeset hash |
repository, push_id, pushdate |
str, int, ts | the push the commit landed in |
bug_id |
int? | bug parsed from the commit message |
landing_id, landing_size, backout |
str, int, bool | the commit's landing, and whether it is a backout; weight or group by landing when training |
label |
int | 1 for culprit commits, 0 for sampled clean commits |
pinned_by |
str? | as in perf_regressions.json, the strongest across the summaries naming the commit; null for negatives and unpinned positives |
attribution |
str | commit, landing or push, as in the scheduling pairs |
summary_ids |
list[int] | summaries that named this commit; empty for negatives |
split |
str | train or test, about the newest tenth, cut at a push time so no push is in both |
commit_message |
str | full message from hg export, header lines removed |
diff |
str | the git-style diff from hg export |
policy_version |
int | rules version |
| Field | Type | Meaning |
|---|---|---|
node |
str | changeset hash |
push_id, pushlog_id, push_time |
int, int?, ts | the push the commit landed in, as in perf_pushes.json |
ignored_reason |
str? | merge, wptsync or ignore-this-changeset when the predictor skips the commit |
risk_score |
float? | the model's raw positive-class probability, which perf_risk_calibration.json maps to a culprit rate; null for ignored commits |
prob, class |
list?, int? | both class probabilities and the predicted class; null for ignored commits |
model_version |
str | the checkpoint that produced the record; read one version at a time |
scored_at |
ts | when the scorer run that produced the record started |
| Field | Type | Meaning |
|---|---|---|
model_version |
str | the checkpoint whose raw scores the mapping takes |
method |
str | isotonic |
x, y |
list[float] | breakpoints: increasing raw scores and the non-decreasing probability at each; interpolate linearly between them and take the nearest end outside |
fit_window |
{since, until} | push times of the oldest and newest labeled commit the mapping was fitted on |
commits, positives, base_rate |
int, int, float | labeled scored commits fitted on, culprits among them, and their share |
bin_size |
int | commits per equal-count bin the regression was fitted over |
holdout |
obj? | the newest --holdout-days: since, until, commits, positives, the raw and calibrated reliability (log_loss, brier, ece) of a mapping fitted on the older commits, and ranking (precision_at_N_percent, recall_at_N_percent for 1, 2 and 5); null without culprits on both sides |
fitted_at |
ts | when the calibrate run started |
policy_version |
int | rules version |
The rules live in bugbug/perf/label_policy.py and are stamped on every derived
record as policy_version. Bump it when they change.
- A summary enters labeling when its status is
investigating,wontfix,fixedorbackedoutand it is not taggedinfra. Harness-caused regressions are kept: a harness change is a repository change the scheduler must react to, andsummary_tagslets a consumer filter them. Anuntriagedsummary enters aspending: Treeherder keeps a summary untriaged while any of its alerts is, even next to an acknowledged regression with a fixed bug, so its push is neither a positive nor a negative until a sheriff triages it. - Whether it counts is then decided by the developer's verdict on the
regression bug, which is far more current than the sheriff's summary status
(most confirmed summaries stay
investigatingforever). Duplicates are followed to their canonical bug first. Theoutcomeis:concluded: the bug was resolvedFIXED,WONTFIXorWORKSFORME.WORKSFORMEis kept because the metric did move even if nobody acted.attributed: the sheriff linked the culprit bug itself instead of filing a regression bug, or closed the summary aswontfix,fixedorbackedoutwithout a bug. The bug's status describes the culprit change there, not the regression, so the link itself is taken as the conclusion.pending: the summary is stilluntriaged, the bug is still open, the summary isinvestigatingwith no bug, the duplicate's canonical bug is unknown, or the linked bug is not inperf_alert_bugs.json(for example a restricted bug), so the verdict is unknown.rejected: the bug was resolvedINVALID,INACTIVE,INCOMPLETEorMOVED: developers concluded it was not a regression. Onlyconcludedandattributedsummaries are positives.pendingandrejectedones make their push neither a positive nor a negative (label_stateinperf_push_status.json). Bugs resolve a median of eight days and up to three weeks after the summary, so pending records promote to concluded on later pipeline runs.
- Within a summary, an alert counts when
is_regressionis set and its status is notdownstream,invalidorinfra. Summaries without such alerts are dropped. - Summaries whose culprit push is younger than
--settle-daysare skipped, as are such pushes in push data and negative sampling. Both bounds are on push time, so a push counted as settled never has an alert that was dropped as too recent; alerts fire up to two weeks after the push. - The culprit push named by the summary is taken as accurate. No attribution range or confidence weighting is applied.
- Within the culprit push, commits are grouped into landings (one bug's
commits), backouts included and flagged. The culprit landing is pinned when
the regression bug's
regressed_bynames a bug landed in the push, when the summary links the culprit bug directly, or when the push holds a single landing, a backout counting as one. Otherwise every commit of the push is labeled withpinned_bynull. Each regression also recordsregressed_by_agrees, whether the bug link and the summary name the same push, as a quality signal. - Backouts are ordinary commits on both sides: a revert can be the culprit,
and a revert in a clean push is a negative. Rows keep the
backoutflag so consumers can filter them. - Negatives are inferred, not observed: any settled push that is not a culprit is clean for every sheriffed test, because the backstop runs every performance test and any regression would have been detected and attributed. This inference only holds while the backstop keeps running the full performance suite. Each commit lands in exactly one autoland push, so a commit of a culprit, pending or rejected push is never a negative.
Task labels are normalized by dropping the -qr platform suffix and folding
retired platforms into their successors through PLATFORM_RENAMES.
Things the datasets do not establish, which an evaluation has to account for:
- A negative means that no confirmed regression was attributed to a settled
push, inferred from the backstop running every perf test; it is not a
per-test check that the test ran and measured flat on that push.
identities_raninperf_push_status.jsonrecords what actually ran. - Positive labels are only as precise as
attributionsays:commitnames the change,landinga bug's commits,pushevery commit of the push. A strict training view keepscommitrows;landing_sizeallows weighting. - Negatives come from clean pushes only. A change that regressed one test contributes no negative for the tests it left alone, so the datasets say little about which test a risky change affects.
- Labels use the current state of Bugzilla and task labels learned from the
whole alert snapshot, including alerts newer than
--until, which bounds push dates rather than what was known at that time. The test split is chronological but not a replay of what a model trained back then would have seen; related changes such as relandings can sit on both sides of it. - The predictor export samples
--negative-ratioclean commits per positive and then drops ignored commits, missing patches and empty diffs, so its class balance is neither production prevalence nor exactly the ratio asked for; the counts are logged. Model outputs are scores, not probabilities, until calibrated on data outside the training set. - Older alerts often have no task label, so a regression can be a predictor
positive without producing scheduling pairs;
label_from_signatureandlabels_from_signatureflag the recovered ones, andPLATFORM_RENAMESpools platforms whileplatform_labelskeeps the original labels. - Positive pairs carry outcome evidence (
summary_ids,platform_families,platform_labels,pinned_by,attribution) that negatives do not. A scheduler's features are the commit, its landing and the test identity; the rest describes the label. build_label_prices()prices labels from the whole cost snapshot by default; passrowsbounded by date to price a historical decision with what was known then.
Runnables out of scope for Firefox perf test selection are dropped before labeling, and a summary made only of excluded runnables is not a regression:
- summaries tagged
infra, since infrastructure changes are not in any push; - the
build_metricsframework, which measures builds rather than tests, and thejs-benchframework, the SpiderMonkey shell benchmarks run fromsource-test-jsshell-bench-*tasks outside the perf harnesses; devtoolssuites other thandamp, such asreload-*anddebugger-metrics, which mochitest-devtools jobs report and which are not perf tasks, listed inFRAMEWORK_PERF_SUITES;- tasks running a non-Firefox application (Chrome, Chromium, Safari), which a Firefox change cannot regress;
- profiling variants (
-profiling,-native-profiling), which are diagnostics, listed inEXCLUDED_VARIANTS; - the synthetic
regression-testscanaries and the harness sample tests; - labels that do not parse as test tasks although they carry a harness
marker: the harnesses' own unit tests (
source-test-python-raptor-*) and toolchain fetches (toolchain-*-talos-pdfs).
Many alerts arrive without a task label: Treeherder expires jobs after 120
days, and some datapoints never link to a job. A signature is one test series
on one platform, so such an alert takes the label that the other alerts of its
signature name, when they all name the same one, and is flagged
label_from_signature. Every rule then applies to it. Alerts left without a
label go through the rules the signature answers: the framework, the suite and
the signature's application. The rules that need a label, variants, canaries
and unparsed labels, cannot apply to them.
get_exclusion_reason(label, framework, application, suite) returns which rule
applied.
Every perf task label is described at four levels by get_runnable_identity:
| Level | Example | Derived by |
|---|---|---|
| task label | test-linux2404-64-shippable/opt-browsertime-tp6-firefox-amazon |
normalization |
test identity (test_name) |
browsertime-tp6-firefox-amazon |
label minus platform and build type |
| family | browsertime-tp6 |
the kind-file entry: harness plus test, before the application token |
| category | page-load |
keyword rules in CATEGORY_RULES; categories lists every match, category is the first |
The platform is a separate axis, platform_family (linux, windows, macosx,
android), because most regressions are platform specific. Datasets are keyed
on the test identity; family and category are attached as features so rare
tests borrow strength from their siblings. split_test_name also exposes the
application and the diagnostic variant.
A test can measure more than one thing, so every matching rule applies and
categories holds them all, primary first: a YouTube power test is media
and power, awsy-tp6 is memory and page-load, MotionMark is graphics
and benchmark. The categories are:
| Category | Measures | Examples |
|---|---|---|
page-load |
loading pages | tp6, tp6m, tp5o, speculation rules, process switch |
startup |
starting the browser or restoring sessions | mozperftest startup, sessionrestore, ts_paint in talos-other |
responsiveness |
reacting to input and UI actions | browsertime-responsiveness, nav-bench, tabswitch, talos-chrome |
graphics |
painting, scrolling, WebGL, PDF rendering | MotionMark, talos-g1, svgr, pdfpaint, perf-reftest |
media |
audio and video playback, codecs | youtube-playback, webcodecs, video-playback-latency |
power |
energy and resource use | browsertime-power, media-playback, Android background-resource |
network |
transfer and DNS | network-bench, upload, TRR, hev3 |
storage |
on-disk stores | IndexedDB, Places, service worker caching |
memory |
memory footprint | awsy, Android resource tests |
benchmark |
synthetic JS, DOM and CSS workloads | Speedometer, JetStream, dromaeo, wasm |
ml |
on-device machine learning | mozperftest ML and translations tests |
devtools |
developer tools | talos-damp |
accessibility |
accessibility tree maintenance | mozperftest accessibility |
other is the fallback for a test no rule recognises, which is where a newly
added test lands until a rule covers it. None of the tests running on autoland
in the six months to September 2026 falls into it;
test_every_known_test_has_a_category in tests/test_perf.py lists one
identity per family and fails if a known family loses its category. To check
a fresh snapshot, group identities_ran from perf_push_status.json by
get_test_category and look at the other bucket.
Mozperftest labels do not follow the test-<platform>/<build>-<test> shape:
the platform comes first and, on Linux, again at the end, as in
perftest-android-hw-a55-aarch64-shippable-startup-fenix-cold-main-first-frame
or perftest-linux-service-worker-linux2404-64-shippable/opt.
split_perftest_label strips the platform tokens so the identity is
perftest-startup-fenix-cold-main-first-frame or perftest-service-worker,
comparable across platforms like every other test.
Mozilla uses two vocabularies that disagree with each other: Perfherder, where
alerts come from, says framework, suite, test; the task graph and harnesses,
where tests are defined, say harness, kind entry, app, variant. Our identity
fields are parsed from task labels, so they follow the task graph where the two
differ. For test-linux2404-64-shippable/opt-browsertime-tp6-firefox-amazon:
| Our field | Example | Perfherder | Task graph / harness |
|---|---|---|---|
framework (from the alert) |
browsertime |
framework | harness; Raptor and Browsertime report as one framework |
test_name |
browsertime-tp6-firefox-amazon |
none: one task feeds many signatures | the label minus its platform |
family |
browsertime-tp6 |
none | the kind-file entry; roughly Raptor's test type |
category, categories |
page-load |
none | none; ours, for the fallback; categories lists every match |
subtest (split_test_name only) |
amazon |
suite: the page or benchmark | app-specific tail of the test name |
application |
firefox |
application | app |
variant |
null (swr, profiling) |
inside extra_options |
test variant from variants.yml |
platform |
linux2404-64-shippable/opt |
platform + option collection | test platform |
build_type |
opt |
option collection | build type |
platform_family |
linux |
none | none; ours |
| none | fcp, loadtime |
test: one metric inside a suite | below the task; kept on runnables as suite and test |
Signature names cannot be turned into labels: the Talos suite glterrain runs
in the task talos-webgl, and the AWSY suite Heap Unclassified in
awsy-tp6. That is why the alerts query maps each alert to its job's task
label instead.
from bugbug import db
from bugbug.perf import datasets, selection_pairs
db.download(datasets.PERF_REGRESSIONS_DB)
for regression in datasets.get_perf_regressions():
print(regression["revision"], regression["pinned_by"], len(regression["runnables"]))
db.download(datasets.PERF_PAIRS_DB)
db.download(datasets.PERF_PUSH_STATUS_DB)
for pair in selection_pairs.iter_labeled_pairs():
# Positives carry family, category and platforms; negatives are inferred.
print(pair["push_id"], pair["node"], pair["landing_id"], pair["test_name"], pair["label"])
db.download(datasets.PERF_REGRESSION_COMMITS_DB)
for commit in datasets.get_perf_regression_commits():
print(commit["label"], commit["landing_size"], commit["split"])task_costs.load_task_costs() indexes the costs snapshot by task id and
task_costs.summarize_push_cost(push, costs) summarizes a push from the pushes
snapshot into machine seconds per worker pool. The generate operation logs
how many perf runs could be matched to a pool, so a drop in coverage is
visible in the task log.
A shadow scheduler's choices never ran, so they cannot be priced from what
ran. task_costs.build_label_prices() turns the snapshot into a price list instead: for
each normalized label, its usual pool and median run duration, valid on any
push because a task's duration barely depends on the commit under test.
task_costs.estimate_labels_cost(labels, prices) then estimates machine seconds per pool for
any set of labels, falling back to the median of the same family on the same
platform family for labels with no history. Its output has the same
machine_seconds_by_pool shape as summarize_push_cost, so a strategy's cost, the
actual cost and the full-suite cost can be compared directly.
from bugbug.perf import datasets, task_costs
costs = task_costs.load_task_costs()
prices = task_costs.build_label_prices()
for push in datasets.get_perf_pushes():
actual = task_costs.summarize_push_cost(push, costs)
everything = task_costs.estimate_labels_cost((job["label"] for job in push["perf_jobs"]), prices)
print(push["revision"], actual["machine_seconds_by_pool"], everything["machine_seconds_by_pool"])The script needs the bugbug virtualenv, REDASH_API_KEY in the environment
and, for export, a Mercurial clone of mozilla-central or mozilla-unified.
Run it from a scratch directory: every dataset is written to ./data/ under
the current directory.
export REDASH_API_KEY=...
bugbug-data-perf-alerts retrieve --months 1
bugbug-data-perf-alerts generate --settle-days 7
bugbug-data-perf-alerts export --settle-days 7 --repo-dir ~/repos/mozilla-unifiedexport reads the clone as it is and drops commits it does not have, with a
warning, so pull it first. --refresh-repo clones or pulls it for you, but it
also reverts local changes and strips unpushed commits, so only pass it for a
clone you keep for this purpose.
The commit risk scorer needs the perf-regression-predictor extra for
PyTorch and Transformers, and downloads the published checkpoint into
./perfregressionpredictormodel unless --model-dir points at one:
uv run --extra perf-regression-predictor bugbug-data-perf-commit-risk \
--repo-dir ~/repos/mozilla-unified --since 2026-09-01 --limit 200Inference runs on the CPU at roughly one commit per second, so bound a local
run with --since, --limit or --time-budget HOURS; the next run continues
where it stopped. With scores and labels in place, calibrate runs in
seconds:
bugbug-data-perf-alerts calibrate --settle-days 7Each command first tries to download the previous snapshot from the
Taskcluster index and falls back to the files in ./data/, logging a warning
until the pipeline has published once. retrieve takes --skip-alerts,
--skip-pushes, --skip-bugs and --skip-costs so one part can be rerun
alone. The short windows above finish in minutes; the production defaults
issue several hundred Redash queries and hold a year of pushes in memory. Once
the pipeline has run in production, generate and export work locally from
the published snapshots without running retrieve.
The perf tasks are part of infra/data-pipeline.yml, so they run with the
rest of the bugbug data pipeline once a release deploys them. The retrieval
task reads REDASH_API_KEY from the Taskcluster secret
project/bugbug/production, the entry the CI failures and try pushes
retrievers already use. Successful runs publish under the index routes below,
after which db.download() fetches the datasets anywhere. The scorer runs in
its own bugbug-perf-commit-risk image, built from
infra/dockerfile.perf_commit_risk with the other images in
docker-compose.yml: the commit-retrieval image plus the
perf-regression-predictor extra, which no other pipeline image ships.
On the first run the alerts snapshot can only map the last four months of alerts to task labels and the pushes older than that have no jobs, both because Treeherder deletes jobs after 120 days; later runs keep what earlier ones captured, see Sources.
infra/data-pipeline.yml runs perf-alerts-retrieval (compute-small, needs
the secret), then perf-regressions-generator (compute-large), then
perf-regression-commits-export, which runs in the bugbug-commit-retrieval
image and keeps its Mercurial clone in the same bugbug-mercurial-repository
cache as commit-retrieval. A worker cache stays on the machine that wrote it,
and the two tasks run on different worker types, so the export passes
--refresh-repo to clone or pull its own copy first. perf-commit-risk-scorer
runs after the retrieval in the bugbug-perf-commit-risk image with the same
clone cache and flags, plus --time-budget=20 so a run always writes its
archive within the task's 24 hours; the first run over a year of commits
takes several runs, later ones score the new pushes in minutes.
perf-risk-calibration (compute-small, bugbug-base image) runs calibrate
once the generator and the scorer are done. Artifacts are indexed under
project.bugbug.data_perf_alerts, project.bugbug.data_perf_regressions,
project.bugbug.data_perf_regression_commits,
project.bugbug.data_perf_commit_risk and
project.bugbug.data_perf_risk_calibration. Apart from the export's and
scorer's --repo-dir, --refresh-repo and the scorer's --time-budget, the
tasks pass no flags, so the DEFAULT_* constants in the two scripts are the
production configuration.