datadog/perf-review
>- Performance-overhead review of a code diff / branch / PR for the dd-trace-java tracer. Flags hot-path allocation, unbounded memory, repeated work, escaping objects, native-boundary crossings, and JVM-specific pitfalls (escape analysis, JNI / virtual-thread pinning, backtracking-regex ReDoS, varargs/boxing hashing, String.format, ByteBuddy-Advice anti-patterns) using the tracer performance rubric. Use whenever the user wants a performance / overhead / hot-path review, asks to check a diff or PR for allocation / GC / memory / latency / startup cost, or mentions the "perf rubric" or the "do no harm / assume hot" tracer posture — verify-first findings; it never edits code.
npx skills add https://github.com/DataDog/dd-trace-java --skill perf-review
Review the current branch's changes for performance overhead in the dd-trace-java
tracer, using the tracer performance rubric bundled in references/. This is a
low-friction advisory nudge, not a gate: it reports findings and stops. It
never edits code.
The tracer shares the customer's process, heap, and latency budget. Do no harm:
overhead is a form of incorrect behavior that can escalate to real customer harm —
missed SLAs, OOM kills, container restarts, cold-start churn. So the review's job is
to catch overhead the customer would feel, and to do it *without becoming noise*.
Two forces are in tension, and the resolution defines everything below:
evidence of cold, assume the code runs on every request, under load, at full
concurrency. The burden of proof runs toward *cold*: ask "is there evidence this is
cold or guarded?" — not "is there evidence this is hot?" (that rationalizes itself
into "probably not").
dies of being ignored. Over-flagging kills it faster than under-flagging. This
actively fights your default to be comprehensive and helpful: here, *not* flagging
a borderline case is the correct, skilled move — not a miss.
You reconcile them with the confidence axis and verify-don't-verdict (below):
assume-hot makes you *look* everywhere; precision makes you *speak* only when the
mechanism is certain or the severity is catastrophic.
cannot render a performance verdict from a code read. Every finding routes into
Benchmark → Profile → Improve → Guard. Phrase each as *"this looks like X;
verify with Y"* — never "this is slow."
code: allocation, boxing, copying, unbounded growth, a native crossing. State it
plainly.
see from source: escape elision, inlining/devirtualization, GC impact. Phrase as
"may X; verify with a profiler/benchmark," never as a certainty.
String.format." Flag *"an eager, unconditional expensive call on a hot,
instrumentation-reachable path."* The same API is fine on a cold path. Two failure
shapes, different fixes: result usually discarded → gate/defer; result always
needed but costly → cheapen/cache.
peephole lint can't answer "reachable from a hot entry, unconditional along the
way." Trace *up* (who calls this? is it reachable from an @Advice root / per-span
callback / request handler?) and *down* (follow callbacks, hooks, and listeners to
their sink before flagging). If a per-span hook's every reachable sink is an
atomic counter (LongAdder, AtomicLong) or a no-op-when-disabled, stay silent — a
"verify contention" nudge there is noise.
valuable *and* least reliable part of a finding — residual false positives cluster
in "called it unconditional, missed an upstream guard." Say *"reachable from
Foo.onEnter via A→B→C, no guard on that path"* so the reader can check the
shakiest link at a glance.
a mechanism that has landed (see the toolkit note in checks.md — cite only what
exists; name "coming" primitives as coming). Don't flag a pattern whose only fix is
a mechanism that isn't built yet.
*aggressively* — a false positive there is cheap insurance against a container kill.
Flag low-severity CPU-micro *conservatively or not at all* — false positives there
only erode trust.
data (raw SQL with literals, per-request strings), *not* caching is the correct
choice — caching it would be the worse SEV-1. Flag a cache *keyed by* high-cardinality
data; never flag the decision not to cache.
The trigger is narrow: the change makes a visible tradeoff that could itself regress —
it removes a lock / guard / synchronization, swaps in a hand-rolled cache or data
structure, or explicitly claims "faster / optimized" — and ships no benchmark or
profile. There a static read genuinely can't tell a win from a regression, so raise one
soft *flag-as-measure* nudge: *"this trades <X for Y>; verify with a JMH benchmark / JFR."*
Do not fire it otherwise — if nothing in the diff could plausibly regress, there is
nothing to measure, so stay silent. Specifically not for: a mechanically-obvious win
(hoisting an invariant out of a loop, a denser data structure, removing an allocation);
routine adoption of a known-better idiom (migrating to a lower-overhead builder / API /
toolkit primitive — no visible downside); or a change that ships a benchmark/JFR
(well-evidenced — recognize it). One line; a nudge, not a code-pattern finding.
If the user points you at specific files or pasted code ("review this class / this
method for perf"), review those directly — skip the diff and go to Step 2 with the same
hot-path mapping and checks.
Otherwise, review the branch changes. Find the merge-base against the DataDog
upstream master and diff against it:
UPSTREAM=$(git remote -v | grep -E 'DataDog/[^/]+(\.git)?\s' | head -1 | awk '{print $1}')
[ -z "$UPSTREAM" ] && UPSTREAM="origin"
MERGE_BASE=$(git merge-base HEAD ${UPSTREAM}/master)
echo "Reviewing changes since $MERGE_BASE"
git diff $MERGE_BASE --stat
git diff $MERGE_BASE --name-status
If there are no changes, say so and stop. Otherwise read the diff **and the full
content of the modified source files** (not just the hunks) — the interprocedural
condition (who calls this, what a helper does, where a hook's sink lands) lives
outside the diff window. Ignore the PR description if the user asks for an
independent review.
For each changed method, decide *which multiplier applies* before flagging anything.
Hot anchors (reachable ⇒ assume hot): @Advice.OnMethodEnter/OnMethodExit,
per-span / per-trace callbacks, request / message handlers, streaming chunk handlers.
Hot-path map (where cost is multiplied per-span × spans/request × requests/sec):
span lifecycle (create / setTag / finish), tag-map ops, serialization/encoding, the
metrics/stats path, decorators, propagation (header read/write).
Cold only with positive evidence: one-time init, startup-only path, a genuinely
rare error branch, or behind a guard that provably fires rarely. Watch the
interprocedural trap — a method three helpers deep from an @Advice entry is
still hot. And note domain adjustment: large-denominator domains (LLMObs, CI
Visibility, DSM) absorb per-call CPU/alloc cost, but the risk *inverts* to payload
memory (SEV-1); streaming handlers fire per-chunk, so the large-denominator relief
suspends inside them. See guide.md §6.
Run the changed hot-path code against the rubric. Keep the check index below in mind;
open the references for the precise conditions, confidence, severity, and fix:
references/guide.md — the narrative "how": severity model, hotness rubric,the 6 categories with worked examples, and the false-positive traps. Read this first
if you're calibrating judgment.
references/checks.md — the precise cost-model: 7 universal checks + the Javaaddendum (J1–J11) + the ByteBuddy-Advice fix idioms + the toolkit-availability note.
Read this for the exact confidence/severity/fix of a specific pattern.
Before writing a finding: confirm the reachability path, confirm it's unconditional
along that path (check for upstream guards), and follow any hook/callback to its sink.
Drop anything that resolves to benign. Then report in the format below.
How many findings to report — scale with diff size:
high-confidence finding, ranked by severity. A tight diff with four real allocation
smells should list all four (as the worked example does).
ones may exist — don't bury the important one under a wall of CPU-micro nits.
what earns the review its credibility.
Follow this structure (see references/example-review.md for a full worked instance —
). Showing your suppressed lookalikes and what you cleared is not
filler: it demonstrates the precision that makes the findings trustworthy.
When providing suggestions as code review comments, prefix the comments with "perf: "
# Perf Review — <branch / PR>
**Scope reviewed:** <the hot method(s) and why they're hot — the multiplier>
## Confirmed findings
### 1. <one-line title>
<the offending code, as a short fenced snippet>
- **Confidence:** flag-with-confidence | flag-as-measure
- **Reachability:** <hot from X via A→B→C, no guard on that path>
- **Rubric check:** <#N / JN>
- **Severity:** SEV-<n>
- **Fix / verify-with:** <the actionable fix, or "verify with an allocation profiler">
## Correctly suppressed (not flagged)
<textual lookalikes deliberately left silent — e.g. the same `Objects.hash` pattern
but at class-init (cold), not per-call — and why the posture suppresses them>
## Checked, no issue
<what you examined and cleared: e.g. "no unbounded cache (#3/J5)", "no native
crossing (#6/J3)", "string-literal tag keys are JVM-interned — no per-call alloc">
## Summary
<count + severity spread; e.g. "4 confirmed hot-path findings, all SEV-2/3
(allocation/CPU); 1 cold-path lookalike correctly suppressed">
If nothing survives the confidence bar, say so plainly — "No high-confidence hot-path
findings; here's what I checked and cleared." A clean review is a valid, valuable
result, not a failure to find something.
Universal (language-agnostic):
Java addendum (JVM-specific — full text + mechanism in checks.md):
pinning · J4 GC pressure → tail latency · J5 cardinality-sensitive aggregator
(SEV-1) · J6 WeakReference.get() in a probe loop strengthens the ref ·
J7 substring → SubSequence zero-copy view · J8 backtracking regex on
external input → RE2J (ReDoS) · J9 Objects.hash(...) varargs/boxing →
HashingUtils · J10 hot-path String.format → Strings · J11 composite-key
maps → Hashtable.
review yet (kept as author reference only; it needs a standing audit, not per-PR
flagging). See checks.md for why.
Config.get() hoisting, @Advice.AllArguments →@Advice.Argument, @Advice.SkipOn+cached-boolean, @Advice.Local, switch(String)
three-tier) — in checks.md.
J7–J11 route an *existing* #1/#2/#3 finding to a landed reusable fix — they are not new
triggers. Don't raise a finding you wouldn't have raised anyway.
Take datadog/perf-review from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.