debugger: a cheaper attached run #43

Closed
opened 2026-10-05 21:07:13 +00:00 by orion · 8 comments
Collaborator

Context

An attached run (torb debug, no breakpoint set) costs two to three and a half times the baseline (DEBUGGER.md section 19): reachStatement (compiler/src/vm/debug.trb) writes a fresh LoopRecord into Debugger.loops at every statement, counts down to the channel look, and tests Debugger.breakpoints (a Set<Int> of code offsets) at every statement. Two changes from DEBUGGER.md section 20 make that cheaper:

  1. The record of a loop written only where a call back begins (and at a stop), not at every statement.
  2. Breakpoints as a bit per statement of the chunk instead of a set of code offsets.

The debugged conformance gate that was part of this issue is split out as #290 (independent: that run has no debugger attached).

Source

  • DEBUGGER.md section 20, sections 7 to 10 (breakpoints, frame height, pause, frames) and 19 (what it costs; those numbers are from one Windows machine under load).

Where the changes go

  • compiler/src/vm/debug.trb: reachStatement, LoopRecord, beginLoop/finishLoop, Debugger.breakpoints, framesOf, beginStep.
  • compiler/src/vm/interpret.trb: the .Statement arm / atStatement, execute (where beginLoop runs), and every site that starts a nested execute (native calls, witness calls, destructors, task resume): the outer loop's position (chunk, pc, base, depth, statement) must be published there.
  • compiler/src/debugger/debuggee.trb (applyBreakpoints), and compiler/src/backend/bytecode/format.trb only if the bit table lives on Chunk.
  • Docs: DEBUGGER.md sections 7, 8, 19, 20.

Seed impact

None expected: ordinary TorbScript in compiler/src/vm and compiler/src/debugger; no syntax change, no new native, no driver flag, no std name looked up by string. If the implementation turns out to need one (for example a new Machine native for bit operations), the two-commit seed rule of CLAUDE.md applies and the item is re-planned before it is pushed; plain Int arithmetic or a Bool/byte table needs none.

Plan (one PR, two Conventional Commits)

  1. test(debugger): measure the attached run in CI - tools/debug-cost.sh plus programs and one session under benchmarks/debug/ (recursive fibonacci(32), a loops program, print "hello"; same three as section 19): per program, best and median of 7 interleaved runs of four kinds - torb run --vm, TORB_VM_DEBUGGED=1 torb run --vm, and torb debug fed a session that launches the program, sets no breakpoint and lets it run to its end. An informational step at the end of tier B (after the lanes, when the machine is quiet), it never fails. Push this commit alone first and read the "before" numbers from its forge CI log; they are the baseline for this PR (the Windows numbers of section 19 are not).
  2. perf(vm): write a loop's record only where a call back begins, and test breakpoints by a bit per statement - both changes, the new tests, and the docs; read the "after" numbers from the CI log of this commit.

Decision rule (decided by the loop, to be applied with the numbers of step 1 and 2)

The change is kept when the attached fibonacci run is at least 10% faster (best of 7, same CI runner, same job) after than before. If the combined gain is below that, the code change is reverted, the measurement and its numbers stay, and section 19/20 of the record say that the cheaper run was measured and is not worth its complexity (the open point is then answered, and this issue closes with that). The size is no argument against either option.

Acceptance criteria

  • Attached-run overhead measured before and after in forge CI (both logs linked in the PR), and recorded in DEBUGGER.md section 19 next to the older numbers, with the decision rule's outcome.
  • A statement of an attached run writes no LoopRecord; the record of an execute is current at every point a nested execute can begin and at every stop (breakpoint, step, pause, entry, panic).
  • Breakpoints are tested by a table indexed by chunk and statement, not by a Set<Int> of code offsets; setBreakpoints replacing a file's breakpoints clears the old ones; a breakpoint moved to the next line with a statement still reports verified and line as before.
  • Behaviour is unchanged: sh tools/debug.sh (all existing sessions under tests/debug/) and compiler/tests/debugger.test.trb pass with their .expected files byte for byte.
  • torb run and torb test (not debugged) are not slower: no change to the loop's shape beyond atStatement; the hello/fibonacci "not debugged" numbers of the new script show no change beyond noise (reported, not a gate).

Proving tests

  • New session tests/debug/callback.dap (+ .expected, tests/debug/callback/main.trb): a breakpoint inside a closure that list.map runs (a call back), stackTrace shows the closure frame, then map's frame and the caller's with the right lines, stepOut returns to the caller's next line, stepIn into a closure called from a native, and a panic inside a call back stops with the same frames (panicked hook).
  • New tests in compiler/tests/debugger.test.trb: breakpoints at the first and last statement of a chunk and in several chunks at once; replaced breakpoint sets leave no stale bit; a breakpoint on a line shared by a closure and its caller stops in both; stopOnEntry and a pause while an execute of a call back is running show the frames of every loop below.
  • The existing sessions breakpoint, panic, tests unchanged.
  • The measurement itself: the CI log of the first commit and of the second.

Model

opus / high (compiler/src/vm hot loop, frame records, runtime interplay).

Dependencies

None known. The open PR #289 (feat(debugger): name the frames of test and group bodies) touches the same files (vm/debug.trb, debugger/debuggee.trb); the implementer rebases onto it when it lands first.

## Context An attached run (`torb debug`, no breakpoint set) costs two to three and a half times the baseline ([DEBUGGER.md section 19](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/DEBUGGER.md#L383)): `reachStatement` (`compiler/src/vm/debug.trb`) writes a fresh `LoopRecord` into `Debugger.loops` at **every** statement, counts down to the channel look, and tests `Debugger.breakpoints` (a `Set<Int>` of code offsets) at every statement. Two changes from DEBUGGER.md section 20 make that cheaper: 1. **The record of a loop written only where a call back begins** (and at a stop), not at every statement. 2. **Breakpoints as a bit per statement of the chunk** instead of a set of code offsets. The debugged conformance gate that was part of this issue is split out as #290 (independent: that run has no debugger attached). ## Source - [DEBUGGER.md section 20](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/DEBUGGER.md#L417), sections 7 to 10 (breakpoints, frame height, pause, frames) and 19 (what it costs; those numbers are from one Windows machine under load). ## Where the changes go - `compiler/src/vm/debug.trb`: `reachStatement`, `LoopRecord`, `beginLoop`/`finishLoop`, `Debugger.breakpoints`, `framesOf`, `beginStep`. - `compiler/src/vm/interpret.trb`: the `.Statement` arm / `atStatement`, `execute` (where `beginLoop` runs), and every site that starts a nested `execute` (native calls, witness calls, destructors, task resume): the outer loop's position (chunk, pc, base, depth, statement) must be published there. - `compiler/src/debugger/debuggee.trb` (`applyBreakpoints`), and `compiler/src/backend/bytecode/format.trb` only if the bit table lives on `Chunk`. - Docs: DEBUGGER.md sections 7, 8, 19, 20. ## Seed impact None expected: ordinary TorbScript in `compiler/src/vm` and `compiler/src/debugger`; no syntax change, no new native, no driver flag, no std name looked up by string. If the implementation turns out to need one (for example a new `Machine` native for bit operations), the two-commit seed rule of `CLAUDE.md` applies and the item is re-planned before it is pushed; plain `Int` arithmetic or a `Bool`/byte table needs none. ## Plan (one PR, two Conventional Commits) 1. `test(debugger): measure the attached run in CI` - `tools/debug-cost.sh` plus programs and one session under `benchmarks/debug/` (recursive `fibonacci(32)`, a loops program, `print "hello"`; same three as section 19): per program, best and median of 7 interleaved runs of four kinds - `torb run --vm`, `TORB_VM_DEBUGGED=1 torb run --vm`, and `torb debug` fed a session that launches the program, sets no breakpoint and lets it run to its end. An informational step at the end of tier B (after the lanes, when the machine is quiet), it never fails. Push this commit alone first and read the "before" numbers from its forge CI log; they are the baseline for this PR (the Windows numbers of section 19 are not). 2. `perf(vm): write a loop's record only where a call back begins, and test breakpoints by a bit per statement` - both changes, the new tests, and the docs; read the "after" numbers from the CI log of this commit. ## Decision rule (decided by the loop, to be applied with the numbers of step 1 and 2) The change is kept when the attached fibonacci run is at least 10% faster (best of 7, same CI runner, same job) after than before. If the combined gain is below that, the code change is reverted, the measurement and its numbers stay, and section 19/20 of the record say that the cheaper run was measured and is not worth its complexity (the open point is then answered, and this issue closes with that). The size is no argument against either option. ## Acceptance criteria - [ ] Attached-run overhead measured before and after in forge CI (both logs linked in the PR), and recorded in DEBUGGER.md section 19 next to the older numbers, with the decision rule's outcome. - [ ] A statement of an attached run writes no `LoopRecord`; the record of an `execute` is current at every point a nested `execute` can begin and at every stop (breakpoint, step, pause, entry, panic). - [ ] Breakpoints are tested by a table indexed by chunk and statement, not by a `Set<Int>` of code offsets; `setBreakpoints` replacing a file's breakpoints clears the old ones; a breakpoint moved to the next line with a statement still reports `verified` and `line` as before. - [ ] Behaviour is unchanged: `sh tools/debug.sh` (all existing sessions under `tests/debug/`) and `compiler/tests/debugger.test.trb` pass with their `.expected` files byte for byte. - [ ] `torb run` and `torb test` (not debugged) are not slower: no change to the loop's shape beyond `atStatement`; the `hello`/`fibonacci` "not debugged" numbers of the new script show no change beyond noise (reported, not a gate). ## Proving tests - New session `tests/debug/callback.dap` (+ `.expected`, `tests/debug/callback/main.trb`): a breakpoint inside a closure that `list.map` runs (a call back), `stackTrace` shows the closure frame, then `map`'s frame and the caller's with the right lines, `stepOut` returns to the caller's next line, `stepIn` into a closure called from a native, and a panic inside a call back stops with the same frames (`panicked` hook). - New tests in `compiler/tests/debugger.test.trb`: breakpoints at the first and last statement of a chunk and in several chunks at once; replaced breakpoint sets leave no stale bit; a breakpoint on a line shared by a closure and its caller stops in both; `stopOnEntry` and a pause while an `execute` of a call back is running show the frames of every loop below. - The existing sessions `breakpoint`, `panic`, `tests` unchanged. - The measurement itself: the CI log of the first commit and of the second. ## Model opus / high (`compiler/src/vm` hot loop, frame records, runtime interplay). ## Dependencies None known. The open PR #289 (`feat(debugger): name the frames of test and group bodies`) touches the same files (`vm/debug.trb`, `debugger/debuggee.trb`); the implementer rebases onto it when it lands first.
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop), source DEBUGGER.md section 20, not a duplicate, no collision with a design record. Two parts: an attached-run overhead measured in forge CI first (decided with the numbers, no owner question), then the debugged conformance run as a tier B gate. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z

[loop:torbscript] intake: in scope (author orion, label loop), source DEBUGGER.md section 20, not a duplicate, no collision with a design record. Two parts: an attached-run overhead measured in forge CI first (decided with the numbers, no owner question), then the debugged conformance run as a tier B gate. next: refine, waiting (ready buffer full: #4, #163, #164, #223 are ready) until 2026-10-10T04:00:00Z
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261010 until 2026-10-10T00:38Z

[loop:torbscript] claim refine by session:loop-torbscript-20261010 until 2026-10-10T00:38Z
orion changed title from debugger: a cheaper attached run and the debugged conformance gate to debugger: a cheaper attached run 2026-10-09 22:41:29 +00:00
Author
Collaborator

[loop:torbscript] decided by the loop: (1) Split. The two halves of this issue are independent: the cheaper attached run touches the VM's reachStatement and the breakpoint table (opus/high), while the debugged conformance run is lowered for the debugger with no debugger attached (TORB_VM_DEBUGGED=1), so it does not profit from the attached-run work; it is tooling and docs (sonnet/medium). The gate is now #290; this issue keeps the attached run. (2) Measurement: the attached run is not measured by any existing CI output (CI never runs torb debug on a program that runs long, and the only numbers are the owner machine's of DEBUGGER.md section 19), so it is the first implementation step, in forge CI, with a keep-or-revert rule at 10% (see body). (3) For #290 the existing CI output is enough for a plan: the last main run (https://git.torb.dev/torbscript/language/actions/runs/710, tier B) shows conformance suite: 324s, conformance suite in the VM: 75s, runtime tests: 73s, tests/language and examples/config-dsl: 74s, tier B: green (507s); lane b1 (both conformance legs) takes 399 s, lane b2 147 s. A debugged leg costs the plain VM leg plus 12% to 35% of the programs' run time (not of their compile time), so about 85 to 100 s; placed in lane b2 it does not lengthen tier B. The exact figure is still measured on the first commit of #290. (4) Seed: no impact in either issue.

[loop:torbscript] decided by the loop: (1) Split. The two halves of this issue are independent: the cheaper attached run touches the VM's `reachStatement` and the breakpoint table (opus/high), while the debugged conformance run is lowered for the debugger with no debugger attached (`TORB_VM_DEBUGGED=1`), so it does not profit from the attached-run work; it is tooling and docs (sonnet/medium). The gate is now #290; this issue keeps the attached run. (2) Measurement: the attached run is not measured by any existing CI output (CI never runs `torb debug` on a program that runs long, and the only numbers are the owner machine's of DEBUGGER.md section 19), so it is the first implementation step, in forge CI, with a keep-or-revert rule at 10% (see body). (3) For #290 the existing CI output is enough for a plan: the last main run (https://git.torb.dev/torbscript/language/actions/runs/710, tier B) shows `conformance suite: 324s`, `conformance suite in the VM: 75s`, `runtime tests: 73s`, `tests/language and examples/config-dsl: 74s`, `tier B: green (507s)`; lane b1 (both conformance legs) takes 399 s, lane b2 147 s. A debugged leg costs the plain VM leg plus 12% to 35% of the programs' run time (not of their compile time), so about 85 to 100 s; placed in lane b2 it does not lengthen tier B. The exact figure is still measured on the first commit of #290. (4) Seed: no impact in either issue.
Author
Collaborator

[loop:torbscript] refine: ready, #43 is now "a cheaper attached run" (the gate is split out as #290, ready too). Two changes in one PR of two commits: a loop's record written only where a call back begins and at a stop, breakpoints as a table indexed by chunk and statement. Commit 1 test(debugger) adds tools/debug-cost.sh and prints the attached-run cost in forge CI (before numbers); commit 2 perf(vm) makes the change, adds tests and docs (after numbers); kept only if the attached fibonacci run gains at least 10%, else reverted and the numbers recorded. Acceptance: overhead measured before and after in CI and in DEBUGGER.md section 19; no per-statement LoopRecord; breakpoint table with replace semantics; tools/debug.sh and compiler/tests/debugger.test.trb unchanged; new session tests/debug/callback and new unit tests for chunk edges, call back frames and shared lines. Seed impact none. Model opus/high. next: implement

[loop:torbscript] refine: ready, #43 is now "a cheaper attached run" (the gate is split out as #290, ready too). Two changes in one PR of two commits: a loop's record written only where a call back begins and at a stop, breakpoints as a table indexed by chunk and statement. Commit 1 `test(debugger)` adds `tools/debug-cost.sh` and prints the attached-run cost in forge CI (before numbers); commit 2 `perf(vm)` makes the change, adds tests and docs (after numbers); kept only if the attached fibonacci run gains at least 10%, else reverted and the numbers recorded. Acceptance: overhead measured before and after in CI and in DEBUGGER.md section 19; no per-statement `LoopRecord`; breakpoint table with replace semantics; `tools/debug.sh` and `compiler/tests/debugger.test.trb` unchanged; new session `tests/debug/callback` and new unit tests for chunk edges, call back frames and shared lines. Seed impact none. Model opus/high. next: implement
Author
Collaborator

[loop:torbscript] implement: deferred until PR #289 lands (shared debugger code, debuggee.trb). next: waiting (PR #289 merge)

[loop:torbscript] implement: deferred until PR #289 lands (shared debugger code, debuggee.trb). next: waiting (PR #289 merge)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T07:00Z (PR #289, the dependency named in comment 3002, landed)

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T07:00Z (PR #289, the dependency named in comment 3002, landed)
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T14:02Z (re-claim: the previous worker died in the gateway gap between 02:10Z and 08:00Z without a write-back)

[loop:torbscript] claim implement by session:loop-torbscript-20261010 until 2026-10-10T14:02Z (re-claim: the previous worker died in the gateway gap between 02:10Z and 08:00Z without a write-back)
Author
Collaborator

[loop:torbscript] implement: PR #298 (head 6ffd6cc5, mergeable, two commits, numbers in the PR body); the resumed worker was killed by a gateway restart after the push, so this write-back comes from the loop session. CI is running on the head, argus requested. next: waiting (review by argus; if CI fails, the next tick re-claims for a fix)

[loop:torbscript] implement: PR https://git.torb.dev/torbscript/language/pulls/298 (head 6ffd6cc5, mergeable, two commits, numbers in the PR body); the resumed worker was killed by a gateway restart after the push, so this write-back comes from the loop session. CI is running on the head, argus requested. next: waiting (review by argus; if CI fails, the next tick re-claims for a fix)
argus closed this issue 2026-10-10 09:59:49 +00:00
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#43
No description provided.