--- name: advise-jfr description: Turns a Jeffrey profile into concrete code changes in the current repository — maps the hottest CPU, wall-clock, allocation and blocking frames, the slowest traces, the waiting on locks and I/O, the database and HTTP work, and a heap dump's leak suspects to real source, recommends minimal behaviour-preserving edits, applies them on request and verifies with the tests and a re-profile. Use whenever the user asks what to change, optimise or fix based on a profile, JFR recording, flamegraph or trace, why an endpoint is slow and what to do about it, or a hotspot is known and the question is what to do about it. allowed-tools: mcp__plugin_microscope_jeffrey__* mcp__jeffrey__* Read Grep Glob Bash(git rev-parse *) argument-hint: "[profile-id | recording-file] [cpu|wall|alloc|lock|latency|waiting|memory|heap]" --- # From a profile to a code change The analysis tools read a profile; this skill is what happens after. It takes the measured call trees, finds the code behind the heaviest frames in the checkout you are sitting in, and proposes the smallest change that would reduce the measured cost — then, only when asked, makes it and checks that it helped. Requested scope: `$ARGUMENTS` — substituted by Claude Code; where a client does not substitute it, take the same scope from the request itself. A profile id or a recording file, then optionally one area (`cpu`, `wall`, `alloc`, `lock`, `latency`, `waiting`, `memory`, `heap`). Empty means the profile the conversation is about (or the most recently modified one in `profiles_list`) and whatever evidence it actually carries. 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. Two phases with a stop between them — **recommend**, then **change** — because an edit made before the recommendation has been read cannot be reviewed on its own terms. Track progress: ``` - [ ] 1. Profile resolved, commit compared with HEAD - [ ] 2. Evidence chosen by the user from what the profile carries — asked, not assumed - [ ] 3. Each source read the way its own document says - [ ] 4. Every code finding tied to a frame and to source that was actually read - [ ] 5. Recommendation written, code and configuration findings kept apart — STOP - [ ] 6. Accepted edits made, built, tested, re-profiled where possible ``` **Started from Implement fixes** — another skill's analysis handed over the profile id and its findings, each with its frame or setting, its figures and its link. Resolve the commit (step 1), skip choosing the evidence (step 2: the user already chose it there), and start at reading the source for those findings. The gate in step 5 stays: nothing is edited before the user picks. ## 1. Resolve the profile and its commit Start from `profiles_list`, or `recordings_analyzeFile` when the user named a file (the `analyze-jfr` skill covers that path). Then `profiles_get` and read `recordingCommit`: - equal to `git rev-parse HEAD` — say so in one line and continue; - different — say so **before anything else**, name both commits, and ask whether to check the recording's commit out. Do not switch branches unasked. A profile of another commit describes code that may have moved, been renamed or been deleted; mapping it onto the wrong tree produces confident nonsense. - `null` — say the commit is unknown, not that it matched. (Tagging the recording with `git.commit` at build time fixes that for next time.) When the code being advised on is not this checkout but one open in the reader's IntelliJ, `ide_windows` reports the branch and HEAD commit of every open window and marks which of them is on the commit the recording was built from — the same check, for the tree the reader is actually looking at. ## 2. Choose the evidence **Ask what the profile carries before asking anything else.** One call to `profiles_summary` says which areas this profile can answer at all — its `investigationAreas` lists each area with its `availability`, its `weight` and, when the evidence points there, `suggested` with the `evidence` (a fired rule, the dominant kind of sample). Analysing every family unconditionally is a fishing expedition: it costs a dozen calls and buries the two findings that matter under twelve that do not. **If the user named an area** — in the scope argument or in the request — do that one and skip the menu. **Otherwise ask.** Put the `AVAILABLE` areas to the user the way `analyze-jfr` does — as one `AskUserQuestion` with a multi-select question per group in Claude Code, a table elsewhere — each with its weight and what it would lead to in the code, the suggested ones marked with their evidence, and the `NOT_RECORDED` ones named in one line with their `remedy`. Then **stop** until the user picks. Do not export anything first to make the menu look better informed. The weight is what an area puts into this conversation: **MEDIUM** a short chain of dashboard calls with compact answers, **HEAVY** a Markdown export of tens of thousands of characters per group, which is also what makes step 4 slower. A pick is worked in the order given, sending the area's `nextTools` unchanged; an area nobody picked is not analysed, and belongs under **not assessed** only if the user's question needed it. ### The flamegraph groups — where the time was spent | Group | Event type (first present wins) | Export options | |---|---|---| | `cpu` | `jdk.ExecutionSample`, else `jdk.CPUTimeSample` | defaults | | `wall` | `profiler.WallClockSample` | defaults | | `alloc` | `jdk.ObjectAllocationSample`, else `jdk.ObjectAllocationInNewTLAB`, else `jdk.ObjectAllocationOutsideTLAB` | `useWeight: true` (bytes, not call count) | | `lock` | `jdk.JavaMonitorEnter`, else `jdk.JavaMonitorWait`, else `jdk.ThreadPark` | `useWeight: true` (nanoseconds blocked) | Report a `notRecorded` group rather than analysing it, with the async-profiler option that would capture it next time: `event=ctimer` (cpu), `wall=10ms`, `alloc=512k`, `lock=10ms`. ### Everything else — and how it reaches source A flamegraph is not the only evidence, and for several real problems it is the wrong one: a thread blocked on a lock or a socket is not on-CPU, so it is never sampled and the graph reports the application as idle rather than as waiting. Each row below says what the evidence proves and how it gets you to a line of code, because a finding that cannot reach source is not something this skill can act on. | Area | Start with | How it reaches source | |---|---|---| | `latency` | `traces_overview`, then `traces_operations`; `traces_notifications` first when any is `CRITICAL` or `HIGH` | `traces_slowestTraces` → `traces_traceExport` → `traces_spanFlamegraphExport`, which gives frames for **one span** — continue from step 4 with that export rather than the whole-recording one | | `latency`, per population | `traces_attributeKeys` → `traces_attributeValues` | A value whose p95 stands apart names the population; `traces_attributeSearch` gives its traces, and one of them exports to frames | | `waiting`, locks | `blocking_overview` → `blocking_monitors` | The monitor class names the lock; find the `synchronized` block or `Lock` in the checkout. `blocking_pinnedThreads` points at a `synchronized` block on a carrier thread | | `waiting`, I/O | `io_overview` (`SOCKET`, `FILE`) → `io_endpoints` → `io_slowest` | The target names the dependency or file; the fix is at the **calling** code — fewer round trips, batching, caching, or not waiting | | `memory` | `memory_allocations` | The types ranked here are the other axis from the allocation flamegraph; take a type back to the `alloc` export to find the site | | `heap`, a dump | `heap_getLeakSuspects` | Each suspect carries its class, its `gcRootPath` (the fields from a root, or the thread and `stackFrame` that hold it) and its `accumulationPoint`: the class and field name the holder. Find where that collection is filled and nothing takes from it — the change bounds it, evicts, or lets go of the reference. One dump shows a state, not a trend: say whether the finding is a leak or a large working set, and `heap_diff` against an earlier dump when it matters | | database | `jdbc_overview` → `jdbc_statementGroup` | The statement text names the query; find where it is issued. `jdbc_pools` is a configuration finding, not a code one — see step 5 | | HTTP, gRPC | `http_overview` / `grpc_overview` (`direction`: `SERVER` or `CLIENT`) | An inbound endpoint is your handler; an outbound call is a dependency, and the change is at the call site | | when, not where | `timeline_hotWindows` | Not a finding on its own. It gives `startEpochMs`/`endEpochMs` to scope an export to, which is what makes a burst visible that a whole-recording graph averages away | Two that shape a recommendation without being one: `profiles_samplerHealth`, because a recording that dropped a large share of its samples understates its own hot paths and every percentage below inherits that; and `jvm_flags` plus `jvm_container`, because a tuning claim is only worth making against the values the JVM really ran with. ## 3. Read each source the way it asks to be read One export per flamegraph group the user picked, whole recording, default threshold. Every export opens with its own reading instructions and an analysis section written for that event type — what counts as a hotspot, what the frame tags mean, what to skip. That document governs, not generic flamegraph lore. Lower `thresholdPct` only to chase one specific path deeper. One picked group: call `flamegraph_export` here, so the document is in front of you for step 4 and for the user's follow-up questions. Several picked groups: send each to a **`profile-analyst`** agent — `microscope:profile-analyst` from the Claude Code plugin, or the Codex custom agent from `codex/agents/profile-analyst.toml` — one delegation per group, all in a single message so they run at once. Each returns the hot frames with their shares; several raw exports would otherwise crowd out the source reading that step 4 depends on. Without an agent, export the picked groups here one after another. A group the user did not pick is not exported, and is not delegated either. When the recommendation is written, offer at most two further areas the evidence points at — each with its tag — rather than analysing them. ## 4. Ground every finding in source The export has call paths and numbers; a frame carries a line only when every sample at it agreed on one, so treat a printed line as a place to start reading and its absence as no information. Map the heaviest frames to the checkout with Read, Grep and Glob — or, when the reader has IntelliJ open with the Jeffrey plugin, ask it: `ide_resolve` returns the file and line for a class and method and flags a position that is decompiled, imprecise, or in a file edited since the recording. It is worth a call for anything a grep would get wrong — a nested class, a Kotlin facade, an inherited or overloaded method — and for library code, where `ide_source` reads what is not in this repository at all. It does not move the reader's editor; `ide_open` does, so save that for when they ask to be shown something. Hold to these rules — they are what separates a recommendation from a guess: - **Never name a file, method or line you have not read.** Open it first. - **Tie each finding to a frame and its share** (`total`, `self`, the percentage) from the export, so the reader can check the claim against the profile. - **Prefer a few high-impact findings** over many speculative ones. Frames under 1 % are noise unless the user is chasing something specific. - **Say when a hotspot cannot be located** — a library you cannot patch, generated code, a method that no longer exists at this commit. Note it once and move to the next frame in this repository. - Distinguish leaf work (`self ≈ total`) from orchestration (`self << total`): recommend changes to the former, walk into the latter. ## 5. Recommend, then stop Write the recommendation in this shape — it is the `report` skill's shape, with the code findings carrying the recommendation the analysis stage left out: - **Summary** — what dominates, across every source you read, in two or three sentences. Say which evidence you used and which you skipped because the profile does not carry it — the `capabilityGaps` from `profiles_summary` that touched the question belong here, as **not assessed**, never as a negative result. - **Code findings.** One **`### : `** section each: the cause, why it is hot according to the profile (the frame and its share, the operation and its p95, the monitor and its blocked time — whichever measured it), and the proposed change in prose — minimal, behaviour-preserving, reviewable on its own. No diffs in this phase. - **Configuration findings**, kept separate and labelled as such. A pool that ran out of connections, a JVM flag left at an ergonomic default, a container quota the scheduler enforced — these are real and often the largest single win, and none of them is a code change. Presenting one as an edit would misrepresent both the fix and the risk. Give the setting, its current value, the evidence, and what to change it to. - **Not located** — hotspots that could not be mapped to this repository, if any. - **Confidence per finding**, with what caps it: sampled data, a lossy sampler (`profiles_samplerHealth`), a rule that fired rather than a figure that was measured. Then ask which findings to apply, and wait for the answer. This is the gate. In Claude Code ask with `AskUserQuestion`, one multi-select option per finding (its one-line title, the edit or setting it leads to in the description), when there are at most four; with more, list them numbered and let the user name the ones to apply. A finding that reaches neither source nor a setting is not a finding yet. Say what it measured and what would be needed to act on it, rather than inventing a change to attach to it. ## 6. Change and verify For each accepted finding: 1. Make the smallest edit that implements it; one reviewable change beats a sweeping rewrite. 2. Run the project's build and tests the way a contributor would. 3. If the recording can be reproduced (a benchmark, a load script, a command the user names), run it, `recordings_analyzeFile` the new recording, and export the **same group with the same parameters** — keep threshold and options identical, or a difference in pruning will read as a change that is not there. Report the delta on the frames you changed. 4. Never claim a saving you did not measure. Without a re-profile, the estimate is capped at the frame's own `total` share: a change cannot save more time than the frame used. 5. With no reproduction at hand, offer **Re-profile and compare** rather than skipping the measurement: the `profile-run` skill records the changed code, and `compare-jfr` puts the new profile against the one the finding came from. 6. A heap finding is verified by a second dump taken under the same load after the change, put against the one the finding came from with `heap_diff` (the earlier dump as the baseline): the suspect's class should stop growing. A build and green tests do not show that. 7. A configuration finding is verified differently, and usually not by you: a pool size or a flag takes effect on the next run of the application, not on the next test. Say what to watch for in that run — the timeout count back to zero, the throttling gone — rather than reporting it fixed. ## Latency rather than throughput The order matters here more than anywhere else, because the wrong first question wastes the whole investigation. A `CRITICAL` or `HIGH` notification is the application's own account of what went wrong and comes before any frame: `traces_notifications` usually names the cause a span tree only shows the cost of. Then the population before the exemplar — `traces_operations` for where the wall-clock went, or `traces_attributeValues` when one tenant or customer is slow and the rest is fine — and only then one trace, its span tree, and the frames inside the slow span. Two traps worth naming. A request that is slow while every statement is fast is waiting for a connection: that is `jdbc_pools`, it appears nowhere else, and no change to a query will fix it. And an outbound call that is slow belongs to a dependency — the local change is to call it less often, batch it, cache it or stop waiting for it, not to optimise code you do not own. ## 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. ## When something is missing - `flamegraph_list` reports no graphable event types → a heap dump, or a recording without samples; there is nothing to advise on from a flamegraph. For a heap dump, the `analyze-heap` skill applies. - The profile's commit differs from `HEAD` and the user does not want to switch → analyse anyway, and say in the summary that every file reference was checked against a different commit than the one profiled. - The code behind the top frame is in a dependency → say so, name the calling frame in this repository, and advise there (fewer calls, a cheaper API, caching) rather than inside the library.