Skip to content

Commit 2ce7d24

Browse files
authored
cairn: install distilled knowledgebase from dd-backend-check investigations (#133)
1 parent a4d9345 commit 2ce7d24

39 files changed

Lines changed: 1479 additions & 0 deletions

‎.knowledgebase/.cairn‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"generator": "cairn", "retired_threshold": 10}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
---
2+
id: dead-bitstate-lazy-fastconsume
3+
title: BitState lazy char-class loop fast-consume measured flat on the real corpus — rolled back; lazy-loop consumption is not the lazy families' cost center
4+
kind: dead-end
5+
tags: [bitstate, lazy-loop, perf, measured-negative, rollback]
6+
applies_to: [reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/BitStateMatcher.java]
7+
source: backend-ready/dead-bitstate-lazy-fastconsume
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
# BitState lazy char-class loop fast-consume measured flat on the real corpus — rolled back
15+
16+
## What was tried
17+
A lazy mirror of the existing greedy fast-consume path in BitStateMatcher: a lazy
18+
single-char-class loop (`c*?`) whose continuation closure contains no anchor, no accepting
19+
state, and at least one consuming transition gets a tight first pass — consume the whole
20+
`c`-run in a scan, mark the same visited cells, push exit jobs only at positions where the
21+
continuation's first-char set can match input[p] (descending push = ascending pop = Perl
22+
lazy priority). Fully implemented and gate-verified (fuzz seeds, real-corpus parity probes
23+
0 divergences, full suite).
24+
25+
## Why it was rolled back
26+
- Box JMH matched sweep: flat to slightly negative vs the pre-change build; controls flat.
27+
- Per-pattern C2-converged profiling showed the path barely engages on the corpus lazy
28+
families: with an all-optional tail (`\s*((?<function>[^@]*)@)?(?<file>.*?)(:?\d+)?...`)
29+
the lazy loop exits EMPTY almost immediately (the zero-width guard correctly disables the
30+
fast path) — its cost is unanchored-seed DFS overhead + greedy give-back, not loop
31+
consumption. Where it does engage (narrow continuation first-set), loop consumption is a
32+
small share of the pattern's total cost.
33+
- Local single-JVM probe "wins" were noise. Rule reinforced: **local single-JVM probe
34+
deltas are not perf evidence — box JMH + per-pattern C2-converged profiling only.**
35+
36+
## What this rules out / redirects
37+
- Lazy-loop stack round-trips are NOT the lazy families' cost center.
38+
- The real lever: a priority-correct (lazy leftmost-first) AND fast captureless DFA find in
39+
the LazyDFA/RD lane — the chain lane proves leftmost-first lazy is achievable for simple
40+
shapes; the fast-consume design above is the template if revisited (greedy fast-consume
41+
fields + ctor shape detection in BitStateMatcher).
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
id: dead-classwriter-subclass
3+
title: ASM ClassWriter.visit/visitMethod are final (9.10) — method emission cannot be intercepted via subclassing; wrap with a ClassVisitor instead
4+
kind: dead-end
5+
tags: [asm, classwriter, final, refuted-approach]
6+
applies_to: []
7+
source: multi-64k/dead-classwriter-subclass
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
15+
# ClassWriter subclassing is impossible — wrap a ClassVisitor
16+
17+
First design: `SplittingClassWriter extends ClassWriter`, override `visitMethod` to return a
18+
buffering MethodVisitor. Died on compilation: ASM 9.10 declares `ClassWriter.visit` and
19+
`visitMethod` `public final`. No interception is possible via subclassing.
20+
21+
The interception point must be a **wrapping `ClassVisitor`** (the standard ASM
22+
instrumentation shape) delegating to the real ClassWriter, which stays the terminal pipeline
23+
stage and `toByteArray()` producer. Chunks are emitted directly on the real writer; the
24+
splitter takes `(node, realWriter, chunk0Visitor)`.
25+
26+
Any future ASM-pipeline work in this repo should start from a ClassVisitor wrapper, not a
27+
ClassWriter subclass.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
id: dead-env-gotchas
3+
title: Build/test/harness gotchas for java-reggie and consumer-repo regex audits — stale-jar trap, gradle caching, piped-build chains, JDK-differential tests, JDK matcher statefulness (resolved — don't re-debug)
4+
kind: dead-end
5+
tags: [environment, git, gradle, classpath]
6+
applies_to: []
7+
source: dd_backend_fit/dead-env-gotchas
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
15+
# Environment gotchas hit (resolved — don't re-debug)
16+
17+
- Both consumer repos had stale `.git/index.lock` from crashed git processes →
18+
pull failed with "an editor opened by git commit" message; fix: `rm .git/index.lock`.
19+
- Large consumer repos (e.g. logs-backend): remote may carry dead refspecs
20+
(plain fetch dies with "couldn't find remote ref …"; workaround
21+
`git fetch origin <branch> && git merge --ff-only FETCH_HEAD`) and
22+
filesystem-wide greps time out — ALWAYS `git ls-files '*.java' | xargs grep …`.
23+
- reggie-runtime jar does NOT bundle ASM (declared `implementation`): any
24+
standalone harness needs asm/asm-commons/asm-util 9.10.1 on the classpath
25+
(from ~/.gradle/caches), else every compile fails with
26+
NoClassDefFoundError MethodTooLargeException (misleading).
27+
- Harness must emit results incrementally + per-entry timing; buffered output
28+
+ one hung pattern (semver) loses 15 min of work.
29+
30+
Reggie build discipline (don't repeat):
31+
- STALE-JAR TRAP: requesting only the runtime jar after editing reggie-codegen sources can
32+
be an up-to-date NO-OP — the fat runtime jar embeds the codegen classes and did not
33+
repackage. After codegen changes run :reggie-codegen:compileJava explicitly and
34+
sanity-check via a changed generated-class hash.
35+
- `./gradlew jar ... | grep BUILD` in an && chain does NOT stop on BUILD FAILED — grep
36+
exits 0 on match and the following test/verify runs against the STALE jar. Check exit
37+
codes explicitly; never pipe-build-then-run in one chain.
38+
- Gradle test tasks are cached: `./gradlew test` returning BUILD SUCCESSFUL
39+
in <1s means UP-TO-DATE, not re-run. For real verification use
40+
`./gradlew cleanTest test`.
41+
- git commit signing via ~/.ssh/datadog_git_commit_signing can fail
42+
("agent refused operation") until the key passphrase is cached; retry after
43+
unlocking (user unlocked on demand 2026-09-16).
44+
- Writing regression tests: do NOT hand-derive expectations (repeated
45+
test-assertion bugs from misread semantics); write JDK-differential tests
46+
(compute expected values from java.util.regex at runtime).
47+
- JDK Matcher is STATEFUL: matcher.matches() then matcher.find() continues
48+
from the end of the previous match — use a fresh Matcher per operation in
49+
differential tests (ReggieMatcher is stateless per call).
50+
- jstack sampling root-causes compile hangs; run the victim via nohup+disown
51+
in one tool call, sample in the next (the tool kills the process group
52+
when the command ends).
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
id: dead-group-boundary-fusion
3+
title: Group-boundary state fusion in the capture-tracking DFS is net-negative — group writes sit on loop edges off the hot path, while the extra ops array taxes every push/pop
4+
kind: dead-end
5+
tags: [fusion, group-boundary, dfs, capture, perf, measured-negative, reverted]
6+
applies_to: []
7+
source: backend-ready/dead-group-boundary-fusion
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
# Group-boundary state fusion in the capture-tracking DFS is net-negative
15+
16+
## What was tried
17+
Fusing group enter/exit-only states into incoming edges so capture writes ride on edges
18+
instead of dedicated marker frames. Two variants built and fully tested:
19+
- EAGER: ops applied at push — pays ~150 wasted capture clones for never-popped
20+
marker-stop frames on greedy paths.
21+
- DEFERRED: ops row on the frame, applied at pop (sound: push pos == pop pos of the
22+
bypassed state); post-write dedup strictly better.
23+
24+
## Why it was refuted (measured)
25+
- Realistic 313-char accept shape: +1.4–2.4us; reject shape: +6.2–7.8us; only degenerate
26+
5-char inputs win ~0.1us.
27+
- Instrumentation: fusion removes only ~10 pops on the 313-char accept — the pre-release
28+
loop body has NO boundary states (group writes sit on loop EDGES, off the winning path) —
29+
while the extra ops array taxes every push/pop (~0.9ns x ~1700 frames); reject paths pay
30+
on 5–10x more frames.
31+
- State-id bit-packing of ops (~0.3–0.5ns/pop) also estimated net-negative for the same
32+
reason.
33+
34+
## Verdict / redirect
35+
Trades a negligible win on degenerate short inputs for microsecond-class losses on
36+
realistic shapes. Short-input closure cost needs a hybrid engine, not fusion.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
id: dead-handler-extent-heuristic
3+
title: Exception-handler noCut zone must be {region start, region end−1, handler entry} only — a handler-extent heuristic (walk to first terminal) over-conservatively no-cuts the whole method for fall-through handlers
4+
kind: dead-end
5+
tags: [exception-handler, no-cut-zone, refuted-approach]
6+
applies_to: []
7+
source: multi-64k/dead-handler-extent-heuristic
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
15+
# noCut = positions separating {region start, end−1, handler entry} — not the handler extent
16+
17+
First attempt at protecting try/handler regions: noCut zone = protected region ∪ handler
18+
*extent*, where the extent was computed by walking forward from the handler label to its first
19+
`athrow`/`return`/`goto`/switch. Two flaws:
20+
21+
1. **Over-conservative**: a fall-through handler (catch sets a flag, then continues into the
22+
method tail) has no terminal → the walk ran to the end of the method → the entire method
23+
became no-cut → `chooseCuts` declined (observed: tryRegion test declined then verbatim
24+
threw).
25+
2. **Wrong model**: the handler body extending across a cut is *fine* — control flows through
26+
the chunk tail chain; only the handler *entry label* must live in the region's chunk.
27+
28+
Correct rule: region + handler entry must share a chunk; bodies may tail-chain. noCut =
29+
positions separating {start, end−1, handler}.
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
id: dead-real-generator-differential-test
3+
title: No constructible pattern both overflows a generated method and compiles within the 10s deadline — an end-to-end overflow differential test is infeasible; splitter semantics must be covered at unit level
4+
kind: dead-end
5+
tags: [testing, differential, probe, ci-flake]
6+
applies_to: []
7+
source: multi-64k/dead-real-generator-differential-test
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
15+
# End-to-end overflow differential test is infeasible
16+
17+
Test-plan idea: "a pattern whose NFA step method overflows 64KB today compiles,
18+
loads, and matches java.util.regex differentially." No such pattern could be constructed:
19+
20+
- Huge literal capture alternations route to BitStateMatcher (compact) or LiteralAlternation
21+
trie strategies — method size never overflows.
22+
- Pushing counts up (4000 alternatives) hits OOM in the analysis phase, before codegen.
23+
- Compiles that do succeed take 8.5–13.5s against the 10s total-compile deadline — too close
24+
for a stable CI test; the probe test was deleted rather than kept flaky.
25+
26+
Consequence: splitter semantics must be covered at unit level (load real classes so the JVM
27+
verifier validates recomputed frames, check exact values) plus the runtime suite through the
28+
wired-in pipeline. Don't spend more time hunting a triggering pattern within the current
29+
strategy/limit envelope.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
id: dead-sourceinterpreter-typing
3+
title: SourceInterpreter is unusable as slot-typing basis for large generated methods — copyOperation masks producers (ASTORE) and source sets grow quadratically at loop-carried merges (78k-insn method hung the test worker >120s)
4+
kind: dead-end
5+
tags: [asm, sourceinterpreter, refuted-approach, performance]
6+
applies_to: []
7+
source: multi-64k/dead-sourceinterpreter-typing
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
# SourceInterpreter pathologies make it unusable for slot-typing large generated methods
15+
16+
The original typing plan derived live-slot types from `SourceInterpreter` frames (per-source
17+
descriptors + unmask recursion for ASTORE/xLOAD). Two fatal flaws:
18+
19+
1. Quadratic set growth on loop-carried locals at merge points — a 78k-insn realistic test
20+
method hung the test worker (>120s, jstack pinned `Analyzer.merge`, Analyzer.java:642).
21+
Generated NFA-style methods are exactly this shape: each merge unions the previous
22+
iteration's accumulated set with new defs → O(k) set unions per merge → O(n²) total.
23+
2. ASTORE source masking forced an unmask-recursion layer (operand sources at the store, local
24+
sources at loads) — `copyOperation` attributes values to the *copy instruction*, so a
25+
local's type cannot be derived directly from its source insns. Built, working, then deleted
26+
with the interpreter swap.
27+
28+
Consequence: use a descriptor-flowing interpreter instead (see the DescriptorInterpreter
29+
memory). SourceInterpreter remains fine for small methods or one-shot analyses; the pathology
30+
needs loop-carried locals surviving merge points.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
---
2+
id: find-api-surface-sufficient
3+
title: ReggieMatcher runtime API covers every JDK Matcher op observed in the dd backends; grok SPI + shadow infra is the insertion seam (top-level reggie.ReggieMatcher facade exposes only matches/find)
4+
kind: finding
5+
tags: [api, compatibility, grok, spi, shadow]
6+
applies_to: [reggie-runtime/src/main/java/com/datadoghq/reggie/runtime/ReggieMatcher.java, reggie-runtime/src/main/java/com/datadoghq/reggie/ReggieMatcher.java]
7+
source: dd_backend_fit/find-api-surface-sufficient
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
15+
# API surface no longer a blocker; grok SPI + shadow infra is the insertion seam
16+
17+
Trunk `com.datadoghq.reggie.runtime.ReggieMatcher` covers every JDK Matcher op
18+
observed in both repos: matches/find/findFrom, match/findMatch→MatchResult
19+
(indexed+named groups, spans), matchInto/findMatchInto (alloc-free),
20+
replaceFirst/replaceAll (literal + Function<MatchResult,String> — covers
21+
appendReplacement loops and results() streams), split(input[,limit]), findAll,
22+
cursor() with appendReplacement/appendTail.
23+
24+
Caveat: top-level `com.datadoghq.reggie.ReggieMatcher` facade exposes ONLY
25+
matches/find — consumers must type against runtime.ReggieMatcher.
26+
27+
re2j maps 1:1 (`Pattern.matcher(input).find()/matches()`, groupCount, named
28+
groups). logs-backend grok pipeline: production runs `JdkRegexPatternSupplier`
29+
(re2j supplier is non-production); `GrokPatternBundle` supports a SHADOW
30+
supplier with configurable shadow_ratio + `RegexMatcherShadowActor` — A/B
31+
validation infra already built for a `ReggieRegexPatternSupplier`. The re2j
32+
supplier's JDK→RE2 syntax-conversion hack (`(?P<` rewrite) becomes unnecessary.
33+
Remaining JDK-adjacent needs: Pattern.quote (keep on JDK), asPredicate adapter,
34+
Jackson Pattern deserialization (pb YAMLInsightProvider), Map<FrameField,
35+
Pattern> adapter type.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
id: find-asm-classwriter-final
3+
title: ASM interception shape — ClassWriter.visit/visitMethod are final in 9.10, so pipeline instrumentation must wrap a ClassVisitor delegating to the terminal ClassWriter
4+
kind: finding
5+
tags: [asm, classwriter, classvisitor, interception]
6+
applies_to: []
7+
source: multi-64k/find-asm-classwriter-final
8+
status: active
9+
recorded: 2026-09-28
10+
valid_at: 2026-09-28
11+
---
12+
13+
14+
15+
# Wrap a ClassVisitor — ClassWriter methods are final
16+
17+
`javap` on asm-9.10.1.jar shows:
18+
`public final void visit(int,int,String,String,String,String[])` and
19+
`public final MethodVisitor visitMethod(int,String,String,String,String[])` in
20+
`org.objectweb.asm.ClassWriter`. A `ClassWriter` subclass therefore cannot intercept method
21+
emission ("overridden method is final" compile errors).
22+
23+
Consequence: the interception point must be a **wrapping `ClassVisitor`** (the standard ASM
24+
instrumentation shape) delegating to the real ClassWriter, which stays the terminal pipeline
25+
stage and `toByteArray()` producer. In the (now dropped) splitter patch, reggie's generators
26+
had parameters typed `ClassWriter` and were mechanically widened to `ClassVisitor`
27+
(~32 files); the only non-visitor use, `RecursiveDescentBytecodeGenerator.generate()`'s
28+
internal `cw.toByteArray()`, was restructured to real-writer + front-visitor. Both pipeline
29+
entry points (`RuntimeCompiler.generateBytecode`, `ReggieMatcherBytecodeGenerator`) construct
30+
`new ClassWriter(COMPUTE_FRAMES|COMPUTE_MAXS)` wrapped by the front visitor — that wiring was
31+
removed when the splitter was dropped and must be re-added on revival (see the L2 memories).

0 commit comments

Comments
 (0)