jevsnes.git / apps / hotdemo / CLAUDE.md

For agents, on top of README.md, which they read first.

  • The flags on this target (rustc_flags, linker_flags in BUCK) are the whole point of the crate - do not "clean them up". Removing -Cdebug-assertions=yes makes subsecond::call a no-op silently (it still compiles and runs, it just never patches); removing -Csave-temps=true removes the .rcgu.o objects tools/hotpatch links into the patch.
  • -Cdebug-assertions=yes on THIS target is not enough by itself - subsecond::call/try_call are cfg!(debug_assertions)-gated (subsecond/src/lib.rs:250-254, :411-414), and cfg!() resolves against whichever crate's OWN compilation the macro invocation lives in, not the downstream consumer's. Under cargo, one profile setting covers the whole dependency graph; under buck2/reindeer, //third-party/rust:subsecond is its OWN independent rustc invocation, built at the toolchain's default (no debug-assertions) unless told otherwise. That's what third-party/rust/fixups/subsecond/fixups.toml's rustc_flags = ["-Cdebug-assertions=yes"] is for. Measured 2026-09-20: without it, apply_patch returns Ok and "patch applied" prints, but nm on the patched binary shows zero calls to subsecond's own get_jump_table from anywhere in main - the whole redirect was silently inert.
  • subsecond::call needs a bare fn item, not a closure that captures anything - tick takes no arguments and reads a static AtomicU64 instead of a closure-captured n, and main calls subsecond::call(tick) directly, never subsecond::call(|| tick(n)). Two independent reasons, both measured 2026-09-20, discovered together as apparent inlining but actually two separate causes:
    1. HotFn::try_call's redirect key depends on size_of::<F>() (subsecond/src/lib.rs:420 on): a closure whose captured environment happens to be exactly pointer-width (one u64, one reference, …) takes the "treat this value as a bare fn pointer" branch (call_as_ptr/transmute_copy::<Self, Self::Real>), which is only correct for an ACTUAL bare fn pointer - for a real capturing closure of that size it transmutes captured DATA as if it were a function address.
    2. At this project's forced -Copt-level=3 (toolchains/BUCK), rustc's default cross-codegen-unit ThinLTO (visible as the *.rcgu.thin-lto-*.bc files next to the .rcgu.os) inlines a small, single-call-site function into its caller. subsecond's crate carries no #[inline(never)] anywhere (it's written assuming a dev-profile-style build where little gets inlined); without #[inline(never)] on tick itself, tick's own body - and the whole subsecond::call/HotFn/HotFunction::call_it chain around it - collapsed into one block inside main with no addressable symbol left to redirect a call to. If either fix is missing, apply_patch still succeeds and prints nothing wrong - the log just keeps printing the old string forever. That silence is what makes both traps worth restating here rather than only in the research doc.
  • tick's string literal is the thing this crate exists to let you change without restarting the process. Editing anything else in main.rs is a bigger, unproven patch (research/subsecond-patch-build.md's workspace-replay path, §2/§5) - keep edits here to that one function while proving the pipeline.
  • TICKS resets to 0 on every patch, on purpose - it's part of the tip crate's own recompiled objects, not carried over from an unchanged dependency crate, so subsecond's "globals are tracked" claim (subsecond/src/lib.rs's module doc) doesn't apply to it. Don't read this as a bug to fix; it's the same limitation as the thread-local one that doc already names.
  • Never run this outside the devshell; like every other target here it needs the pinned fenix rustc, not whatever rustc is on the host PATH.