Skip to content

Commit f4a2a07

Browse files
authored
Merge branch 'master' into dougqh/tagset
2 parents 8db94b3 + eaabb65 commit f4a2a07

145 files changed

Lines changed: 3434 additions & 514 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
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.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Local rubric-maintenance lab file — the delta-since-last-roll-up changelog.
2+
# Kept local (not shipped): folded into the local rubric + rolled up to the published copies periodically.
3+
unpublished.md

0 commit comments

Comments
 (0)