diff --git a/BUILT.md b/BUILT.md index 421fd9d..aae38fb 100644 --- a/BUILT.md +++ b/BUILT.md @@ -4872,6 +4872,19 @@ invariant the whole of `flan_dev.c`'s watch section rests on. The cost is that a next runs rather than at the instant of the reset — and for a frame loop that is the only moment it could sensibly begin. The test waits for it rather than reading once, and says why. +**The lazy clear stays lazy, and a stopped program is the case that decides it.** The obvious tidy-up is to make the +reader epoch-aware — compare the slot's epoch with the global one under the seqlock and report an untouched slot as +empty — which would make a reset visible in the very next tick instead of in the tick after the program's next +sample. It is the wrong trade, because **a stopped program does not sample**. An epoch-aware reader would blank the +watch for as long as the program sat in a break loop, and reading the numbers from the moment you stopped is the +entire point of stopping. The lazy clear gives exactly the right answer there. What was actually wrong was narrower +and lives in the editor: `flan-watch--tick` was sending `:reset t` five times a second at a program that could not +answer it. So the reset is now guarded on `flan-dev--stopped` — the read still goes out every tick, only the reset +field drops — and the runtime is untouched. That keeps the policy where the rest of this section already put it: +"since you last looked" is the editor's idea, not the table's. `watch_render_num`'s unreachable `n=0` arm is deleted +rather than commented, since the only way to reach it is the epoch check that was just rejected, and dead code is an +invitation to add one. + **An `i64` accumulates as a double**, so a magnitude past 2^53 loses precision in the sum and in the ends of the range. Recorded rather than designed around: a count, a coordinate and a tile index are what this is pointed at, and a second integer accumulator for a case nobody has would be two code paths for one tool. `watch-num-i64` and diff --git a/HANDOFF-f3.md b/HANDOFF-f3.md deleted file mode 100644 index a1015ab..0000000 --- a/HANDOFF-f3.md +++ /dev/null @@ -1,252 +0,0 @@ -# Handoff — a watch for a running program (`PORTING.md` Tier 1, item 5) - -Written because the session was wound down for budget. **The work is finished and `dune test` is green**, run twice. -This file exists so the reasoning is not re-derived, and so the two things I would have done next are named. - ---- - -## The finding that reframes the task - -**Most of item 5 was already built before this session.** The brief reads as though a watch had to be built from -nothing; it did not. What existed at `344e571`: - -- `runtime/flan_dev.c` — the watch table: 64 slots, a name and rendered text per slot, a per-slot seqlock, an - `on`/`off` flag so a watch call costs a load and a not-taken branch when nobody is looking, and four scalar entry - points (`flan_dev_watch_i64`/`_u64`/`_f64`/`_str`). -- `vendor/agent/flan_agent.c` — `watch`, `watch on`, `watch off` on the agent socket. -- `lib/dev.ml` — `watch_read`, `watch_enable`, and the `:op "watch"` / `:op "watch-enable"` arms. -- `emacs/flan-watch.el` — the watch buffer *and* the inline ghost text, both fed from one reply. -- `test/test_dev.ml` — a block driving a real daemon and a real running program. - -That is `watch.clj`'s `spy`, end to end. **What was missing was `spy-num`** — the numeric accumulator for hot loops, -which is precisely the part `PORTING.md` calls "least obvious and most valuable". That is what this session built. - ---- - -## The accumulator decision - -This is the expensive part to re-derive, so it is stated in full. The long-form version is in `BUILT.md`, "A hot loop -keeps five numbers, and the window is the editor's"; `runtime/flan_dev.c` carries it at the code. - -### What a slot keeps - -**Count, min, max, last, mean.** Five numbers per label. - -The scalar watch keeps one value per name. Sampled from a hot inner loop that is nearly useless — you see whichever -of the 91,200 cells happened to run last. `watch.clj`'s own docstring says so and is why `spy-num` exists there. - -Each of the five answers a question you can ask *without building a query*: - -- **`n`** — how many times the expression actually ran. This is the first thing that is wrong when a loop is wrong. A - count that tracks the frame counter rather than the cell count is a loop that is not running. -- **`min` / `max`** — the range. This is the thing a single sample can never show you, and it is what you are looking - for when you suspect an index or a velocity is leaving the region it should stay in. -- **`last`** — the one sample. Kept because it is what the scalar watch would have given you, and losing it on the - way to something richer would be a regression. -- **`mean`** — carried as a running `sum` and divided at read time. A mean accumulated as a mean drifts; a sum does - not. - -### What it deliberately does not keep - -**A ring, or a history.** A small ring of the last N samples was the serious alternative. It loses on the only ground -that matters here: N samples out of 91,200 is a sample of the *tail* of the loop, not of the loop. It answers "what -did the last few cells do" when the question is "what did the cells do". Past five numbers, every richer answer is a -UI for building a query — and a query builder is the one thing this whole design exists not to be. The ghost text -section of `BUILT.md` already settled the same question the same way for the scalar case ("a watch inside a loop -shows the last value written... every better answer is a UI for building a query. Settled, not open."). - -### The write path does no formatting - -This is the feature, not an optimisation. An `snprintf` per sample at thousands of samples a frame is a HUD that -costs more than the game. A sample is: one relaxed load of the epoch, five compares and stores, and the slot's -seqlock. The **reader** — the agent's listener thread, once per editor tick — turns the five numbers into text. -`watch.clj` reaches the same place with a `double-array` per label and a `render` function Emacs calls. - -This is also why the slot grew a `num` flag rather than getting a second table: the read path already copies under a -seqlock, so it copies five doubles instead of 192 bytes and renders them out of locals *after* the counter check. - -### The window is since the last reset — a deliberate divergence from `watch.clj` - -`watch.clj` is cumulative until `reset-spies!` is called by hand. **This is not**, and the disagreement is on purpose. - -Cumulative is the wrong default for a frame loop. A `min` and a `max` over a whole session reach the session's -extremes within a few seconds of play and then never move again — so the two most useful of the five numbers go dead -*exactly* when you start interacting with the thing you are debugging. This tool exists to show you a number while -you drag the mouse. So `flan-watch--tick` sends `:reset t` beside its read, and what you see is "since you last -looked": a fifth of a second, about a dozen frames, which keeps the range tracking the present. - -A caller who wants cumulative numbers gets them by not resetting. The setting lives in the editor, not the runtime. - -### Reset is its own message, never a side effect of reading - -A destructive read was the tempting shape and is wrong: it makes *looking* change what is there. Anything that polls -— a test's `await`, a second editor, a person reading twice — would silently shorten the window and come back with a -count that is noise. So `watch reset` is its own agent command, `:reset t` is a field on the read op, and `dev.ml` -sends the reset *after* the read so a tick reports the window it just closed. - -This was settled before the test was written rather than after, which is the only reason the test can assert anything -about `n` at all. - -### The reader never writes the table - -Reset bumps one global epoch counter and touches no slot. A slot clears itself on its **next sample**, when it -notices the epoch moved, and does so *inside* its own odd-generation window so a reader can never catch a -half-cleared slot. The game thread stays the only writer of the table — the invariant the whole watch section of -`flan_dev.c` rests on. - -The cost: a new window begins when the program next runs, not at the instant of the reset. For a frame loop that is -the only moment it could sensibly begin. **This bit me in the test** (see "What did not work" below) and the test now -waits for it and says why. - -### `i64` accumulates as a double - -A magnitude past 2^53 loses precision in the sum and in the ends of the range. Recorded rather than designed around: a -count, a coordinate and a tile index are what this is pointed at, and a second integer accumulator for a case nobody -has would be two code paths for one tool. `watch-num-i64` and `watch-num-f64` are two entry points only so a program -need not cast at the call site. - -### A whole number prints as one - -`watch.clj`'s `fmt-num` does this and the reason survives the port: a watch on an array index that reads `66.0000` -sends you looking for a rounding bug that is not there. - ---- - -## What was built, file by file - -All of it is **working** — built, run against a real daemon and a real running program, and covered by the test. -Nothing is stubbed and nothing is half-written. - -| File | Change | State | -|---|---|---| -| `runtime/flan_dev.c` | `watch_slot` grew `num`, `epoch` and five doubles; `watch_begin` clears `num`; new section "A number sampled thousands of times a frame" with `watch_record`, `flan_dev_watch_reset`, `flan_dev_watch_num_i64`, `flan_dev_watch_num_f64`, `watch_num_str`, `watch_render_num`; `flan_dev_watch_read` renders a num slot instead of copying its (unused) text | working | -| `vendor/agent/flan_agent.c` | `flan_dev_watch_reset` declaration; a `watch reset` command beside `watch on`/`watch off` | working | -| `lib/dev.ml` | `watch_read` takes `~reset` and sends `watch reset` *after* the read; the `:op "watch"` arm reads a `:reset` field the same way `watch-enable` reads `:on` | working | -| `emacs/flan-watch.el` | `flan-watch-ghost-call-regexp` `?` → `*`; docstring updated; `flan-watch--tick` sends `(:op "watch" :reset t)` | working | -| `test/programs/dev-watch.flan` | `declare-c watch-num-i64`; a `loop-cells` hot loop sampling `"cell"` eight times a step at 0,3,…,21 | working | -| `test/test_dev.ml` | assertions inside the existing watch block: the row exists, `n > 8` (per-sample not per-step), `min < max` (a range, not just `last`), a plain read does not reset, `:reset t` reopens the window | working | -| `BUILT.md` | new section "A hot loop keeps five numbers, and the window is the editor's", after the ghost text one | working | -| `PORTING.md` | item 5 struck in house style with the accumulator decision | working | -| `NEXT.md` | "What is left on `PORTING.md`'s list" corrected: item 5 done, item 6 is next | working | - -### Verified end to end, not just compiled - -Against a real `flan dev` daemon and the real running program, the `"cell"` row read back: - -``` -n=184 min=0 max=21 last=21 mean=10.5000 (first read) -n=368 min=0 max=21 last=21 mean=10.5000 (second read — a plain read does not reset) -n=16 min=0 max=21 last=21 mean=10.5000 (after :reset t — the window reopened) -``` - -The loop is 0,3,…,21, so `min`, `max`, `mean` and the per-step count of 8 are each pinned by a different part of it. - -### Test result - -`dune test` is **green**, run twice in full. `test_dev.ml`'s first block — the separately-known-flaky socket bind race -— failed on one earlier run with "the daemon never listened" and passed on both final runs. That is exactly the -documented flake and another agent is on it; it is not related to anything here. - ---- - -## The ghost text path — the brief's assumption, checked - -The brief said to reuse the ghost text path rather than invent a second one. **It fits, and it needed one character.** - -The important property is in `BUILT.md`: ghost text anchors on the **string literal in the source**, not on a source -location carried in the table. `flan-watch--ghost-sites` scans buffers shown in a window for -`( ""` and matches the name against the rows. So a new *kind* of watch needs nothing new in the daemon, -nothing new in the wire, and no new display path — the accumulator's rendered value is a string like -`n=368 min=0 max=21 last=21 mean=10.5` and flows through the existing `:watch` rows untouched. - -**Is it pull-based?** No, and the brief's phrasing is worth correcting for whoever reads this next. The watch is -**push-based** and that is load-bearing: `watch.clj` is pull (Emacs polls a render function), and `flan_dev.c`'s -comment explains at length why that does not port — an evaluation here compiles a module and `dlopen`s it, so a 5Hz -poll would be hundreds of `.so` files a minute. So the program pushes into a table and the editor reads *memory*. -Ghost text is not a poller at all: it is painted from `flan-watch--absorb`, off the same single reply that paints the -buffer, which is what keeps the two from disagreeing and preserves the one-request-in-flight invariant. The -accumulator sits inside that unchanged. - -**The one thing that did not fit.** `flan-watch-ghost-call-regexp` was `watch\(?:-[[:alnum:]]+\)?` — *one* optional -hyphenated segment. `[[:alnum:]]` does not match a hyphen, so on `(watch-num-i64 "cell" ...` the regexp matched -`watch-num`, then required whitespace, found `-i64`, and backtracked to failure. A numeric watch therefore appeared -normally in the watch buffer and got **no inline value at all** — a silent half-failure. `*` for `?` is the whole -fix, verified in `emacs --batch` against all four head shapes. - -That is worth recording as the predictable cost of the decision the ghost text section defends. Anchoring on the name -rather than on the head is what makes the head a `defcustom`, and a `defcustom` with an enumerated default is a -default that needs widening whenever the set of heads grows. Cheaper than the alternative, but not free. - ---- - -## What remains - -Item 5 is complete, and so is the fixture the previous list asked for: `emacs/test-flan-watch.el` now has a block -"Every head the default regexp claims to cover" pinning `watch-num-i64` and `watch-num-f64` beside the one-segment -heads. It was verified by mutation — restoring the old `?\=` in `flan-watch-ghost-call-regexp` makes exactly those -two checks fail and nothing else — so a regression in that regexp is now caught. - -One item is left, and it turned into a finding rather than a test. - -1. **`watch_render_num`'s `n=0` branch is unreachable, so the test that was asked for cannot be written.** - `runtime/flan_dev.c:764` renders `n=0 last=` for a window with no samples. Nothing can present that state to - the reader: - - - `s->num` is set to 1 in exactly one place, `watch_record` (flan_dev.c:701), and that function always falls - through to `s->n += 1`. The clear (`s->n = 0` on an epoch change) and the increment are both inside one - odd-generation window, so a reader cannot see between them. - - `flan_dev_watch_begin` sets `s->num = 0` and never touches `s->n`, so the scalar path cannot leave a num slot - at zero either. - - `flan_dev_watch_read` never compares `s->epoch` to `watch_epoch`. A reset therefore does not make the slot read - as empty. - - The user-visible consequence is worth stating on its own, because it is the thing the branch was presumably - written for: **after `:reset t`, a read taken before the program's next sample reports the *previous* window's - five numbers, not an empty one.** That is why `test_dev.ml`'s reset assertion has to `await` the count dropping - rather than read once — the test is already working around this. - - Two ways out, and both are decisions rather than test work, which is why neither was taken here (the lane was - scoped to tests): - - - **Delete the branch.** Honest about what the design says: a window begins when the program next runs, and until - then the last window is what there is to show. - - **Make the read epoch-aware** — compare `s->epoch` with `watch_epoch` under the seqlock and render `n=0` when - they differ. This makes a reset visible immediately without the reader writing the table, and would let the - existing `await` in the test become a single read. It is a behaviour change to what an editor sees in the tick - after a reset, so it belongs to whoever owns `runtime/flan_dev.c`, not to a test pass. - -Item 6 (frame rollback as a worked example) is the next real piece of work and `NEXT.md` points at it. - ---- - -## What I tried that did not work - -- **Two Flan type errors in `dev-watch.flan`.** `(* i 3)` over a `let`-bound `0` is `i32`, and `watch-num-i64` wants - `i64` — fixed with an explicit `(i64 …)`. Then `loop-cells` was declared `i64` and returned the `i32` counter; - declared `i32`. Both are ordinary, but they cost two daemon round trips to find because a build error goes to the - daemon's stdout, not to the client — worth knowing if you are driving `flan dev` by hand. - -- **My first probe script spoke the wrong protocol.** `Wire.send` is **length-prefixed** (`"%d\n%s"`), not - newline-delimited. A newline-delimited client gets the connection closed with no error text. If you write a - throwaway client, read `lib/wire.ml:32` first. - -- **Two attempts at the reset assertion failed before the third stuck**, and both failures were informative rather - than noise: - 1. *"the window did not reopen: 8 after 8"* — one step writes eight samples, so two reads back to back leave no - room *under* the count for a reset to show in. The test now runs the accumulator up past 32 first (which is - itself the other half of the claim: every one of those polls is a plain read, and a plain read must not reset, - or the count could never climb). - 2. *"the window did not reopen: 40 after 40"* — reading immediately after `:reset t` still sees the old count, - because the reset bumps an epoch and the **slot clears on its next sample**. That is the design and not a bug; - the test now `await`s the drop and the comment explains why the laziness is what keeps the reader out of the - table. - -- **Not attempted, deliberately:** the `(watch "hp" hp)` arm in `check.ml`. Both `PORTING.md` and `flan_dev.c` say - it is only wanted for *composites* (a struct, a slice, a union), which need `Render.render` pointed at the - `flan_dev_watch_emit_*` emitters. A scalar and an accumulator both reach the runtime by plain `declare-c` and need - no compiler change, which is the property that kept this change out of `check.ml` entirely. - -- **One worktree note.** This worktree was checked out at `2c232dd`, an ancient commit, rather than at `dev-loop`'s - tip — none of `PORTING.md`, `NEXT.md`, `lib/dev.ml` or `runtime/` existed in it. `git reset --hard dev-loop` on a - clean tree fixed it. Also, `dune` finds the parent repo's root from inside a worktree, so every build here is - `dune build --root .` / `dune test --root .`. diff --git a/NEXT.md b/NEXT.md index 818e2c5..dc01a8f 100644 --- a/NEXT.md +++ b/NEXT.md @@ -279,7 +279,7 @@ wait a minute now, bounded by the watchdog, with a named `listening` helper in ` re-derive from `HANDOFF-f1.md` is its "lengthening any timeout" bullet — that applies to the `agent.sock` check only, and reading it as a rule is what kept this open. -### From `HANDOFF-f3.md` — the watch is built; two tests are not +### ~~From `HANDOFF-f3.md` — the watch is built; two tests are not~~ `spy-num` landed and `PORTING.md` Tier 1 item 5 is closed. The `spy` half already existed — the finding that reframed the task is that the table, the agent commands, the buffer and the ghost text were all there at `344e571`, @@ -291,12 +291,12 @@ writes the table. The fixture is in: `emacs/test-flan-watch.el` pins both accumulator heads beside the one-segment ones, verified by putting the old `?` back in `flan-watch-ghost-call-regexp` and watching exactly those two checks fail. -1. **`watch_render_num`'s `n=0` branch is dead, and the choice is whose it is to remove.** No test can reach it: - `s->num` is set only in `watch_record`, which always falls through to `s->n += 1` inside one odd-generation - window, and the reader never compares the slot's epoch to the global one. The live consequence is that a read - after `:reset t` and before the program's next sample reports the *previous* window — which is why the test waits - for the count to drop instead of reading once. Either delete the branch or make the read epoch-aware; both are - `runtime/flan_dev.c` decisions. `HANDOFF-f3.md` has the argument. +**Closed.** The dead `n=0` branch in `watch_render_num` is deleted and the reader stays epoch-blind on purpose: a +stopped program does not sample, so an epoch-aware reader would blank the watch for the whole of a break loop, which +is exactly when the numbers are being read. What was actually wrong was in the editor — `flan-watch--tick` sent +`:reset t` five times a second at a program that could not answer it — and it is now guarded on `flan-dev--stopped`, +with the read still going out every tick. `emacs/test-flan-watch.el` pins both halves. `HANDOFF-f3.md` is deleted; +the reasoning is in `BUILT.md`, "A hot loop keeps five numbers, and the window is the editor's". ### Not started: five more raylib core examples diff --git a/emacs/flan-watch.el b/emacs/flan-watch.el index 029d7db..e3ccd97 100644 --- a/emacs/flan-watch.el +++ b/emacs/flan-watch.el @@ -395,7 +395,23 @@ sends the next question, leaving at most one request in flight — the invariant ;; show a number while you drag the mouse. The reset happens after ;; the read, on the daemon's side, so this tick's numbers are the ;; last tick's window and nothing is lost between the two. - (progn (flan-dev--send flan-dev--connection '(:op "watch" :reset t)) + ;; + ;; Except while the program is stopped, when the read still happens + ;; and the reset does not. A stopped program takes no samples, so + ;; there is no window for a reset to close and none for it to open: + ;; the runtime clears a slot lazily, on its next sample, which is + ;; what keeps a paused program showing the numbers from the moment + ;; you paused it — the whole reason to pause. Resetting anyway + ;; would ask for a new window five times a second that nothing can + ;; fill. The guard lives here rather than in the runtime because + ;; "since you last looked" is the editor's policy, not the table's. + ;; `flan-dev--stopped' is the one place that state is tracked, and + ;; `flan-dev.el''s background poll keeps it current whether or not + ;; anyone is evaluating. + (progn (flan-dev--send flan-dev--connection + (if flan-dev--stopped + '(:op "watch") + '(:op "watch" :reset t))) (setq flan-watch--pending t)) (error (flan-watch-stop))))))) diff --git a/emacs/test-flan-watch.el b/emacs/test-flan-watch.el index 5b33c68..f2e18a9 100644 --- a/emacs/test-flan-watch.el +++ b/emacs/test-flan-watch.el @@ -210,5 +210,54 @@ what bounds its cost; a `with-temp-buffer' would be scanned by nothing." (test-flan--check "and the last consumer out disarms the table" (and (null flan-watch--consumers) torn)))) +;; --- The reset is guarded on the stop ------------------------------------ +;; +;; `flan-watch--tick' asks for "since you last looked" by sending `:reset t' +;; beside the read. A stopped program takes no samples, so there is no window +;; for a reset to close and none for it to open — and the runtime's lazy clear +;; is what keeps a paused program showing the numbers from the moment you +;; paused it. So the tick must drop `:reset' while stopped and keep reading. +;; +;; Asserted at the level the rest of this file works at: no daemon, no socket. +;; The tick is a function from `flan-dev--stopped' to the form it puts on the +;; wire, and that is the whole claim, so `flan-dev--send' and `process-live-p' +;; are stubs. What this cannot reach is the daemon actually honouring the +;; absent field; `test/test_dev.ml' drives a real program for that. + +(defun test-flan-watch--tick-form (stopped) + "The form `flan-watch--tick' sends with the program STOPPED or not." + (let ((sent nil) + (flan-dev--stopped stopped) + (flan-dev--connection 'a-process) + (flan-dev--busy nil) + (flan-watch--pending nil) + ;; Not `buffer': that consumer checks for a live watch buffer first and + ;; would drop the subscription instead of ticking. + (flan-watch--consumers '(ghost))) + (cl-letf (((symbol-function 'process-live-p) (lambda (_) t)) + ((symbol-function 'flan-dev--take-reply) (lambda (_) nil)) + ((symbol-function 'flan-dev--send) + (lambda (_proc form) (setq sent form)))) + (flan-watch--tick) + (list sent flan-watch--pending)))) + +(let ((running (test-flan-watch--tick-form nil)) + (stopped (test-flan-watch--tick-form "BoundsError"))) + (test-flan--check + "a tick while the program runs asks for the window it is closing" + (equal (nth 0 running) '(:op "watch" :reset t))) + (test-flan--check + "a tick while the program is stopped sends no reset" + (null (plist-get (nth 0 stopped) :reset))) + ;; Skipping the tick outright would be worse than resetting: the watch would + ;; freeze at whatever it held when the program stopped, and a break loop is + ;; exactly when the numbers are being read. + (test-flan--check + "but it still reads the table" + (equal (plist-get (nth 0 stopped) :op) "watch")) + (test-flan--check + "and still has a reply in flight, so the cycle survives the pause" + (and (nth 1 running) (nth 1 stopped)))) + (provide 'test-flan-watch) ;;; test-flan-watch.el ends here diff --git a/runtime/flan_dev.c b/runtime/flan_dev.c index 3c439ea..4a7ddb3 100644 --- a/runtime/flan_dev.c +++ b/runtime/flan_dev.c @@ -755,26 +755,35 @@ static void watch_num_str(char *out, size_t cap, double d) { * slot, so nothing here races and nothing here runs per sample. Comfortably * inside [WATCH_VAL]: five numbers at %.4f and their labels is well under 192 * bytes, and [snprintf] truncates rather than overruns if a caller's buffer is - * smaller than [flan_dev_watch_val_cap]. */ + * smaller than [flan_dev_watch_val_cap]. + * + * **[n] is at least 1 whenever this runs**, so there is no empty-window arm + * and [sum / n] cannot divide by zero. A slot only becomes a num slot inside + * [watch_record], which always falls through to [s->n += 1], and the clear on + * an epoch change sits in the same odd-generation window as that increment, so + * no reader can catch a num slot at zero. + * + * There used to be an "n=0 last=..." arm here, for the window a reset opens + * before the program's next sample. It is deleted rather than kept, because + * the only way to reach it is to compare [s->epoch] with [watch_epoch] in + * [flan_dev_watch_read] — and that is the one thing the reader must not do. A + * stopped program does not sample. An epoch-aware reader would blank the watch + * for as long as the program stayed stopped, and looking at the numbers from + * the moment you stopped is the whole point of stopping. The lazy clear is + * right there; its cost is one stale tick in a *running* program, and the + * editor keeps even that honest by not sending [:reset] while the program is + * stopped. See [flan-watch--tick]. */ static uint64_t watch_render_num(double n, double lo, double hi, double last, double sum, char *vd, uint64_t vcap) { if (vcap == 0) return 0; char b[4][40]; int k; - if (n == 0) { - /* No sample since the window opened. The range and the mean have nothing - * behind them and showing the previous window's would be a lie, but [last] - * is still the last value this name ever had, so it is kept. */ - watch_num_str(b[0], sizeof b[0], last); - k = snprintf(vd, (size_t)vcap, "n=0 last=%s", b[0]); - } else { - watch_num_str(b[0], sizeof b[0], lo); - watch_num_str(b[1], sizeof b[1], hi); - watch_num_str(b[2], sizeof b[2], last); - watch_num_str(b[3], sizeof b[3], sum / n); - k = snprintf(vd, (size_t)vcap, "n=%lld min=%s max=%s last=%s mean=%s", - (long long)n, b[0], b[1], b[2], b[3]); - } + watch_num_str(b[0], sizeof b[0], lo); + watch_num_str(b[1], sizeof b[1], hi); + watch_num_str(b[2], sizeof b[2], last); + watch_num_str(b[3], sizeof b[3], sum / n); + k = snprintf(vd, (size_t)vcap, "n=%lld min=%s max=%s last=%s mean=%s", + (long long)n, b[0], b[1], b[2], b[3]); if (k < 0) return 0; if ((uint64_t)k > vcap - 1) return vcap - 1; return (uint64_t)k;