Java24 ttd - #7
Open
jon-bell wants to merge 211 commits into
Open
Conversation
Phase 0: within-method backward stepping via explicit Ttd.breakpoint() calls in user code. Single-threaded; deterministic-body required. Mechanism: * Ttd.session(root, body) takes Crochet.checkpoint(root) on entry, runs body. Each Ttd.breakpoint() increments a step counter and either pauses (yielding to a REPL) or returns silently (silent replay past an earlier point). * Backward step / goto-prior throws a Restart inside breakpoint(); session catches it, rolls back root, sets the new target stop, and re-executes body. Forward stepping just bumps the target and returns. * REPL: stdin/stdout, line-oriented. Commands: next, back, goto N, inspect, where, quit, help. inspect dumps root's fields via reflection. Tested: * forward_then_back_then_forward — verifies body re-executes fully on rollback, hit sequence is [1,2,3,1,2,3] for [forward,forward,back, forward,quit] with 3 BPs in body. * inspect_shows_current_state_after_back — verifies state.value reverts from 20 to 10 after `back` (this is the load-bearing semantic claim — Crochet rollback genuinely restores state). * goto_forward_skips_intermediate — `goto 3` from BP 1 silently skips BP 2. Limitations documented in README: * Single-thread only (multi-thread needs Fray-style determinism) * Body must be deterministic on replay * Cannot back-step out of the session lambda (Crochet doesn't restore call stack) * Only the explicitly-tracked root is rolled back (not arbitrary reachable state outside it) This is the Crochet-substrate "general TTD" application discussed in ~/tapestry/docs/scope-and-applications.md, independent of Fray. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds:
* @TimeTravelBody method annotation
* TtdAgent (-javaagent) that registers LineMarkerTransformer
* LineMarkerTransformer (ASM-based) that inserts Ttd.lineHit(owner,
methodSig, line) at every LineNumberTable entry of any method
bearing the annotation
* Ttd.lineHit() runtime entry — works like breakpoint() but carries
source-location context (owner.method:line) for the REPL to display.
Silent no-op outside an active session.
* Repl prompt now shows "at step N <Class>.<method>(<desc>):<line>"
for line-marker hits
Mechanism is the same as Phase 0: each line-marker fires a step counter
bump; if past the REPL's target stop, it pauses and yields to the REPL;
otherwise (silent replay) it returns immediately. Back-stepping throws
a Restart caught by Ttd.session, which rolls back the tracked root via
Crochet and re-runs the body with a new target stop.
Pom packaging follows crochet-agent's pattern: shaded ASM relocated to
edu.neu.ccs.prl.crochet.ttd.shaded.asm so the agent jar is
self-contained and doesn't conflict with user-side ASM versions.
Premain-Class manifest entry. Surefire argLine adds both the TTD agent
and the Crochet agent (TTD first so its line markers exist in the
bytecode by the time Crochet's transformer sees the class).
Tests:
* TtdSmokeTest (Phase 0) — 3/3 pass, unchanged
* TtdLineMarkerTest (Phase 1) — 2/2 pass:
- auto_line_markers_fire: verifies the REPL prompt mentions the
source class + method, proving auto-instrumentation is active
- auto_line_markers_back_step_restores_state: jumps forward to step
5, inspects, back-steps to step 1, inspects, asserts state.value
is smaller (rolled back to a prior mutation)
Bug fixed during development: skip-list prefix
"edu/neu/ccs/prl/crochet/ttd/Ttd" was matching test classes named
TtdSomething. Replaced with narrower bootstrap-only skip; the
constant-pool pre-filter handles agent-internal classes (none of
which carry @TimeTravelBody).
Limitations unchanged from Phase 0 (single-thread, deterministic body,
no cross-method back-step); annotation only applies to non-lambda
non-synthetic methods.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
For each Phase 0/1 limitation (multi-thread, auto-root collection, cross-method back-step, replay determinism), proposes architecture options with explicit tradeoffs and a recommended path. Identifies phase ordering: auto-root + checkpointAll first (cheap, no new deps), then Fray-backed multi-threaded session (closes 3 of 4 limitations at once via Fray's existing scheduler+nondet machinery), then IDE integration as the demo-worthy capstone. Stack-frame restoration is documented as out of scope (blocked on JVMTI not exposing frame push); IO record/replay also out of scope. Includes an honest observation: Phase 1's auto-instrumented line markers actually give us cross-method back-stepping for free WITHIN a session — methods called from the body get their lines woven into the global step counter, and replay-based stack reconstruction is implicit. The remaining limitation is "can't back-step out of the session lambda," which is fundamental to the checkpoint+replay model. Open design questions flagged for discussion: module split (keep crochet-ttd Fray-free vs fold in), REPL-vs-IDE-first ordering, snapshot-diff inspector as a cheap add-on, replay-divergence verification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tiered pie-in-the-sky spec pulled from TTD design, Tapestry integration work, crochet-junit5, and bench-harness experience. Tier 1 (concrete, scoped — "we know how to build this"): * Stack-as-data snapshot — JVMTI stack capture stored alongside the heap snap; for display, not restoration * Snapshot diff API — programmatic access to "what changed between V1 and V2 of this object?" * Delta checkpoints — only record fields that changed since last checkpoint; persistent-data-structure style * Replay-divergence detection — flag when nondeterministic calls diverge between original and replay * @CrochetSkip class-level opt-out annotation Tier 2 (research-grade, harder): * checkpointWorld() — whole-program reachable-instance + static snapshot via JVMTI heap iteration * Per-thread checkpoint scope — thread-local snap chain; DRF-precondition soundness story * Cooperative checkpoint with thread sync — safepoint-synchronized whole-program snap * External-state hooks — register custom serializers for file descriptors, sockets, DB state * Memory-budgeted snap retention — LRU eviction with pinning * Annotation-processor for compile-time @CrochetCheckpoint methods Tier 3 (speculative; may need JVM changes): * Stack-frame restoration via JVMTI (blocked: JVMTI doesn't expose frame-push; would need JEP or Loom Continuation co-design) * Persistent immutable snapshot history (cross-JVM portable) * Time-travel within JIT-compiled code (extend version-zero short-circuit to TTD hooks) * Composable Crochet — multi-agent composition diagnostics + protocol Prioritization sketch identifies stack-as-data + snapshot-diff as the highest-leverage Tier-1 items; per-thread checkpoint scope as the key enabler for Tier-2 TTD work; remaining Tier-2 items as follow-on standalone project proposals. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nits Source-of-truth spec for the fleet rollout. Decomposes WISHLIST.md into 30+ units across phases A-H sized for one agent each, with explicit dependencies, validation gates, and reviewer requirements per unit. Establishes 21 universal quality gates that every unit clears before merge. Key decisions captured up-front: * 3.1 (stack-frame restoration) reframed as bytecode CPS, promoted to centerpiece (Phase B). * 3.2 (cross-JVM snapshot transport) dropped as out of scope. * 2.6 (compile-time checkpoint) routed through the existing transformer rather than APT. * 2.2 (per-thread scope) spun off as research (Phase G) gated on a new I4 footprint-disjointness invariant. * 1.3 full / 2.5 (snap chain + budgeted retention) gated on Phase A measurement; default is 1.3 lite (PUTFIELD dirty bit) only. * No-Fray TTD ships as a product; D.3 (record/replay of documented nondet sources) is mandatory, not conditional. design-future-phases.md updated to reflect the bytecode-CPS choice in option (C); old "JVMTI frame-push not feasible" framing replaced with the Quasar/Kilim pattern and a forward reference to PLAN.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Establishes the measurement protocol for snap-memory analysis before any measurements are taken, per the PLAN.md requirement that the spec is frozen at commit time. Covers workloads (DaCapo h2, h2o, microbench), JDK build (Java 21 Temurin at /tmp/jdk-inst-A.1), checkpoint cadence, warmup counts, trial count (5 runs, report median+p95+IQR), and the numeric success metric for F.2 go/no-go. Documents the W3 substitution: Tapestry's Gradle/Fray integration is not trivially runnable in isolation; the existing eval/microbench/ harness is used instead as a conservative (more-aggressive-cadence) proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds net.jonbell.crochet.annotation.CrochetSkip so application authors can exclude specific classes from checkpoint/rollback tracking without touching the hardcoded CrochetTransformer.shouldSkip list. The hardcoded list documents JDK-internal, Hibernate, Fray, and other framework incompatibilities that require suppression regardless of whether user code annotates the class; it remains the authority for those cases. @CrochetSkip is additive and user-facing only. Implementation decisions (see designs/A.2/DESIGN.md): - Check fires in transform() AFTER the hardcoded shouldSkip() — the two mechanisms are ORed, hardcoded list always takes priority. - Annotation is read directly from class-file bytes via ASM ClassReader, not via reflection, so no class loading occurs at transform time. - Inheritance is explicit: hasSkipAnnotation() walks the superclass chain via class-loader resource reads (same technique as SafeClassWriter .superOfUncached), stopping at java/lang/Object or any name the hardcoded list already covers. Java's @inherited is not used because it requires the annotated class to be loaded. Tests (10 new, 45 total): - Direct annotation → transformer returns null - Subclass of annotated class (depth 1) → also skipped - Grandchild of annotated class (depth 2) → also skipped - Unannotated class → instrumented normally (control) - Hardcoded skip-list still fires first; no interaction surprises - hasSkipAnnotation() false for unannotated class, true for annotated Annotation marked @stable per gate 14. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements unit A.4 of PLAN.md in full:
1. InstrumentedSurfaceVerifier — a lowest-priority ClassFileTransformer
registered after TransformerWrapper in CrochetAgent.premain. When
-Dcrochet.verifyInstrumented=true is set, re-reads each class after all
transformers have run and checks that @CrochetInstrumented, $$crochetVersion,
$$crochetSnap, $$crochetAccess, $$crochetCheckpoint, $$crochetRollback, and
CRIJInstrumented are all present. Emits:
[Crochet-Verify] SURFACE_MISMATCH class=<name> missing=<elements>
on mismatch — NOT a ClassFormatError. Detects silent agent-stack clobbering
(e.g. Byte Buddy rewriting $$crochetAccess to a no-op) at load time.
The check is annotation-gated: only classes where @CrochetInstrumented is
already present (i.e. Crochet successfully transformed them) are checked for
the remaining surface. This prevents noise for third-party classes that the
transformer declined to process.
2. crochet-compose-kit reactor module — pre-bakes the Fray skip-list
(org/pastalab/fray/) and provides @CrochetCompositionTest / AgentConfig for
multi-agent matrix testing. README enumerates known-good compositions and the
failure mode each pre-baked skip-list entry prevents ($ByteBuddy$,
$HibernateProxy$, _$$_Weld, $$$view, org/pastalab/fray/).
3. @stable / @experimental / @internal annotations under
net.jonbell.crochet.annotation. Retroactively applied: CheckpointRollbackAgent
→ @stable (user-facing API, ABI frozen); CRIJInstrumented → @stable; RollbackException
→ @stable; CrochetEager → @stable; CrochetInstrumented → @internal; all
runtime helpers (ArrayRegistry, ClassMeta, ReflectionFilter, RuntimeReady,
RuntimeTracer, StackRoots, Tag, CRIJFast) → @internal; CrochetTransformer →
@internal. Default @internal for all: easier to widen than narrow.
4. .github/workflows/universal-gates.yml — GitHub Actions CI covering all 21
universal quality gates from PLAN.md as parallel jobs: unit-tests (gate 1),
integration-tests (gate 2), demo-scenarios (gate 3), dacapo-functional (gate
4, vacuous when JAR absent), bytecode-verification (gates 5+18), dacapo-
regression (gate 6, vacuous when no baseline), skip-list-hygiene (gate 11),
downstream-smoke (gate 12), composition-assert (gate 13, greps for
SURFACE_MISMATCH), stability-annotations (gate 14), claude-md-check (gate 20),
design-doc-check (gate 21). Gates 7/8/9/15-17 require human judgment; gated
via branch protection or PR review conventions.
5. CrochetTransformer.shouldSkip made public so the verifier and test bridge
can call it without bypassing package encapsulation.
Negative test: a class with $$crochetAccess stripped is detected by
InstrumentedSurfaceVerifier with SURFACE_MISMATCH naming the missing element.
Positive test: complete-surface class passes silently; shouldSkip classes and
interfaces are never checked.
All 48 existing crochet-agent tests pass (41 unit + 7 new verifier tests).
crochet-compose-kit tests: 7 tests pass (3 positive composition + 4 verifier).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eld-level diff Gives users programmatic access to "what changed" between a live object (or class) and the most-recent Crochet snapshot, without forcing them to invoke rollback just to inspect state. Intentionally out of scope (v1): - Graph recursion: referents are compared by reference, not walked. - Snap chains: only the single-slot snap from the most-recent checkpoint. - Reference-array element diffs: reference arrays are opaque in v1. Key discovery during implementation: the static-field helper (StaticFieldHelperTemplate) uses EAGER snapshot semantics — $$crochetCheckpoint stores static values directly into the helper's own instance fields (no $$crochetSnap shadow). The live-checkpoint guard for the static case is helper.$$crochetGetVersion() != 0, not snap != null. This differs from the instance diff path. Field discovery uses Class.getDeclaredFields() filtered to the same predicate as FieldAdder.visitField (non-static, non-final, non-synthetic, non-$$crochet*), ensuring diff walks exactly the set that checkpoint/rollback operates on. 36 new tests cover: all 8 primitive types, reference, primitive arrays (all 8), reference arrays, null transitions in both directions, self-edge cycle safety, static-field diff, and a property test (diff is the inverse of rollback over 50 random mutation rounds). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nterning
This is the runtime layer that the bytecode CPS transformer (B.3) and the
session integration (B.4) will call into. Zero-alloc steady state is the
property that lets @TimeTravelBody be cheap-when-idle: outside an active session
the hot path is a single volatile read and a conditional branch; no ThreadLocal
access, no ResumeFrame allocation.
New surface in crochet-ttd:
- ResumeFrame (public, @Internal-TODO) — (methodId:int, bci:int, prims:long[],
refs:Object[]) value object capturing live locals at a CPS save point.
- Ttd.saveFrame(int, int, long[], Object[]) — pushes a frame onto the per-thread
ArrayDeque<ResumeFrame>; early-return guard on TTD_ACTIVE_SESSIONS==0 before
any ThreadLocal.get() or alloc.
- Ttd.popResumeFrame(int) — peek-and-conditionally-pop by methodId match;
same zero-alloc guard.
- Ttd.internMethodId(String) — ConcurrentHashMap<String,Integer> interning table
keyed on "class.method(desc)"; B.3 will call this at class-load time.
- Ttd.TTD_ACTIVE_SESSIONS — stand-in for C.1 TTD_GEN; plain volatile int that
C.1 will promote to a parity-encoded generation counter. C.1 should adopt
this field directly and rework its semantics.
Session lifecycle change: sessionWithRepl now increments TTD_ACTIVE_SESSIONS on
entry and decrements it + calls clearSessionState() (drain + FRAME_DEQUE.remove())
in the finally block, covering both normal and exceptional exits.
ThreadLocal init strategy: withInitial(ArrayDeque::new) rather than lazy-init; the
supplier fires only inside a session (after the TTD_ACTIVE_SESSIONS guard), so the
JIT sees a non-null get() on every hot call site and can eliminate the null-check.
Zero-alloc property verified by ResumeFrameTest using MethodHandle (not
Method.invoke, which boxes the long return value) to call
com.sun.management.ThreadMXBean.getThreadAllocatedBytes. Both saveFrame and
popResumeFrame show delta=0 bytes across 10,000 warm calls.
@internal annotation uses TODO marker; the annotation class ships in A.4 which
is not yet merged. B.3 and B.4 are the next consumers of this API.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion This is the centerpiece of unit E.1. The sketch argues that: 1. STW via SuspendThreadList eliminates torn-snap races: after SuspendThreadList returns, no mutator can execute PUTFIELD/PUTSTATIC, so the heap-walk sees a frozen world. 2. The lazy model preserves I1/I2/I3: $$crochetCheckpoint(V) writes only two words (klass header CAS + version CAS), no field copy. The snap allocates lazily on the first post-checkpoint PUTFIELD, so rollback still restores the before-image correctly. 3. checkpointWorldSafe strictly subsumes checkpointAll: same static pass + STW instance walk covers non-stack-reachable heap body. 4. Mid-iteration class-load is impossible during STW; post-resume new instances are at version 0 and are correctly skipped by rollback. 5. Seven threats to validity are documented explicitly (T1–T6+): JIT register cache, native oop pointers, finalizers, Unsafe plain memory order, Loom virtual threads, static-vs-instance ordering. The fallback decision: fall back to checkpointAll with a non-suppressible stderr warning if the native agent is not loaded (not fail-fast), because checkpointAll is sound for the majority of practical workloads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the full A.1 unit: methodology spec (frozen at commit 3fddb98), runner, analysis script, raw data from 3 workloads, and the F.2 go/no-go memo. Key findings from 13 complete trials (5 microbench, 5 h2o, 3 h2): - All workloads show top-10-class concentration = 100%: the snapshot working set is structurally narrow (≤6 class types account for all fastAccess). - fastAccess per checkpoint: 121 (microbench), 1,094 (h2o), 3,878 (h2), all well below the 100K threshold needed to justify F.2 ABI complexity. - fastAccess in real workloads is dominated by java.lang.Thread objects (checkpointAll walks the thread list), not user-domain objects. - sfHelperFor counts dwarf fastAccess (2.9M vs 4.4K for h2o), indicating static-field access is the dominant hot path, not instance checkpoints. Decision: DEFER F.2 (snap chain). Build F.1 (dirty-bit) instead — it would eliminate ~100% of fastAccess allocation on all tested workloads. DaCapo note: dacapo-23.11-chopin data archive not available on this machine (same failure seen in eval/dacapo/baseline-phase-a). Synthetic H2 and H2O proxies are used per METHOD.md §Workloads amendment policy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…data
The ResumeFrame deque (B.2) already IS the stack-as-data; this unit surfaces
it via Ttd.captureStack(), dropping WISHLIST 1.1's originally-planned
native-JVMTI path entirely (~100 LOC vs. a native agent).
New surface:
- StackEntry record — (classMethodLine, List<LocalSnapshot>) per frame,
innermost first.
- LocalSnapshot record — (name, typeDescriptor, value) per local slot.
- Ttd.captureStack() — snapshot copy of the current thread's FRAME_DEQUE;
empty list outside active session; decoupled from the live deque.
- Ttd.registerMethodLine(methodId, bci, label[, primNames, primDescs,
refNames, refDescs]) — populates the (methodId, bci)->MethodLineInfo
debug table at class-load time (B.3 will call this; tests call it
directly to exercise the API layer before B.3 lands).
- Ttd.serializeStack(List<StackEntry>) — JSON schema version 1 with
{"schemaVersion":1,"frames":[...]} wrapper; deterministic for same input.
Design decisions:
- Debug table key is (methodId << 32 | bci) packed into a long — one entry
per save-point (not per method), because bcis within a method map to
distinct source lines.
- transform-time emit approach: registerMethodLine is a separate call from
internMethodId so B.3 can call both at class-init time without changing
internMethodId's existing signature (which B.3 drafts already call).
- LocalSnapshot fallback to "$slotN"/"?" when no LVT info is registered
(covers -g:none case; B.3 will register real names when LVT is present).
Validation:
- 14 new tests in StackCaptureTest covering LIFO order at depths 1–5,
registered/sentinel labels, LVT-present/absent local names, serialization
stability, deque decoupling, JSON escaping.
- All 31 tests pass (14 B.5 + 17 pre-existing).
Upstream dependency: unit/B.2-resume-frame (this unit is NOT standalone).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B.3 needs to know which locals are live at each save point so it emits only those into the ResumeFrame; saving non-live locals wastes alloc and breaks zero-alloc-when-no-session. The alternative — saving all declared locals — would force every save point to allocate a max-sized long[] and Object[] (sized to max_locals), regardless of how many variables are actually live at that BCI. Algorithm: ASM Analyzer<BasicValue> with BasicInterpreter runs one forward typed pass over the CFG. At each save-point BCI, a slot is live iff its frame entry is not UNINITIALIZED_VALUE (TOP). Category-2 types (long, double) are reported as one LiveLocal at slot N with getSize()==2; the phantom slot N+1 (TOP placeholder) is suppressed. Branch joins are handled conservatively by the forward merge; exception edges are handled automatically by the Analyzer. Key finding: BasicInterpreter does NOT distinguish uninitialized-this from a live reference — both appear as Object. Uninitialized-this rejection (save point before super() in <init>) is therefore implemented via bytecode scan: find the first INVOKESPECIAL <init> instruction; any save-point BCI strictly before it throws IllegalStateException. Deliverables: - crochet-ttd/src/main/java/.../cps/LivenessAnalyzer.java (analyzer) - crochet-ttd/src/test/java/.../cps/LivenessAnalyzerTest.java (13 tests) - crochet-ttd/src/test/java/.../cps/CorpusLivenessTest.java (fuzz corpus, hash-pinned: cd17554cb5595739b08352bd7778fe0dd5cd5aecc331fe565752b422e25828c3 over 27,834 JDK 21 class files) - crochet-ttd/src/jmh/.../LivenessBenchmark.java (timing harness) - designs/B.1/DESIGN.md (algorithm rationale, budget, corpus hash) - pom.xml + crochet-ttd/pom.xml: asm-tree + asm-analysis 9.9 deps Performance: java.lang.String (167 concrete methods, all BCIs as save points): median 4.74 ms. Budget = median × 1.5 → 10 ms (rounded for CI GC variance). LivenessBenchmark.PER_CLASS_BUDGET_MS = 10. Universal gate 18 (deterministic emission): corpus test hash-pinned. Universal gate 21 (design doc): designs/B.1/DESIGN.md committed. All 19 crochet-ttd tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
B.3 needs to know which locals are live at each save point so it emits only those into the ResumeFrame; saving non-live locals wastes alloc and breaks zero-alloc-when-no-session. The alternative — saving all declared locals — would force every save point to allocate a max-sized long[] and Object[] (sized to max_locals), regardless of how many variables are actually live at that BCI. Algorithm: ASM Analyzer<BasicValue> with BasicInterpreter runs one forward typed pass over the CFG. At each save-point BCI, a slot is live iff its frame entry is not UNINITIALIZED_VALUE (TOP). Category-2 types (long, double) are reported as one LiveLocal at slot N with getSize()==2; the phantom slot N+1 (TOP placeholder) is suppressed. Branch joins are handled conservatively by the forward merge; exception edges are handled automatically by the Analyzer. Key finding: BasicInterpreter does NOT distinguish uninitialized-this from a live reference — both appear as Object. Uninitialized-this rejection (save point before super() in <init>) is therefore implemented via bytecode scan: find the first INVOKESPECIAL <init> instruction; any save-point BCI strictly before it throws IllegalStateException. Deliverables: - crochet-ttd/src/main/java/.../cps/LivenessAnalyzer.java (analyzer) - crochet-ttd/src/test/java/.../cps/LivenessAnalyzerTest.java (13 tests) - crochet-ttd/src/test/java/.../cps/CorpusLivenessTest.java (fuzz corpus, hash-pinned: cd17554cb5595739b08352bd7778fe0dd5cd5aecc331fe565752b422e25828c3 over 27,834 JDK 21 class files) - crochet-ttd/src/jmh/.../LivenessBenchmark.java (timing harness) - designs/B.1/DESIGN.md (algorithm rationale, budget, corpus hash) - pom.xml + crochet-ttd/pom.xml: asm-tree + asm-analysis 9.9 deps Performance: java.lang.String (167 concrete methods, all BCIs as save points): median 4.74 ms. Budget = median × 1.5 → 10 ms (rounded for CI GC variance). LivenessBenchmark.PER_CLASS_BUDGET_MS = 10. Universal gate 18 (deterministic emission): corpus test hash-pinned. Universal gate 21 (design doc): designs/B.1/DESIGN.md committed. All 19 crochet-ttd tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gives users a sound primitive for capturing external state (file descriptors, DB savepoints, socket cursors, Redis keys) that is invisible to Crochet's heap walk, without Crochet shipping the long-tail of integrations. Adapter refusal is by design: this commit ships the registry API only. No JDBC, Redis, or filesystem adapters are included in-tree. The adapter surface is unbounded and coupling Crochet to third-party library ABIs would propagate breakage to unrelated users. The javadoc on Crochet.registerExternalState makes this explicit and shows a three-line wiring pattern. Ordering contract: - snapshot() runs serially on the calling thread BEFORE checkpointAll()'s root walk, so hooks see the pre-checkpoint heap. - restore() runs AFTER rollbackAll()'s heap restore, so hooks see the post-rollback heap. The snapshot return value (e.g. a DB savepoint handle) is delivered to restore() — users close over state via the Supplier/Consumer pair. Throw semantics: - Throws-in-snapshot: fail-fast; subsequent hooks skipped; checkpointAll() propagates the original exception unwrapped; lastSnapResults set to null. - Throws-in-restore: all hooks run; all exceptions collected as suppressed throwables on a new RollbackException.HookFailure (which extends RollbackException so existing catch sites see it). Hook name appears in the suppressed message. Zero-allocation cold path (gate 7): CopyOnWriteArrayList.isEmpty() is a single volatile array-length read when no hooks are registered — no iterator, no array copy, no allocation. Files added/modified: - ExternalStateRegistry.java — registry storage (CopyOnWriteArrayList + ConcurrentHashMap), Hook record, fireSnapshots()/fireRestores() dispatch. - RollbackException.java — adds static inner class HookFailure. - CheckpointRollbackAgent.java — wires fireSnapshots() into checkpointAll() (before root walk) and fireRestores() into rollbackAll() (after heap restore). - Crochet.java — adds registerExternalState() and unregisterExternalState() with explicit no-adapters javadoc and @stable stability note. - ExternalStateRegistryTest.java — 14 new tests covering the full validation matrix: ordering contract, throws-in-restore, throws-in-snapshot, empty registry, duplicate registration, unregister, null-check, composition (gate 13), HookFailure subtype, registration-order preservation. Test count: 71 → 85 (14 new tests, all pass). Demo smoke: 21/21 scenarios pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This is the soundness foundation for checkpointWorldSafe(), the primitive that Phase E's whole-program snap promise depends on. The lazy model means the STW pause is short: we write 1 word per instance (klass header CAS for the Fast-proxy swap + version field), not copy field data. Changes: - crochet_jvmti.cpp: new native entry point Java_net_jonbell_crochet_runtime_HeapWalker_iterateAndCheckpoint. Two-phase algorithm: (A) tag all live CRIJInstrumented instances via IterateOverInstancesOfClass (jvmtiHeapObjectCallback), (B) retrieve tagged jobjects via GetObjectsWithTags, call $$crochetCheckpoint(V) on each. Both phases run inside SuspendThreadList..ResumeThreadList STW window so no PUTFIELD can interleave. Adds can_tag_objects capability. VMInitCallback now engages both StackRoots and HeapWalker. - HeapWalker.java: Java-side facade matching StackRoots pattern. Native method iterateAndCheckpoint(int, Class[]); engaged flag; markEngaged() called by VMInitCallback at VM init. - CrochetWorldSafe.java: user-facing API. Three phases: (1) static-field pass via checkpointAll() before STW to minimize pause, (2) STW heap walk via HeapWalker.checkpointWorldSafe(v), (3) stack-root pass (belt-and-suspenders; already done in phase 1). Fallback to checkpointAll with non-suppressible stderr warning when native agent is not loaded. - CheckpointRollbackAgent.java: adds package-private getInstrumentation() accessor for HeapWalker to read INSTRUMENTATION_HANDLE. - Stable.java: new @stable annotation in annotation package. Required by Crochet.java (A.3) which is being built in parallel by other agents; also needed to gate universal quality gate 14 in unit A.4. - HeapWalkerTest.java: 10 unit tests covering engaged-flag semantics, fallback path, version monotonicity, concurrent mutation lifecycle, version-0 instance isolation, and no-exceptions contract. End-to-end validated on stock JDK + javaagent + native agent: HeapWalker engages, checkpointWorldSafe() returns correct odd versions, rollbackAll completes cleanly, concurrent-mutation stress test passes. Soundness sketch at designs/E.1/SOUNDNESS.md documents the STW argument and threats to validity (committed separately as first commit). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… unit/B.3-transformer-cps
…/D.2-crochet-checkpoint
…-crochet-checkpoint
…/D.3-nondet-record-replay
Six amendments applied: 1. §9 rewritten to describe the actual two-phase algorithm (Phase A: tag inside jvmtiHeapObjectCallback; Phase B: CallVoidMethod outside any callback, on the iteration thread, still within the STW window). The original sketch incorrectly implied CallVoidMethod was called from inside the callback, which is undefined behaviour per the JVMTI spec. 2. §6 fabricated "spec (§2.6.5)" citation removed. Replaced with a first-principles argument: HotSpot cannot start a relocating GC while application threads are already stopped by JVMTI (the GC coordinator would deadlock waiting for threads that cannot respond to safepoint polls). Phase A does no JNI allocation; Phase B JNI local refs are tracked by the JNI frame and immune to GC. 3. §7 T7 added: partial SuspendThreadList failure silently voids §1 if the walk proceeds. Documents the hardening implemented in the native (abort + resume + IllegalStateException); records that the best-effort alternative was explicitly rejected. 4. §6 corrected: description now says "IterateOverInstancesOfClass with JVMTI_HEAP_OBJECT_EITHER" (what the code uses) rather than the incorrect "JVMTI_HEAP_FILTER_CLASS_TAGGED" (which belongs to the IterateThroughHeap API we do not use). §9 carries the same correction. 5. §4 v-allocation point clarified: v is allocated inside CheckpointRollbackAgent.checkpointAll() on line ~79 of CrochetWorldSafe.java; the same v is passed to HeapWalker. 6. §3.4 added: documents the double-visit of user klass + Fast-proxy klass by HeapWalker.collectCRIJClasses(), and explains why idempotency of $$crochetCheckpoint makes this safe even under concurrent klass swap. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lure
Previously, Java_net_jonbell_crochet_runtime_HeapWalker_iterateAndCheckpoint
checked only the aggregate SuspendThreadList return code and logged a warning
if it was non-NONE, but continued the heap walk regardless. A thread whose
per-thread suspend_results[i] entry is non-NONE (other than the benign
JVMTI_ERROR_THREAD_SUSPENDED) is still running and can mutate Java fields
concurrently with Phase A or Phase B, silently voiding the §1 STW guarantee.
Fix: iterate suspend_results[i] after SuspendThreadList. If any entry is a
non-benign failure:
- Resume only the threads we successfully suspended (results == NONE).
- Leave threads that reported JVMTI_ERROR_THREAD_SUSPENDED alone (we
didn't suspend them, so we must not resume them).
- Emit a per-entry diagnostic to stderr (thread index, error code, name).
- Throw java.lang.IllegalStateException so the Java caller cannot
silently continue with a degraded snapshot.
The "best-effort continue" alternative was rejected: it would void the §1
guarantee in an error path the caller cannot detect.
JVMTI_ERROR_THREAD_SUSPENDED is treated as benign because it means another
agent or a prior call already suspended the thread — the thread is stopped
and presents no mutation risk.
Native build: make (libcrochet-jvmti.so) confirmed clean.
Java tests: 45/45 pass (mvn -pl crochet-agent test).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forward replay past a CPS-resume point requires the next forward execution to see the same nondeterministic values as the original run; otherwise the user observes inconsistent state across step-forward / step-back / step-forward cycles. This commit implements the mandatory nondet record/replay layer (Decision #5 from PLAN.md). What's added: - NondetTransformer: ClassFileTransformer installed by TtdAgent that rewrites INVOKESTATIC/INVOKEVIRTUAL call sites for System.currentTimeMillis, System.nanoTime, System.identityHashCode, Object.hashCode (static type Object only), Random.next*/nextXxx, and Math.random into NondetRecorder.fetchOrCallXxx(siteId) helpers. Uses COMPUTE_MAXS to handle the extra LDC stack slot. Skips JDK and crochet-ttd packages (call-site rewriting in user code, not in JDK class definitions -- consistent with the existing minimal-pipeline policy). - NondetRecorder: runtime layer with ThreadLocal recording/replay state. Cold path (no session): two ThreadLocal.get() + real JDK call, zero alloc. Recording: appends NondetEvent to a ThreadLocal ArrayList. Replay: dequeues from a siteId-keyed map, emits NondetDivergenceEvent on SITE_ABSENT / QUEUE_EMPTY / WRONG_KIND divergence. - NondetEvent / NondetDivergenceEvent / NondetDivergenceHandler: event types. Divergence schema: (siteId, siteDesc, recordedBits, actualBits, kind, cause). - Repl.installDivergenceHandler / emitDivergence: routes divergence events through the REPL output stream. - TtdAgent: installs NondetTransformer before LineMarkerTransformer. - designs/D.3/DESIGN.md: coverage scope, rewrite-shape decision (fetchOrCall per-method), site-id assignment, storage model, cold-path guarantee, visitor insertion point rationale. - crochet-ttd/docs/nondet-coverage.md: covered + explicitly uncovered nondet sources; @CrochetSkip interaction documented. - Tests: 34 tests across NondetRecorderTest (22), NondetTransformerTest (4), NondetOverheadTest (3), existing TtdLineMarkerTest (2) + TtdSmokeTest (3). All pass. Cold-path overhead: ~0% (measurement noise, well under 5% gate). @CrochetSkip interaction: NondetTransformer is independent of CrochetTransformer; a @CrochetSkip class still has its nondet call sites intercepted by the TTD agent. This is intentional and documented. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The static-field pass in checkpointWorldSafe() runs BEFORE the STW window, not inside it, to keep pause length short. This is sound because static fields are held by sfHelper instances (one CRIJInstrumented object per user class) that are themselves picked up by the STW heap walk — the second $$crochetCheckpoint(V) call on an already-snapped sfHelper is an I3-idempotent no-op (CAS fails, no double-write). Any PUTSTATIC between the static pass and the STW triggers fastAccess on the sfHelper, which copy-on-writes the snap before overwriting — so the snap holds the pre-pass values regardless of the interleaving. This argument is cross-referenced to E.1 SOUNDNESS.md §4 and §7 T6. E.2 also refines the missing-native fallback decision (from E.1): the warning fires at most once per JVM lifetime, not on every call. Documents the scope boundary with E.3 (latency budget) and E.4 (Loom / native-oop scope limits). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t tests
Two changes:
1. CrochetWorldSafe: make the missing-native warning fire at most once per
JVM lifetime (AtomicBoolean FALLBACK_WARNED). Workloads that call
checkpointWorldSafe() in a loop (TTD exploration, fuzz harnesses) were
flooding stderr. The first call still warns; subsequent calls are
silently forwarded to the fallback without re-logging. The semantic
fallback (checkpointAll) is unchanged.
Also adds the ordering explanation in the class-level Javadoc:
the static-field pass BEFORE STW is sound because sfHelper instances
(which hold static field values) are CRIJInstrumented and subsumed by
the heap walk; the double-checkpoint is an I3-idempotent no-op.
2. HeapWalkerTest: 7 new tests covering the E.2 validation matrix:
- staticStateCheckpointedByWorldSafe — snap/mutate/rollback on a
static-field holder (same protocol as checkpointAll's class-level walk)
- mixedStaticAndInstanceStateViaCheckpointWorldSafe — both classes
of state checkpointed and rolled back in one round-trip
- missingNativeWarningEmittedOnlyOnce — captures stderr, verifies the
warning string appears exactly once across two checkpointWorldSafe calls
- fallbackWarnedFlagSetAfterFirstCall — white-box: FALLBACK_WARNED true
- checkpointWorldSafeIsAdditiveWithExistingCheckpointAll — new API
coexists with the existing checkpointAll-based callers
- (two helpers: StaticHolder fixture + the existing lifecycle test
retained with no changes)
Test count: 43 → 50 (all green).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…zation A.1 measurements (commit ef54552, unit/A.1-snap-memory) show 100% top-10-class concentration on every measured workload (W1/H2, W2/H2O, W3/microbench). The dirty-bit eliminates shadow allocation for instances not mutated between consecutive checkpoints — expected savings of 75-95% on idle-object-heavy workloads (e.g. Thread objects that dominate fastAccess in W1/W2). SOUNDNESS.md covers: - Storage option choice (a): new $$crochetDirty field vs bit-in-version-word - I1/I2/I3 preservation arguments - Critical race §7b (noteDirty between gate-check and dirty-set; fastAccess reads dirty==0 and skips shadow): resolved by requiring snap!=null as an additional guard for the skip, so first-checkpoint always allocates - Threats: reflective writes, Unsafe writes (pre-existing gaps, not introduced) - Memory savings argument citing A.1 ef54552 DESIGN.md documents: Option (a) rationale; emit shape for PUTFIELD pre-hook; checkpoint-time skip condition (snap!=null && dirty==0); rollback-time clear; VarHandle for dirty read (acquire semantics under stripe-lock); eager-path dirty-bit optimization deferred per design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements the F.1 dirty-bit optimization. Per A.1 measurement (ef54552), 100% top-10 concentration across all workloads — dirty-bit targets exactly this: skip shadow allocation when an instance has not been mutated since the prior checkpoint. Changes: FieldAdder: emit $$crochetDirty (private transient synthetic int) alongside $$crochetVersion and $$crochetSnap. DIRTY_FIELD constant exported for FieldAccessWrapper to reference without string literal duplication. FieldAccessWrapper: emit INVOKESTATIC CheckpointRollbackAgent.noteDirty(Object) BEFORE emitPreHook() in the PUTFIELD path (both 1-slot and 2-slot cases). This is the load-bearing timing invariant: dirty==1 before any concurrent fastAccess observes the object. Gate is still the existing VERSION_GATE check. CheckpointRollbackAgent: add noteDirty(Object) facade delegating to FastProxySupport.noteDirty. Tolerates null and pre-F.1 classes gracefully. FastProxySupport: - noteDirty(Object): resolves user class, gets VersionHandles.dirty VarHandle, sets dirty=1 via VarHandle.set (plain write; stripe-lock provides ordering). - fastAccess checkpoint branch: reads dirty via VarHandle.getVolatile, checks snap != null condition. Skip shadow only if dirty==0 AND snap!=null (first- checkpoint safety: when snap==null, always allocate to close the §7b race). After allocating shadow, clears dirty via VarHandle.setVolatile under stripe lock. - fastAccess rollback branch: adds VarHandle.setVolatile(obj, 0) to clear dirty after restoring fields. Semantics: post-rollback the instance is in its pre-checkpoint state ("never mutated since snap version V"). ClassMeta.VersionHandles: adds dirty VarHandle field alongside version VarHandle. Resolved via $$crochetLookup() in versionHandles() with graceful fallback (null) for pre-F.1 instrumented classes. fastAccess treats null dirty handle as always-dirty (no optimization, but no crash — safe rollback compatibility). Measured savings (synthetic simulation, see designs/F.1/DESIGN.md): Consecutive checkpoint pattern (idle Thread objects, A.1 W1/W2 analogues): - 0% mutation rate: 75-95% shadow allocation skip - 1% mutation rate: 85-94% skip - 100% mutation (baseline): 0% skip (expected) Rollback-loop pattern: 0% skip (snap cleared by rollback; soundness requirement) All 21 demo scenarios pass. 41/41 pre-existing unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Six tests covering PLAN.md §F.1 validation matrix:
Test 1: checkpoint (eager), no mutation, rollback → fields unchanged.
Verifies eager checkpoint + rollback basic correctness with dirty-bit active.
Test 2: checkpoint (eager), single reflective write, rollback → field reverted.
Verifies snap is taken at eager-checkpoint time (not lazily) and rollback
correctly restores even when dirty-bit was not set by the reflective write
(reflective gap pre-existing per SOUNDNESS.md §7c).
Test 3: N=10,000 mock instances, 1% mutation rate, ≥99% skip rate on V3
checkpoint (consecutive checkpoint, not rollback-based loop). Verifies the
core skip condition (snap!=null && dirty==0) fires at the expected rate.
First and second checkpoints always allocate (snap==null from rollback);
third checkpoint with prior snap+no-mutation achieves ≥99% skip.
Test 4: concurrent noteDirty+checkpoint race (2000 trials). Verifies that
all observable snap values after the race are drawn from valid field-value
history {0, 99} — no impossible values produced.
Test 5: $$crochetDirty field emitted by FieldAdder. Reflectively verifies
the field exists and its VarHandle resolves via ClassMeta.VersionHandles.dirty.
Test 6: logic unit test of snap!=null && dirty==0 skip condition directly.
Total after commit: 47 tests (41 pre-existing + 6 DirtyBitTest), all green.
Demo: 21/21 scenarios pass on baseline and instrumented JDK.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n VI
- Add deprecation banners to CASE_STUDY{,-II,-III}.md pointing to CASE_STUDY-VI.md
- Move prior Haiku/Sonnet × Phase I/II results into archive-pre-VI/ for
side-by-side comparison with the corrected-prompt Phase VI sweep
- Skeleton CASE_STUDY-VI.md with §1-§8 outline (results sections TODO once
sweeps land)
- analyze-phase-vi.py: aggregator that builds Phase VI tables and computes
TTD-mention proxy from agent_log per condition
Note: the prior commit (da163f3) was a chain-script false-success that
deleted results-sonnet-4-6/ from the index; the actual files are preserved
in archive-pre-VI/results-sonnet-4-6/ and are restored by this commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sweep 2 of 4 — re-run after da163f3 false-success bug. Phase I × Sonnet 4.6: 9/11 C1, 9/11 C2, 8/11 C3. 3 timeouts on Lang-10 (all conditions). TTD-mention rate to be computed in CASE_STUDY-VI.md once all sweeps land. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
All four sweeps run cleanly on prompts with {{FIX_SUMMARY}} replaced by
{{TEST_FAILURE_OUTPUT}} (the test's actual stderr+stdout). 138 total
trials across:
- Phase I × Haiku 4.5: 10/11 C1, 11/11 C2, 9/11 C3
- Phase I × Sonnet 4.6: 9/11 C1, 9/11 C2, 8/11 C3
- Phase II × Haiku 4.5: 10/12 C1, 9/12 C2, 7/12 C3
- Phase II × Sonnet 4.6: 1/12 C1, 2/12 C2, 1/12 C3
Headline metric: 0/46 valid C3 trials invoked any TTD command. The
Phase III '0 invocations' finding survives the prompt-leak fix.
Findings:
1. Crochet TTD still doesn't help LLM-agent debugging on D4J after the
leak is removed. C3 ties or underperforms C1 in every cell, never
above.
2. The leak was doing significant lift for Sonnet on hard bugs — Phase
II × Sonnet collapses from 'rate-limit contaminated 3/3 valid'
(Phase III) to 1/12 (Phase VI). Sonnet was relying on the named-
method hint.
3. The leak was doing modest lift for Haiku — 1-2 bugs per Phase I
cell, 0 bugs on Phase II.
4. Per-bug pass rates on Phase II × Haiku unchanged from Phase III
(the fix summaries for multi-file Jsoup/Closure bugs were already
too vague to help the model).
Erratum stamps added to CASE_STUDY.md, CASE_STUDY-II.md,
CASE_STUDY-III.md pointing readers to CASE_STUDY-VI.
The PR is now mergeable with a defensible negative finding. Followup
work (multi-seed replication, Opus rerun, harder-than-D4J corpus like
GitBug-Java) is queued for the user's cluster sweep.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase VI — Re-run Phase I+II with the FIX_SUMMARY prompt leak removed.
Methodology bug found: Phase I/II/III's trial prompts substituted
`{{FIX_SUMMARY}}` (the canonical Defects4J fix description) into
every agent prompt, essentially handing the agent the answer. Phase VI
fixes the prompts and re-runs on Haiku 4.5 + Sonnet 4.6.
Headline: 0/46 valid C3 trials invoked any TTD command. Pass-rate
parity / C3-underperforms hold across both models × both phases. The
negative finding survives the prompt fix.
Per-phase results (corrected):
- Phase I × Haiku 4.5: 10/11 C1, 11/11 C2, 9/11 C3
- Phase I × Sonnet 4.6: 9/11 C1, 9/11 C2, 8/11 C3
- Phase II × Haiku 4.5: 10/12 C1, 9/12 C2, 7/12 C3
- Phase II × Sonnet 4.6: 1/12 C1, 2/12 C2, 1/12 C3
Notable: Phase II × Haiku numbers are IDENTICAL to the Phase III
(leaky) measurement. The fix summary helped Sonnet (which dropped
from 3/3 valid to 1/12 without it) but not Haiku — suggests Sonnet
was relying on the named-method hint that Haiku already derived from
the failure output.
The prior writeups (CASE_STUDY.md, CASE_STUDY-II.md, CASE_STUDY-III.md)
each carry an ERRATUM stamp pointing to CASE_STUDY-VI for the
corrected numbers; their TTD-invocation count (0/N) is unaffected and
remains correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.