CH Loading the latest capture…

Energy & Carbon per Run

All scenarios of a run summed

CH1 AMD · CPU package, on its own scale

CH2 M2 · CPU cluster, on its own scale

CH1 AMD · average energy per run, by stage

CPU package power, on its own scale: never read against CH2.

CH2 M2 · average energy per run, by stage

CPU cluster power, on its own scale: never read against CH1.

CH1 · AMD Windows runner CPU package power, AMD µProf

CH2 · Apple M2 runner CPU cluster power, powermetrics (excludes GPU and ANE)

CH1 AMD · average energy per run, by stage

CPU package power, on its own scale: never read against CH2.

CH2 M2 · average energy per run, by stage

CPU cluster power, on its own scale: never read against CH1.

SCI, ISO/IEC 21031 SCI = (E × I) + M E energy consumed (kWh) · I grid intensity at run time (gCO₂eq/kWh) · M embodied carbon, not counted

Carbon Intensity Over Time

CH1 AMD · carbon per stage

Grams follow this runner’s own energy domain: its own scale.

CH2 M2 · carbon per stage

Grams follow this runner’s own energy domain: its own scale.

Showing...
Scaling check (methodology) Phase 1, 1M items: sort_operation ≈ 1.76 J Phase 2, 5M items: sort_operation ≈ 14.85 J 5× the items took 8.4× the energy; the infrastructure tax stayed flat. Two points show the trend; they do not prove a complexity class.

CH1 AMD · energy by span

The twelve spans that used the most energy, on this runner’s own scale.

CH2 M2 · energy by span

The twelve spans that used the most energy, on this runner’s own scale.

Level 1: the idea

Everything on this page follows from one problem and one trick. If you read only this much, you have the method.

The problem: nobody can price a test suite in Joules

A CI pipeline burns real electricity. You can already find out what the whole machine drew, and how long each step took. What no existing tool will tell you is how many Joules that one test suite cost — and you cannot optimise what you cannot attribute.

What you can use todayWhat it gives youWhat it cannot tell you
docker stats, topCPU %, memoryJoules — CPU % is not energy
Cloud invoiceMoney per instance-hourWhich job, stage or test spent it
RAPL / MSR, powermetricsReal Joules, whole CPU socketWhich operation caused them
Tracing / APM (OpenTelemetry)Milliseconds per operationAny energy figure at all

One side of the split knows energy but not structure. The other knows structure but not energy. Nothing joins them — and inside a container it gets worse, because the workload cannot see the host's power counters at all.

The trick: join them on the clock

Run the hardware power sensor and the operation timeline at the same time, against the same clock. Then walk the power trace one sample at a time and ask a question a database person would recognise — this is an interval join:

“During this 100 ms of measured energy, which operations were running, and for how much of it?”

Split that sample's Joules between them in proportion to the overlap. Do it for every sample and each operation accumulates its own energy total. Nothing is estimated from a model — every Joule attributed was physically measured at the socket first.

The operations are timed from outside the workload, by the CI job that launches them, so no source file in any profiled project is modified. That is what makes the method applicable to software you do not own.

1

See it happen — one sample being split

Press play. The cursor sweeps the power trace one sample at a time; watch the Joules drop into whichever operations were running. This single figure is the method.

t = 0.00 s

Press Play, or drag the slider, to step through the samples.

Illustrative trace with a 4 s window so individual samples are visible; real runs are 60 s or longer. The arithmetic shown is exactly what analyst_agent.deconvolve() performs.

2

Three rules that keep the number honest

The interval join above is the easy part. These three corrections are what make the result defensible — each one exists because leaving it out produces a specific, quantifiable lie.

1

Subtract the machine's own idle draw

Attribute only max(0, P − P_idle(t)) — the surplus above what the machine burns doing nothing.

Without it: an operation that merely exists for 10 s gets billed for 10 s of the operating system, screen, and background daemons. Long operations would look expensive purely for being long.

The idle level is measured per run — the median of the 100 samples before the workload and the 100 after it, interpolated between the two, so thermal drift is tracked rather than assumed away.

2

Integrate over the real sample spacing

Multiply each sample's surplus watts by dt, that sample's own measured gap — never by a hard-coded 0.1 s.

Without it: silent undercounting. Profilers do not hold their requested interval under load, and the error is invisible because the output still looks like a plausible number of Joules.

Real extract from a run: timestamps …59:358, …59:563, …59:669. Those gaps are 205 ms and 106 ms, not 100. Assuming 0.1 s would have undercounted that window by about a third.

3

Cap concurrent shares at 100 % of a sample

Scale the shares by min(1, dt / Σoverlaps) whenever several operations run at once.

Without it: you invent energy. Two operations each covering 60 ms of a 100 ms sample would take 0.6 + 0.6 = 120 % of it — attributing 3.0 J out of a 2.5 J sample.

The min(1, …) matters in the other direction too: when operations do not fill a sample, the remainder must stay unattributed rather than being inflated to fill the gap.

Emeasured  =  Σ Eoperations  +  Einfrastructure

The check that all three rules worked. Every measured Joule is either attributed to an operation or left in E_infrastructure — the container start-up, image pulls and scheduling that no single operation owns. That remainder can never go negative, which means the method cannot quietly invent or lose energy. It is the one property worth testing, so it is asserted on every run and re-checked on every stored row.

Level 2: how one measurement is produced

Concept settled, here is the actual procedure: how a single run is timed, how its idle baseline is removed, and the exact arithmetic that turns watts into per-operation Joules.

3

Profiling timeline (animated)

10 s pre-baseline → workload → 10 s cooldown. The profiler runs end-to-end at a requested 100 ms interval. Press play to see how data fills the window.

t = 0.0 s
Samples: 0
Pre-baseline 10 s
Workload
Cooldown 10 s
START_TIME
END_TIME
0 s    
Profiler running, system idle — the median of the last 100 samples before START gives Ppre The project's own unmodified CI command runs inside Docker Profiler keeps sampling — the median of the first 100 samples after END gives Ppost, so drift is measured, not assumed
4

Baseline subtraction — see it happen

The raw power trace mixes the workload's own energy with the runner's idle background. Phase 3 strips the background out. Step through the four operations the analyst performs on every CSV.

P_idle

Click 1. Raw trace to begin.

Samples —
Pidle pre → post —
Peak active —
Esoftware —

Synthetic trace for illustration — the analyst applies the same four operations to every real timechart.csv. Real runs at ResearchData/cpudata/ request 100 ms sampling and use the live runner's own idle level. The drift shown here is deliberately modest (about 7 % of the run's energy), so this illustrates a run that passes the quality screen rather than one the screen would flag.

Phase 1: Hardware Acquisition

AMD µProf: 100 ms requested, socket0-package-power (W) — whole CPU package

Apple powermetrics: 100 ms requested, --samplers cpu_power → the CPU Power line only — CPU cluster, excluding GPU and ANE

The two sensors read different electrical domains, so absolute Joules are never compared across platforms — only within-platform ratios are.

100 ms is the requested interval; the interval actually achieved is measured per run and stored as mean_sample_interval_ms, and every sample is integrated over its own spacing. Nyquist still limits resolution of sub-200 ms spikes — explicitly disclosed.

Phase 3: Signal Deconvolution

P_pre = median(100 samples before START)
P_post = median(100 samples after END)
P_idle(t) = P_pre + (P_post − P_pre) ⋅ progress(t)
E_software = Σ max(0, P_t − P_idle(t)) ⋅ dt_t

The baseline is a median (robust to idle spikes), taken twice and linearly interpolated, so thermal drift across the workload is tracked rather than assumed away. dt_t is each sample's real spacing, not a hard-coded 0.1 s.

Phase 4: Temporal Correlation

ΔP_t = max(0, P_t − P_idle(t))
E_t = ΔP_t ⋅ dt_t
overlap_ratio = span_overlap_in_sample / dt_t
share_scale = min(1, dt_t / Σ overlaps)
E_span += E_t × overlap_ratio × share_scale

Only the baseline-subtracted surplus ΔP is attributable — idle power is not a span's fault. share_scale caps concurrent spans at 100 % of a sample, which is what makes conservation hold.

Conservation: E_Total = Σ E_Spans + E_Infrastructure, with E_Infrastructure ≥ 0 — true by construction and asserted in test/test_attribution.py.

Conservation — how we know

This is a claim with a failure history, so it is stated with one. Conservation is not an assumption of the method; it is a property that share_scale makes hold, and it did not hold before that factor existed.

PeriodRowsViolations
before 2026-09-1727712
on/after 2026-09-172230

The 12 violations (bookstack 7, thelounge 3, fider 2, dated 12–16 Sep 2026) are detected by the spans_exceed_total rule and excluded from every model and report. They are not deleted — the original telemetry is in archived-telemetry/, so they can be re-derived under the corrected algorithm rather than re-measured.

Two independent checks: 12 unit tests assert the identity on synthetic spans including the concurrent case, and the read-time screen re-checks it against the stored numbers for every row.

Validation Packet

Full methodology audit, validity threats, and reviewer checklist:

Level 3: what has been measured, and how bad data is kept out

A method is only as good as the corpus it produced. This level covers what has actually been profiled, the gates that stop unusable data from being believed, and the raw artefacts kept for every run so any number here can be re-derived.

5

Pick a scenario

Each CI/CD stage is profiled with the same baseline/workload/cooldown protocol, on two architectures. The walkthrough below follows BookStack in detail; it is one of five profiled projects, and the protocol is identical for all of them.

What has actually been measured live database, 2026-09-20
—projects
—distinct scenarios
—pipeline runs
—scenario measurements
—attributed spans
—CPU architectures
ProjectStackRunsScenarios profiled
Loading from the live database…

 

Scenario names differ per project because each one runs that project's own CI commands — no source file in any profiled repository is modified. default is the original Node.js proof-of-concept, retained because its spans are synthetic and therefore independently verifiable.

6

The 7 pipeline gates — click any gate to expand

Every run passes through these gates in order. The first five block bad data from entering the research database; Screen then decides what is fit to be used, because some defects are only visible once the energy has been computed. Govern is what closes the loop and schedules the next run.

7

What we keep — click an artefact to see a real sample

Per scenario we persist 4–5 files. Together they let anyone reconstruct, audit, and re-process the run from scratch.

8

W3C trace context — the seam between power data and span data

Hover any part of the traceparent below to see its role.

00 - 168962b8fd5b4ab9b459644 3eb33ae61 - 1ed8bf1bc786403c - 01
Hover any segment above …

Database Schema & Relations

Loading schema…

Relationship Explorer

Pick a pipeline run to see how one energy_metrics row links to its child span_metrics rows (joined on project_name · run_id · scenario).

Table Data (all rows)

Download Full Dataset (CSV)

Download = one unified dataset (energy_metrics joined to span_metrics), not separate files.

Loading...
Project Run ID Scenario Prefix (bucket area) Size Last Modified Download
Loading...

Drag & drop a CSV file here

or click to browse

Agent Platform

Every agent is its own process with its own RabbitMQ queue, and they talk in FIPA ACL messages. The one shortcut: the governance agent reads each task's energy history straight from the attribution agent's database tables. This list is read live from the Directory Facilitator, where each agent registers with a 3-minute lease.

Grid Carbon at Each Decision

Dashed: the deferral threshold (median of the previous 14 days). Amber points: jobs held for a cleaner hour.

What the Runners Bid: Quota Share

Share of each runner's daily energy quota the job would leave used. Runners are compared on this and on finish time — never on Joules: AMD µProf measures the whole CPU package, powermetrics only the M2 CPU cluster.

Governance Audit Trail

Click a row to see the agents' conversation. Energy is the winning runner's, in its own measurement domain.

Date Project Priority Decision Status Winner Quota Predicted Measured Error
Loading...

Dry-Run Negotiation

Runs the real agents in-process (in-memory transport) on the live history. Nothing is dispatched; the decision is stored as simulated.

Deadlines: urgent 2 h and never held for carbon; normal and batch 14 h (up to 12 h held + 2 h to run).

Energy Regression Alerts — recorded with each result

Spans the agent flagged in a governed run's measured result (or in spans supplied to a dry run), against the history at that moment. A historical record: it does not change as the corpus grows. For the detector's verdict on the corpus as it stands now, see Flagged Outlier Spans.

Date Job Flagged Span Ratio vs Median Robust z
Loading...

Run a Governed Job — team devices only

Asks the governance agent to run a pipeline, as python -m agents submit does: the runners bid, the winner starts the workflow, and the measured result and its energy-gate verdict appear in the tables on this page. Only approved team devices can submit, over Tailscale and with the admin token, and every attempt is logged.

Checking access…

Energy Gate — task-level verdicts

After a run, every task is judged against its own main-branch history, and a pull request gets one decision (PASS, WARN, BLOCK, RE-MEASURE, IMPROVED or ACCEPTED). The last column is what the whole-run total alone would have said about the same run: the whole-run (A1) comparison. Click a row for the agents' conversation.

Date Project Runner PR Decision Flagged tasks Extra per run Whole-run view (A1)
Loading...

How the Governance Agent Works

Five kinds of agent, four FIPA protocols, one RabbitMQ broker.

Who talks to whom

The Directory Facilitator, governance and attribution agents run on the VPS; a runner agent runs on each runner machine, where it can see whether its GitHub runner is idle or busy. Agents find each other in the directory (FIPA Request), runners fetch their measured history (Query) and are told of every new attributed run (Subscribe), and jobs are awarded by Contract Net. Messages carry a conversation id, so every decision can be replayed.

When: carbon

From Electricity Maps: a normal or batch job is held while the grid is above the median of the last 14 days, re-checked every 30 minutes, and never held more than 12 hours. Urgent jobs are never held. Timing is the only lever that reduces carbon.

Where: quota and deadline

Each runner bids the share of its daily quota the job would use (the quota is in that runner's own units, by default 4 pipelines a day) and when it would finish. Lowest U = 0.6·quota share + 0.4·time to finish ÷ time to deadline wins. A runner refuses if it has no history, no quota left, cannot meet the deadline, or is offline.

Afterwards: measured result

The winning runner dispatches the project's workflow on its own machine (a PR's own branch for a PR job), waits for the attribution agent to analyse every stage, and reports the measured energy. The energy gate then judges every task against its own main-branch history and gives the PR one verdict, as a GitHub status and comment. A run is atomic, so timing only uses its total; the gate is where request-level data decides.

Predictor Accuracy — Cross-validation grouped by run
Mean Absolute Error — Median error on unseen runs
Model Architecture — Inputs: project, scenario, platform
Anomaly Detector — Robust z-score · false alarms / catch rate on injected 2× regressions

Predictor Comparison

R² on unseen runs (cross-validation grouped by run)

Why the earlier R² ≈ 0.85 was not a real prediction

Stored energy is exactly software_tax_w × duration_s, and both values are only known after a job has run. A model given those inputs rebuilds the answer (R² = 1.0 by plain multiplication) instead of predicting it. The predictors above only use what is known before execution.

Live Energy & Carbon Sandbox

The bid each runner would make for a job, from its measured history

Quick Presets:
Predicted Workload Consumption: —
Predicted Energy
—
—
Predicted Carbon
—
at the grid intensity above
Typical Duration
—
median of past runs
Evaluating cross-platform efficiency...

Live Span Anomaly Scorer

Compare a span's energy with past runs of the same span (flag at robust z ≥ 3.5 and ≥ 1.2× the median)

Status: Not scored yet
Robust z: —
vs Median: —
Verdict: —

Flagged Outlier Spans — recomputed now

Measured spans far above the other runs of the same project, span and platform. Recalculated from the screened history on every load, so this list changes as runs accumulate. For what the agent flagged at the moment it decided, see Energy Regression Alerts.

—
Span / Operation Duration Energy (J) Robust z
Loading anomaly dataset...
Awaiting connection...

Display Settings

  • Timezone:

Database Stats

  • Loading...

Research Team

  • Project: A Multi-Agent Framework for Request-Level Energy Attribution
  • Member 1: M.M.N.D. Seneviratne (TG/2021/1017)
  • Member 2: B.D.D. Devendra (TG/2021/1038)
  • Supervisor: Dr. Chandana Pushpakumara

Governance Live

The agents, the process each one runs in, and every message they send through RabbitMQ.

Governance Live is for team devices

Checking this device…