Skip to content

Show how much moved, not only how fast - #36

Closed
evandhoffman wants to merge 4 commits into
mainfrom
window-totals
Closed

Show how much moved, not only how fast#36
evandhoffman wants to merge 4 commits into
mainfrom
window-totals

Conversation

@evandhoffman

Copy link
Copy Markdown
Contributor

Closes #35.

A second number in every legend entry: how much moved over the window the
chart is drawing, beside how fast it is moving now. Network and disk in
bytes, packets and disk operations as counts.

┌─ Network ─── 2 min ──────────────────┐
│  ● In     245 Mbit/s        1.4 GB   │
│  ● Out     12.4 Mbit/s       88 MB   │
│ ▁▂▅█▆▃▂▁▁▂▄▇█▅▃▂▁▁▂▃▅▆█▄▂▁▁▂▃▄▅▃▂▁   │
└──────────────────────────────────────┘

The sum is already in the buffer

A rate sample is the mean over the gap before it — RateTracker produces
Δcounter / Δt — so Σ(rate × preceding gap) telescopes back to exactly the
counter delta. WindowTotal is that sum. No counter history is kept, no
source changes, and nothing is read that the ring buffer does not already
hold, so this stays well inside the rule that nothing is written to disk.

The round-trip test is the whole premise: build a synthetic counter,
differentiate it with RateTracker, integrate it back, expect the original
delta. Deliberately uneven intervals, so it cannot pass by multiplying by a
constant it guessed right.

Three things it refuses to fake

  • It reports the span it covered. Ten seconds after launch the buffer
    holds ten seconds, and "2 min" over a twelfth of that looks exactly like the
    right answer — which is why nobody would check it. The span sits beside the
    title and reads the window only when the card really has it.
  • A gap nobody sampled is not traffic. An interval wider than
    maximumGap is clipped and the remainder comes off covered, so a laptop
    back from sleep cannot credit one sample with an hour that never crossed the
    wire. AppModel.totalGap is four ticks of the master clock, so it follows
    the Sampling tab rather than assuming 0.5 s.
  • Fewer than two samples has no total. Nil, not zero — the same answer
    RateTracker gives on a first read, for the same reason.

Network totals in bytes

The rate stays Mbit/s because a link is quoted in bits. A volume is quoted in
bytes everywhere it matters. MetricUnit.accumulation holds the ÷8 as its
only definition, the way NetworkSource holds the ×8 — derived from the unit
like direction and composition, so a source added later gets a total
without anything being told it exists.

The one thing worth arguing about

showsTotals is off by default, following mirrorsPairs and
stacksParts. The precedent's reason is that those change what the picture
means; this only adds a number. The narrower reason it still applies: it
adds one to a header already decided by ViewThatFits, so switching it on
reflows cards onto two lines — a card changing shape on upgrade. Happy to
ship it on instead and say so in the release notes.

Non-goals, each deliberate

No totals row in the CSV (a spreadsheet's SUM × interval is this same
arithmetic, and a "Total" row under the Time column is a type error waiting to
be pasted into a chart). No odometer tile — considered, and it is very much
this app's idiom, but it costs panel space and a third tile kind in
PanelArrangement. No monitorctl totals; no watt-hours; nothing past ten
minutes until the store lands.

Verification

swift build && swift test (205 tests), swift build -c release, and
swiftformat --lint all clean. Not yet looked at in swift run monitor — the
open question is whether a legend entry carrying two numbers still reads at a
glance, and no test can answer that.

https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj

A rate sample is the mean over the gap before it, so multiplying each
sample by the interval it measures and summing telescopes back to
exactly the counter delta. No counter history is kept, no source
changes, and nothing is read that the ring buffer does not hold.

WindowTotal reports the span it actually covered alongside the value.
Ten seconds after launch the buffer holds ten seconds, and putting
"2 min" under a number covering a twelfth of that is the quiet kind of
wrong. A gap wider than maximumGap is clipped rather than credited in
full, so a slept laptop cannot invent traffic.

MetricUnit.accumulation says what a rate adds up to, derived from the
unit the way ChartMirror and ChartStack read direction and composition.
Network totals in bytes and the divide by eight lives there: a link is
quoted in bits, a volume in bytes.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
A second number per legend entry: how much moved, beside how fast it is
moving. The span sits once beside the title rather than after every
entry, since the History picker is global and repeating it down a legend
is four copies of one fact.

The span reads the window only when the card really has it. Ten seconds
after launch it says ten seconds, because "2 min" over a twelfth of that
looks exactly like the right answer and nobody would check it. An entry
covering materially less than the card's stated span is dimmed — one
source failed while its neighbour kept reading.

AppModel.totalGap is four ticks of the master clock, so it follows the
Sampling tab. A ceiling fixed for the 0.5 s default would clip every
interval of a sampler slowed to 2 s and quietly report a quarter of the
traffic.

Off by default, like mirroring and stacking. Narrower reason: those
change what the picture means and this only adds a number, but it adds
one to a header already decided by ViewThatFits.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
@evandhoffman evandhoffman added enhancement New feature or request release:minor Merging this bumps the minor version labels Aug 28, 2026
The argument for matching mirroring and stacking lost on first contact:
shipped off, the reaction to the finished feature was "I don't see it".

Those two change what the picture means, so a reader who never asked for
them deserves the chart they had. A total only adds a number beside one
already there, and a number nobody can find is worth less than a header
that reflows.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
Wrapped entries put the totals at four different left edges down the same
card. A card with totals now draws a small Grid instead of a FlowLayout:
swatch, name, rate, total.

Right-justified, because these are magnitudes and a magnitude is read by
where its last digit sits — 104 MB over 48 MB aligned on the left puts
the hundreds above the tens and hides the difference the column exists to
show.

Headed, because a second bare number beside a rate does not say what it
is. The span beside the title says how long; the heading says of what.

Under the title, never beside it. ViewThatFits is the right question for
a wrapping row and the wrong one for a column of figures: pushed right of
the title on a wide card, the heading floats in the middle of the header
with nothing under it that reads as a table.

Cards with nothing to total keep the flow — Memory's seven slices and
five temperature sensors want wrapping, not a column.

Claude-Session: https://claude.ai/code/session_01EqTTtmt4fyNtMxjc2ZVBzj
@evandhoffman

Copy link
Copy Markdown
Contributor Author

Folded into #38, which carries these commits unchanged. Splitting this off bought nothing: the stack was only ever mergeable in order, so it was three squash-merges and three releases for one change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request release:minor Merging this bumps the minor version

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Show how much moved, not only how fast: cumulative totals over the window

1 participant