|
| 1 | +--- |
| 2 | +name: perf-review |
| 3 | +description: >- |
| 4 | + Performance-overhead review of a code diff / branch / PR for the dd-trace-java |
| 5 | + tracer. Flags hot-path allocation, unbounded memory, repeated work, escaping |
| 6 | + objects, native-boundary crossings, and JVM-specific pitfalls (escape analysis, |
| 7 | + JNI / virtual-thread pinning, backtracking-regex ReDoS, varargs/boxing hashing, |
| 8 | + String.format, ByteBuddy-Advice anti-patterns) using the tracer performance |
| 9 | + rubric. Use whenever the user wants a performance / overhead / hot-path review, |
| 10 | + asks to check a diff or PR for allocation / GC / memory / latency / startup cost, |
| 11 | + or mentions the "perf rubric" or the "do no harm / assume hot" tracer posture — |
| 12 | + even if they just say "review this for perf" without naming the rubric. Advisory and READ-ONLY: it reports ranked, |
| 13 | + verify-first findings; it never edits code. |
| 14 | +user-invocable: true |
| 15 | +context: fork |
| 16 | +allowed-tools: |
| 17 | + - Bash |
| 18 | + - Read |
| 19 | + - Grep |
| 20 | + - Glob |
| 21 | +--- |
| 22 | + |
| 23 | +# Performance Review |
| 24 | + |
| 25 | +Review the current branch's changes for performance overhead in the dd-trace-java |
| 26 | +tracer, using the tracer performance rubric bundled in `references/`. This is a |
| 27 | +**low-friction advisory nudge**, not a gate: it reports findings and stops. It |
| 28 | +never edits code. |
| 29 | + |
| 30 | +## Why this exists (read first — it sets the whole posture) |
| 31 | + |
| 32 | +The tracer shares the customer's process, heap, and latency budget. **Do no harm**: |
| 33 | +overhead is a form of incorrect behavior that can escalate to real customer harm — |
| 34 | +missed SLAs, OOM kills, container restarts, cold-start churn. So the review's job is |
| 35 | +to catch overhead the customer would feel, and to do it *without becoming noise*. |
| 36 | + |
| 37 | +Two forces are in tension, and the resolution defines everything below: |
| 38 | + |
| 39 | +- **Assume hot.** We don't know what's on a customer's critical path. Absent positive |
| 40 | + evidence of cold, assume the code runs on every request, under load, at full |
| 41 | + concurrency. The burden of proof runs toward *cold*: ask "is there evidence this is |
| 42 | + cold or guarded?" — not "is there evidence this is hot?" (that rationalizes itself |
| 43 | + into "probably not"). |
| 44 | +- **Precision over recall — be silent when unsure.** A false-positive-prone review |
| 45 | + dies of being ignored. Over-flagging kills it faster than under-flagging. This |
| 46 | + actively fights your default to be comprehensive and helpful: here, *not* flagging |
| 47 | + a borderline case is the correct, skilled move — not a miss. |
| 48 | + |
| 49 | +You reconcile them with the **confidence axis** and **verify-don't-verdict** (below): |
| 50 | +assume-hot makes you *look* everywhere; precision makes you *speak* only when the |
| 51 | +mechanism is certain or the severity is catastrophic. |
| 52 | + |
| 53 | +## Core rules |
| 54 | + |
| 55 | +- **Findings are prompts to *verify*, not verdicts.** You reason statically; you |
| 56 | + cannot render a performance verdict from a code read. Every finding routes into |
| 57 | + **Benchmark → Profile → Improve → Guard**. Phrase each as *"this looks like X; |
| 58 | + verify with Y"* — never "this is slow." |
| 59 | +- **Confidence axis on every finding:** |
| 60 | + - **flag-with-confidence** — the cost is *mechanism-determined* and visible in the |
| 61 | + code: allocation, boxing, copying, unbounded growth, a native crossing. State it |
| 62 | + plainly. |
| 63 | + - **flag-as-measure** — the cost depends on JIT/GC/optimizer decisions you can't |
| 64 | + see from source: escape elision, inlining/devirtualization, GC impact. Phrase as |
| 65 | + "may X; verify with a profiler/benchmark," never as a certainty. |
| 66 | +- **Predicate-with-default, not a banned-API list.** Don't flag "you called |
| 67 | + `String.format`." Flag *"an eager, unconditional expensive call on a hot, |
| 68 | + instrumentation-reachable path."* The same API is fine on a cold path. Two failure |
| 69 | + shapes, different fixes: result usually **discarded** → gate/defer; result always |
| 70 | + **needed but costly** → cheapen/cache. |
| 71 | +- **Resolve interprocedurally — this is the review's whole reason to exist.** A |
| 72 | + peephole lint can't answer "reachable from a hot entry, unconditional along the |
| 73 | + way." Trace *up* (who calls this? is it reachable from an `@Advice` root / per-span |
| 74 | + callback / request handler?) and *down* (follow callbacks, hooks, and listeners to |
| 75 | + their **sink** before flagging). If a per-span hook's every reachable sink is an |
| 76 | + atomic counter (`LongAdder`, `AtomicLong`) or a no-op-when-disabled, stay silent — a |
| 77 | + "verify contention" nudge there is noise. |
| 78 | +- **Make the reachability path the headline.** The reachability claim is the most |
| 79 | + valuable *and* least reliable part of a finding — residual false positives cluster |
| 80 | + in "called it unconditional, missed an upstream guard." Say *"reachable from |
| 81 | + `Foo.onEnter` via A→B→C, no guard on that path"* so the reader can check the |
| 82 | + shakiest link at a glance. |
| 83 | +- **Only flag toward a fix that exists.** A finding must be actionable *now*. Route to |
| 84 | + a mechanism that has landed (see the toolkit note in `checks.md` — cite only what |
| 85 | + exists; name "coming" primitives as coming). Don't flag a pattern whose only fix is |
| 86 | + a mechanism that isn't built yet. |
| 87 | +- **Triage by severity.** Flag SEV-1 (unbounded memory / OOM, cardinality blowups) |
| 88 | + *aggressively* — a false positive there is cheap insurance against a container kill. |
| 89 | + Flag low-severity CPU-micro *conservatively or not at all* — false positives there |
| 90 | + only erode trust. |
| 91 | +- **Never flag the *absence* of a cache on high-cardinality input.** For open-cardinality |
| 92 | + data (raw SQL with literals, per-request strings), *not* caching is the correct |
| 93 | + choice — caching it would be the worse SEV-1. Flag a cache *keyed by* high-cardinality |
| 94 | + data; never flag the decision not to cache. |
| 95 | +- **A *visibly contestable* perf tradeoff shipped without data → one soft flag-as-measure.** |
| 96 | + The trigger is narrow: the change makes a **visible tradeoff that could itself regress** — |
| 97 | + it removes a lock / guard / synchronization, swaps in a hand-rolled cache or data |
| 98 | + structure, or explicitly claims "faster / optimized" — **and** ships no benchmark or |
| 99 | + profile. There a static read genuinely can't tell a win from a regression, so raise one |
| 100 | + soft *flag-as-measure* nudge: *"this trades <X for Y>; verify with a JMH benchmark / JFR."* |
| 101 | + Do **not** fire it otherwise — if nothing in the diff could plausibly regress, there is |
| 102 | + nothing to measure, so stay silent. Specifically not for: a **mechanically-obvious win** |
| 103 | + (hoisting an invariant out of a loop, a denser data structure, removing an allocation); |
| 104 | + **routine adoption of a known-better idiom** (migrating to a lower-overhead builder / API / |
| 105 | + toolkit primitive — no visible downside); or a change that **ships a benchmark/JFR** |
| 106 | + (well-evidenced — recognize it). One line; a nudge, not a code-pattern finding. |
| 107 | + |
| 108 | +## Workflow |
| 109 | + |
| 110 | +### Step 1 — Get the code to review |
| 111 | + |
| 112 | +**If the user points you at specific files or pasted code** ("review this class / this |
| 113 | +method for perf"), review those directly — skip the diff and go to Step 2 with the same |
| 114 | +hot-path mapping and checks. |
| 115 | + |
| 116 | +**Otherwise, review the branch changes.** Find the merge-base against the DataDog |
| 117 | +upstream `master` and diff against it: |
| 118 | + |
| 119 | +```bash |
| 120 | +UPSTREAM=$(git remote -v | grep -E 'DataDog/[^/]+(\.git)?\s' | head -1 | awk '{print $1}') |
| 121 | +[ -z "$UPSTREAM" ] && UPSTREAM="origin" |
| 122 | +MERGE_BASE=$(git merge-base HEAD ${UPSTREAM}/master) |
| 123 | +echo "Reviewing changes since $MERGE_BASE" |
| 124 | +git diff $MERGE_BASE --stat |
| 125 | +git diff $MERGE_BASE --name-status |
| 126 | +``` |
| 127 | + |
| 128 | +If there are no changes, say so and stop. Otherwise read the diff **and the full |
| 129 | +content of the modified source files** (not just the hunks) — the interprocedural |
| 130 | +condition (who calls this, what a helper does, where a hook's sink lands) lives |
| 131 | +outside the diff window. Ignore the PR description if the user asks for an |
| 132 | +independent review. |
| 133 | + |
| 134 | +### Step 2 — Map the changed code onto hot paths |
| 135 | + |
| 136 | +For each changed method, decide *which multiplier applies* before flagging anything. |
| 137 | + |
| 138 | +**Hot anchors** (reachable ⇒ assume hot): `@Advice.OnMethodEnter`/`OnMethodExit`, |
| 139 | +per-span / per-trace callbacks, request / message handlers, streaming chunk handlers. |
| 140 | +**Hot-path map** (where cost is multiplied per-span × spans/request × requests/sec): |
| 141 | +span lifecycle (create / setTag / finish), tag-map ops, serialization/encoding, the |
| 142 | +metrics/stats path, decorators, propagation (header read/write). |
| 143 | + |
| 144 | +**Cold only with positive evidence:** one-time init, startup-only path, a genuinely |
| 145 | +rare error branch, or behind a guard that provably fires rarely. Watch the |
| 146 | +**interprocedural trap** — a method three helpers deep from an `@Advice` entry is |
| 147 | +still hot. And note **domain adjustment**: large-denominator domains (LLMObs, CI |
| 148 | +Visibility, DSM) absorb per-call CPU/alloc cost, but the risk *inverts* to payload |
| 149 | +memory (SEV-1); streaming handlers fire per-chunk, so the large-denominator relief |
| 150 | +suspends inside them. See `guide.md` §6. |
| 151 | + |
| 152 | +### Step 3 — Apply the checks |
| 153 | + |
| 154 | +Run the changed hot-path code against the rubric. Keep the check index below in mind; |
| 155 | +open the references for the precise conditions, confidence, severity, and fix: |
| 156 | + |
| 157 | +- **`references/guide.md`** — the narrative "how": severity model, hotness rubric, |
| 158 | + the 6 categories with worked examples, and the false-positive traps. Read this first |
| 159 | + if you're calibrating judgment. |
| 160 | +- **`references/checks.md`** — the precise cost-model: 7 universal checks + the Java |
| 161 | + addendum (J1–J11) + the ByteBuddy-Advice fix idioms + the toolkit-availability note. |
| 162 | + Read this for the exact confidence/severity/fix of a specific pattern. |
| 163 | + |
| 164 | +### Step 4 — Resolve, then emit |
| 165 | + |
| 166 | +Before writing a finding: confirm the reachability path, confirm it's unconditional |
| 167 | +along that path (check for upstream guards), and follow any hook/callback to its sink. |
| 168 | +Drop anything that resolves to benign. Then report in the format below. |
| 169 | + |
| 170 | +**How many findings to report — scale with diff size:** |
| 171 | +- **Small, focused diff** (one method, a handful of files): report *every* genuinely |
| 172 | + high-confidence finding, ranked by severity. A tight diff with four real allocation |
| 173 | + smells should list all four (as the worked example does). |
| 174 | +- **Large PR:** lead with the 1–3 highest-severity findings and note that lower-severity |
| 175 | + ones may exist — don't bury the important one under a wall of CPU-micro nits. |
| 176 | +- Either way, the gate is *confidence*, not a count: silence on the uncertain ones is |
| 177 | + what earns the review its credibility. |
| 178 | + |
| 179 | +## Output format |
| 180 | + |
| 181 | +Follow this structure (see `references/example-review.md` for a full worked instance — |
| 182 | +). Showing your suppressed lookalikes and what you cleared is not |
| 183 | +filler: it demonstrates the precision that makes the findings trustworthy. |
| 184 | + |
| 185 | +When providing suggestions as code review comments, prefix the comments with "perf: " |
| 186 | +```markdown |
| 187 | +# Perf Review — <branch / PR> |
| 188 | + |
| 189 | +**Scope reviewed:** <the hot method(s) and why they're hot — the multiplier> |
| 190 | + |
| 191 | +## Confirmed findings |
| 192 | + |
| 193 | +### 1. <one-line title> |
| 194 | +<the offending code, as a short fenced snippet> |
| 195 | +- **Confidence:** flag-with-confidence | flag-as-measure |
| 196 | +- **Reachability:** <hot from X via A→B→C, no guard on that path> |
| 197 | +- **Rubric check:** <#N / JN> |
| 198 | +- **Severity:** SEV-<n> |
| 199 | +- **Fix / verify-with:** <the actionable fix, or "verify with an allocation profiler"> |
| 200 | + |
| 201 | +## Correctly suppressed (not flagged) |
| 202 | +<textual lookalikes deliberately left silent — e.g. the same `Objects.hash` pattern |
| 203 | +but at class-init (cold), not per-call — and why the posture suppresses them> |
| 204 | + |
| 205 | +## Checked, no issue |
| 206 | +<what you examined and cleared: e.g. "no unbounded cache (#3/J5)", "no native |
| 207 | +crossing (#6/J3)", "string-literal tag keys are JVM-interned — no per-call alloc"> |
| 208 | + |
| 209 | +## Summary |
| 210 | +<count + severity spread; e.g. "4 confirmed hot-path findings, all SEV-2/3 |
| 211 | +(allocation/CPU); 1 cold-path lookalike correctly suppressed"> |
| 212 | +``` |
| 213 | + |
| 214 | +If nothing survives the confidence bar, say so plainly — "No high-confidence hot-path |
| 215 | +findings; here's what I checked and cleared." A clean review is a valid, valuable |
| 216 | +result, not a failure to find something. |
| 217 | + |
| 218 | +## Check index (the map — details in the references) |
| 219 | + |
| 220 | +**Universal (language-agnostic):** |
| 221 | +1. Per-span/per-call allocation on a hot path (retained/escaping) — SEV-2/3 |
| 222 | +2. Repeat work across calls (regex compile / format / parse / concat recomputed) — SEV-2/3 |
| 223 | +3. Unbounded memory / collection, or keyed by high-cardinality input — **SEV-1** |
| 224 | +4. Expensive work on the critical path that could be deferred — SEV-1/2 |
| 225 | +5. Polymorphic dispatch defeating inlining/devirt — flag-as-measure — SEV-2/3 |
| 226 | +6. FFI / native-boundary crossing per-item (not batched) — SEV-1/2 |
| 227 | +7. Escape / allocation-elision defeated by a refactor — flag-as-measure — SEV-2/3 |
| 228 | + |
| 229 | +**Java addendum (JVM-specific — full text + mechanism in `checks.md`):** |
| 230 | +- **J1** escaping allocation defeats Escape Analysis · **J3** JNI crossing + virtual-thread |
| 231 | + pinning · **J4** GC pressure → tail latency · **J5** cardinality-sensitive aggregator |
| 232 | + (**SEV-1**) · **J6** `WeakReference.get()` in a probe loop strengthens the ref · |
| 233 | + **J7** `substring` → `SubSequence` zero-copy view · **J8** backtracking regex on |
| 234 | + external input → RE2J (ReDoS) · **J9** `Objects.hash(...)` varargs/boxing → |
| 235 | + `HashingUtils` · **J10** hot-path `String.format` → `Strings` · **J11** composite-key |
| 236 | + maps → `Hashtable`. |
| 237 | +- **J2** megamorphic dispatch is **PARKED** — do *not* raise megamorphism findings in |
| 238 | + review yet (kept as author reference only; it needs a standing audit, not per-PR |
| 239 | + flagging). See `checks.md` for why. |
| 240 | +- ByteBuddy-Advice idioms (`Config.get()` hoisting, `@Advice.AllArguments` → |
| 241 | + `@Advice.Argument`, `@Advice.SkipOn`+cached-boolean, `@Advice.Local`, `switch(String)` |
| 242 | + three-tier) — in `checks.md`. |
| 243 | + |
| 244 | +J7–J11 route an *existing* #1/#2/#3 finding to a landed reusable fix — they are not new |
| 245 | +triggers. Don't raise a finding you wouldn't have raised anyway. |
0 commit comments