Skip to content

#92: render every counter, and make truncation visible when it happens - #95

Merged
mgrossmann merged 2 commits into
mainfrom
m5-92-stats-truncation
Aug 31, 2026
Merged

#92: render every counter, and make truncation visible when it happens#95
mgrossmann merged 2 commits into
mainfrom
m5-92-stats-truncation

Conversation

@mgrossmann

@mgrossmann mgrossmann commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

#92 — the STATS renderer truncated silently, and the host ring dropped its tail

Two problems in two layers, fixed separately and gated separately. A prerequisite for
(e), which measures throughput by reading counters.

Red lines held: no counter removed or renumbered (verified in the diff), anchor layout
unmoved, ANCVERNO 3, NSFRQE frozen, asm/nsfvsvc.asm untouched.


Problem 1 — F NSFS,STATS rendered a third of the registry and said nothing

Mechanism, from source. op_stats (src/nsfopr.c) put one sts_render into one
char buf[512]. sts_render breaks at the first line that would not fit and returns a
byte count — so "rendered everything" and "stopped a third of the way in" are the same
observation
to the caller. Each line is "%.8s %.12s %u\n", and it carries the counter's
value
, so the cut point moves as values gain digits.

Measured live on the deployed module, before any change:

NSF810I STATS 52 COUNTER(S)
   ... 33 NSF811I lines ...          <- 19 counters missing, nothing said so

The 19 that were invisible are not a random tail — they are the entire UDP counter set
plus most TCP diagnostics: NSFUDP in/out/binds/noport/badcksum/badlen/rxfull, NSFTCP rexmit / twreclaim / wndprobe / oooseg / dupack / rxfull / datadrop / …, and NSFSX wakeposts. Earlier rounds that quoted those figures read them through sts_value inside the
tests, not through this report, so those results stand — but (e) reads the report.

The fix, two halves.

  1. Completests_render_from(buf, bufsize, first, *next) renders from an index and
    reports where it got to; op_stats resumes until every counter is out. The buffer now
    bounds one chunk, not the whole reply.
  2. Visibleop_stats counts what it emitted against sts_count() and emits
    NSF818W STATS INCOMPLETE -- RENDERED n OF m if they ever differ. This is the durable
    half: without it the next counter added past some future boundary is lost in the same
    silence.

nsfsx_stats_extra stays as a mechanism but is no longer the reason a counter is placed
outside the registry — it was a workaround for this, it worked, and it was not a fix.

Problem 2 — the host capture ring dropped its tail (host-only)

Mechanism, from source. nsfmsg_emit stores only while g_capn < CAP_MAX but always
increments, so it retains the first 64 lines and drops the rest — the newer ones, not
the older ones as the old comment claimed. F NSFS,APPS at a full registry emits 1 + 64 + 1
= 66 lines, so the last slot and the NSF816I summary were gone, and
nsfmsg_cap_line() returned NULL for them — which reads as "no such line", not "dropped".

Fix: CAP_MAX 64 → 256, and nsfmsg_cap_dropped(), because a bigger number alone is
the same defect at a new threshold. The overflow is now countable, so a test asserts it is
zero instead of inferring completeness from a line count it cannot distinguish from a short
reply.


Verified host-side

  • make test-host 3342 → 3414 PASS / 0 FAIL (TSTOPR 34 → 113).
  • Discriminating by construction, and proven: with 52 counters registered at
    ten-digit values, reverting op_stats alone gives
    FAIL: EVERY counter rendered, at ten-digit values (got 22, want 52).
    That 22 also measures the moving boundary — ~33 at live (small) values, 22 at
    ten-digit ones. A fix that worked at small values and failed at large ones would be the
    same defect at a new threshold; this is the case that catches it.
  • The ring gate discriminates too: reverting CAP_MAX alone gives
    dropped (got 2, want 0) plus the two missing lines — exactly the last slot and the
    NSF816I summary.
  • NSF818W is proven to FIRE, not merely present. It is unreachable in production (a
    line is ~33 bytes, the chunk 512), so the chunk size became an NSF_DEBUG-only settable —
    because a warning nobody has seen fire is not designed in, it is asserted, which is
    this issue's own shape one level up. The test shrinks the chunk, sees
    NSF818W ... RENDERED 0 OF 52, restores it, and sees the warning gone and all 52 render.
  • And the debug proof transfers to production, stated rather than left to the reader
    an unstated step is an assumption, which is the same class as the defect being fixed.
    op_stats, its resume loop, the emitted-vs-total comparison and the NSF818W call are
    all outside any NSF_DEBUG guard; the only thing the guard changes is whether
    nsfopr_set_stats_chunk exists to change g_statschunk, which production keeps at
    OPR_STATS_CHUNK. The test forces the branch by varying one constant, not by running
    different code.
  • Cross-build clean (6 modules + 53 test modules); alias scan 244 unique, all ≤ 8
    (NSFSTRNF, NSFOPSCH, NSFMSGCD new).
  • The cross-build caught a real error mid-work: the gate called NSF_DEBUG-only helpers from
    outside the guard — 4 unresolved externals, invisible to the host build.

Verified live (MVSCE)

Before/after on the same stand, one assertion moving:

NSF810I header NSF811I lines NSF818W last counter
before (deployed main) STATS 52 33 absent NSFTCP resetsent
after (fixed) STATS 52 52 absent NSFSX wakeposts

NSF817I APPSWEEP still comes last, so the supplement's ordering is unchanged.

round result
NSFV — TSTSVC/TSTMVCK/TSTUBUF/TSTXFW/TSTDEATH 438 PASS / 0 FAIL, CC 0 batch+TSO
NSFS — TSTRQXC/TSTRQXF 122 PASS / 0 FAIL, CC 0 batch+TSO

Zero dumps; both STCs start and stop clean; stand left with nothing running.

Raw console captures: docs/measurements/m5-92/stats-before.log, stats-after.log.

Two defects in two layers, both losing output silently, and both a
prerequisite for the throughput round that reads counters.

The operator STATS reply put one render into one 512-byte buffer and
stopped at the last line that fitted, returning a byte count -- so
rendering everything and stopping a third of the way in were the same
observation to the caller. Each line carries its counter's value, so the
cut point moved as the numbers grew. Measured on the deployed module: a
header announcing 52 counters followed by 33 lines, and the missing 19
were the entire UDP set plus most TCP diagnostics.

The renderer is now resumable and the caller loops until every counter is
out, so the buffer bounds a chunk rather than the whole reply. That alone
would have been a fix with the same shape as the bug, so the caller also
counts what it emitted against what exists and says so if they differ.
That warning cannot occur in production, which is why the chunk size
became settable in debug builds: a warning nobody has seen fire is not
designed in, it is asserted, and that is this issue one level up.

The host capture ring kept its first sixty-four lines and dropped the
rest -- the newer ones, not the older ones its comment claimed -- so a
full registry report lost its last slot and its summary, and the reader
could not tell a dropped line from one that was never emitted. Raising
the bound alone would be the same defect at a new threshold, so the
overflow is now countable and a test asserts it is zero.

Both gates are verified to discriminate by reverting each fix alone, and
the counter gate is driven at ten-digit values because a fix that works
at small values and fails at large ones has not fixed anything.
The warning is forced in a debug build by shrinking the render chunk, and
it has to work in the production build. If the only difference is the
chunk constant then the proof carries, but that was a step the reader had
to make, and an unstated step is an assumption -- the same class as the
defect being fixed here.

So it is now said at the test and in the PR: op_stats, its resume loop,
the emitted-against-total comparison and the warning itself are all
outside any debug guard. The guard changes only whether a setter exists
to alter the chunk size, which production leaves at its default. The test
forces the branch by varying one constant, not by running different
code.
@mgrossmann
mgrossmann merged commit 58dfaab into main Aug 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant