PORTING.md Tier 1 item 5. The spy half of the watch was already built — the pushed table, the buffer, the inline ghost text. What was missing is spy-num, which is the part that item calls least obvious and most valuable, and it is what this is. A slot keeps count, min, max, last and mean. Each answers a question you can ask without building a query: n is the first thing wrong when a loop is wrong, the range is what one sample can never show you, last is what the scalar watch would have given you, and the mean is a running sum divided at read time because a mean accumulated as a mean drifts. A small ring of the last N samples was the other candidate and loses — N out of 91,200 is a sample of the tail of the loop rather than of the loop, and past five numbers every richer answer is a UI for building a query. The write path does no formatting, which is the feature rather than an optimisation: a snprintf per sample at thousands a frame is a HUD that costs more than the game. A sample is a load, five compares and the slot's seqlock; the listener thread renders once per editor tick. The window is since the editor's last tick, and that is a deliberate divergence from watch.clj, where the stats are cumulative until reset-spies!. Cumulative min and max reach the session's extremes within seconds of play and then never move again, so the two most useful of the five go dead exactly when you start interacting with the thing you are debugging — and this tool exists to show you a number while you drag the mouse. Reset is its own message and never a side effect of reading, because a destructive read makes looking change what is there and anything that polls would shorten the window under the editor that owns it. It bumps one epoch counter and clears no slot; a slot clears itself on its next sample, so the reader never writes the table. Ghost text needed one character. The call regexp allowed one hyphenated segment, so watch-num-i64 backtracked to failure and a numeric watch got no inline value while appearing normally in the buffer. dune test is green, run twice. HANDOFF-f3.md carries the reasoning, the two small gaps left behind it, and what did not work on the way.
55 lines
2.4 KiB
Plaintext
55 lines
2.4 KiB
Plaintext
;;;; A program that pushes values into the watch table from its own loop.
|
|
;;;;
|
|
;;;; The point of the case is that this needs no compiler change: the watch
|
|
;;;; entry points are ordinary C functions, so a program reaches them through
|
|
;;;; [declare-c] the same way it reaches anything else in the runtime. That is
|
|
;;;; deliberate — a [(watch "hp" hp)] form would be an arm in the checker, and
|
|
;;;; a scalar does not need one.
|
|
;;;;
|
|
;;;; The values are written every iteration and are *not* read back from here.
|
|
;;;; What reads them is the daemon's [watch] op, over the agent, while this
|
|
;;;; program is still running — which is the whole design: the program pushes
|
|
;;;; at frame rate and the editor reads memory.
|
|
(import agent "vendor:agent")
|
|
|
|
(declare-c watch-i64 [name string x i64] i32 "flan_dev_watch_i64")
|
|
(declare-c watch-f64 [name string x f64] i32 "flan_dev_watch_f64")
|
|
(declare-c watch-str [name string s string] i32 "flan_dev_watch_str")
|
|
;; The accumulator, for a value sampled from inside a hot loop. A scalar watch
|
|
;; there shows whichever iteration happened to run last, which is the case this
|
|
;; exists for; see flan_dev.c, "A number sampled thousands of times a frame".
|
|
(declare-c watch-num-i64 [name string x i64] i32 "flan_dev_watch_num_i64")
|
|
|
|
(defvar ticks i64)
|
|
|
|
(defn loop-cells [] i32
|
|
(let [i 0]
|
|
(while (< i 8)
|
|
(watch-num-i64 "cell" (i64 (* i 3)))
|
|
(set i (+ i 1)))
|
|
i))
|
|
|
|
(defn step [] i64
|
|
(set ticks (+ ticks 1))
|
|
;; Three types, because the table stores *rendered text* and the rendering
|
|
;; is per type: an i64 and an f64 do not print the same way, and a string is
|
|
;; quoted and escaped so that a newline in one cannot become a second row.
|
|
(watch-i64 "ticks" ticks)
|
|
(watch-f64 "half" (/ (f64 ticks) 2.0))
|
|
(watch-str "label" "sand")
|
|
;; A hot inner loop, and the value *varies* across it — which is what makes
|
|
;; the row a test of the accumulator rather than of the plumbing. A slot that
|
|
;; only kept [last] would report 21 and no range; n, min and max are each
|
|
;; pinned by a different part of this loop.
|
|
(loop-cells)
|
|
ticks)
|
|
|
|
(defn main [] i32
|
|
(agent/start "/tmp/flan-dev-watch-fallback.sock")
|
|
;; A watch call costs a load and a not-taken branch until somebody opens a
|
|
;; watch buffer, so spinning here writes nothing until the test arms it.
|
|
(while (= (agent/wait 20) 0) (step))
|
|
(step)
|
|
(while (= (agent/wait 20) 0) (step))
|
|
0)
|