--- name: regression-check description: Decides whether a change made this project slower, by profiling two revisions the same way and weighing them against each other — build and record each, import both into a running Jeffrey Microscope, then report what moved and by how much. Use when the user asks whether a commit, branch or pull request regressed performance, wants a before-and-after measured rather than argued, or suspects a slowdown appeared somewhere in a range of commits. allowed-tools: mcp__plugin_microscope_jeffrey__* mcp__jeffrey__* argument-hint: "[baseline-ref] [candidate-ref] [workload]" --- # Did this change make it slower Two revisions, the same workload, measured the same way. `compare-jfr` reads two profiles that already exist; this produces them, which is where most of the rigour lives. Requested scope: `$ARGUMENTS` — the baseline, the candidate, and what to run. Defaults: the merge base against `HEAD`, and whatever benchmark the repository makes obvious. Say which you picked before spending the minutes. Tool names below omit the prefix your client puts in front of them — `mcp__plugin_microscope_jeffrey__` in Claude Code with the `microscope` plugin, `mcp__jeffrey__` in Codex or wherever the server is registered by hand as `jeffrey`, `mcp_jeffrey_` in Gemini CLI; the rest of the name is exact camelCase. ## The shape of it ``` 1. Pick the pair, and the workload both can run 2. Record the baseline → recordings_analyzeFile → profileId B 3. Record the candidate → recordings_analyzeFile → profileId C 4. compare_list(C, baselineProfileId=B) → are these two even comparable compare_quality(C, baselineProfileId=B) → sampling, duration and workload evidence 5. compare_movements → what moved, ranked 6. compare_flamegraph → where it moved, in the call tree ``` Track it, because step 2 and 3 are long and it is easy to lose which build is on disk: ``` - [ ] 1. Pair and workload chosen - [ ] 2. Baseline recorded and imported - [ ] 3. Candidate recorded and imported - [ ] 4. Comparability checked - [ ] 5. Movements read - [ ] 6. Verdict ``` ## 1. Pick a pair that can be compared Both revisions must run the **same workload** — a benchmark that only exists on one side measures nothing. When the candidate introduces the benchmark, run it from the candidate against a checkout of the baseline, or say plainly that no before-measurement is possible. Check the working tree is clean before switching revisions, and put it back afterwards. A stash left behind is a worse outcome than an unanswered question. ## 2 and 3. Record both, identically The **profile-run** skill has the recording flags; this skill's contribution is that the two runs must differ in exactly one thing. - Same JVM, same flags, same `settings=profile`, same duration, same machine. - Same warm-up. A cold JIT on one side and a warm one on the other is the commonest false regression. - **Not in parallel.** Two profiled JVMs on one machine contend for CPU, and each records the other's interference as its own slowness. - Prefer alternating repeats — baseline, candidate, baseline, candidate — when the machine is noisy or the difference is expected to be small. A single pair on a laptop with a browser open measures the browser. Name them so the pair survives the session: ``` recordings_analyzeFile(path="/abs/base.jfr", name="baseline ", force=true) recordings_analyzeFile(path="/abs/head.jfr", name="candidate ", force=true) ``` `force=true` on both, because both files were just recorded: a re-run to the same path can match an earlier import by name and size, and then the call returns that old profile — and ignores `name`. Both may come back with a status of `RUNNING` and an `operationId`; poll `operations_status` with it, and `operations_cancel` it if the wrong build was recorded. ## 4. Ask whether they are comparable before reading the difference ``` compare_list(profileId=, baselineProfileId=) compare_quality(profileId=, baselineProfileId=) ``` `compare_list` identifies shared event types and window differences. `compare_quality` returns evidence, not a single comparability verdict: inspect `samplingConfiguration`, `findings`, each side's `samplerHealth`, and `truncation` using the interpretation in `compare-jfr`. Call both before quoting a delta. If no shared evidence supports the requested claim, explain why and re-record or stop that comparison. Matching settings and lengths say the two were *recorded the same way*, not that they did the same work: never write "the runs are comparable" on its own — say which of the two the evidence shows, as `compare-jfr` words it. `durationNormalization.available=true` permits exposure scaling, not an assumption of equal load. `workloadNormalization.available=false` means the recorded server events cannot supply a complete per-operation denominator. You may still compare hotspot distributions and qualified movements; do not turn those alone into a faster/slower-per-request verdict. Use the controlled workload and measured benchmark outcomes from this run to support a regression claim, or report the missing denominator under **Not assessed**. ## 5 and 6. Read what moved, then say what it means `compare_movements` ranks what grew and what shrank; `compare_flamegraph` shows where in the call tree. Use `useWeight: true` when comparing allocation or lock time rather than sample counts. Report, in the `report` skill's shape: - **The verdict first** — slower, faster, indistinguishable, or insufficient evidence — and the measurement or evidence gap it rests on. Missing support for a performance claim is not proof that the runs are indistinguishable. - **Where**, as a frame with its share on each side. - **The uncertainty.** One run each on a shared machine supports "no obvious change"; it does not support "3% slower". A movement smaller than the difference between two runs of the *same* revision is noise, and the honest way to know that is to record the baseline twice. A regression whose cause is visible in the diff is worth naming; **advise-jfr** turns it into a change. A regression with no candidate in the diff is still a finding — report it rather than hunting until something fits. ## Implement fixes You are usually running inside the repository of the application that was profiled, so a finding can become a change to its code. When it can, offer that as an option labelled exactly **Implement fixes** — not "I can propose a change" in passing — beside the follow-ups that close the answer. In Claude Code close with `AskUserQuestion` whenever this option is on offer, a named area's answer included: the follow-ups, **Implement fixes**, and "Stop here". Elsewhere, one line that starts "Implement fixes:". Offer it, never take it: nothing is edited until the user picks it. Offer it only when both hold, or it is noise — and when they do not, leave it out without a word: no "Implement fixes: not on offer", no sentence on why there is nothing to fix. Run both checks before you write the offer: one that says it has not yet checked whether the class is here is not offered. - the working directory is a repository — a `.git` there (`Glob`), or `git rev-parse --show-toplevel` answering; - at least one finding names something that repository holds — the class of the hot frame, the throw site, a leak suspect's class or the field that holds its accumulation point (its `gcRootPath` names the field, or the thread and `stackFrame` that hold it), the configuration file (`logback.xml`, `application.yml`, a pool's settings): `Grep` or `Glob` for it finds a file there, not only JDK, framework or library code. Then say how the code relates to the profile in the option's description: `profiles_get`'s `recordingCommit` against `git rev-parse HEAD` — "profiled at abc1234, HEAD is def5678" when they differ, "commit not recorded" when `recordingCommit` is null (a heap dump carries none). That qualifies the offer; it never removes it. The option's description names what it would change, at most two findings by place (say, logback.xml's caller data, the pricing class's line formatter, or the audit log's unbounded list). When the user asks what else there is, or the analysis winds down, the ledger's **Covered** findings that pass the same check become one multi-select **Implement fixes for:** question. Picked, it hands over to the `advise-jfr` skill with what this conversation already established — the profile id, each finding's frame or setting, its figures and its link — so it starts at reading the source rather than choosing evidence again, and keeps its own gate: the proposed change per finding first, then which to apply, and only the accepted ones are edited. A setting (a JVM flag, a pool size) is proposed as a value to change, not as an edit to code. After the build and the tests, offer **Re-profile and compare** — `profile-run` records the changed code, `compare-jfr` puts it against this profile — so the fix is measured rather than assumed. A heap finding is measured by a second dump taken under the same load and put against this one with `heap_diff`, the earlier dump as the baseline. ## Link what you name Whenever a reply names something Microscope can open — the profile, a flamegraph, an operation, a trace, an endpoint, a statement group, a GC, JIT or thread page, a heap class or object, a differential, a catalogue row, a ledger entry — make the name a Markdown link marked with `↗`: `[trace 7e34c5994dc96208 ↗]()`, and in a table a column of `[↗ open]()`. Every link carries the mark — every profile named too, the analysed one and its baseline alike, and a page named in passing ("the exceptions page"), the links most often left bare: `[checkout candidate ↗]()` — and never the bare URL. The `↗` is what makes it a link the user sees: a terminal that renders links shows the text alone, clickable, with the URL behind it, and an unmarked name reads as plain words. That holds in every reply, not only in a written report: a finding, a ranked list, a follow-up offer, a sentence in passing. **Before you send a reply, check it: every `](` closes a link text that holds `↗`.** - Take the link from the answer the thing came from. A row with its own `uiLink` — a trace, an operation, an exemplar — links with that one; otherwise the answer's `uiLink` is the page that shows the row. Quote the `uiLinkNote` beside it when there is one. - No link in hand for one of the profile's pages (GC, JIT, threads, the heap views, …) → `profiles_viewLink`, one cheap call; the profile itself → `profiles_link`. - Never build, edit or guess a URL, and never fetch one: the link is for the user, and one typed by hand opens the wrong page or none. - An area not yet analysed is not a thing and needs no link, and a result no page shows — a raw SQL query — has none; say so rather than inventing one. ## What this skill will not do - **It will not bisect silently.** A range of commits is many builds and many profiles; propose it, with the cost, and let the user decide. - **It will not declare a regression from one pair of short runs.** It says what it measured and how confident that makes it. Related skills: `profile-run` for the recording flags, `compare-jfr` for reading a pair that already exists, `advise-jfr` for turning a confirmed regression into a fix, `report` for how the verdict and its uncertainty are written down.