Commit 5ba3d7c
adapter/statsclient: read ring buffers incrementally
A ring-buffer stat could only be read whole. CopyEntryData copies every
thread's entire ring into a fresh allocation, so the cost of a read is the
size of the ring rather than the number of entries produced into it - which
is the wrong way round. A ring is sized for burst headroom, so making it big
enough to survive a stall makes it too expensive to poll: an 8192-entry ring
of 128-byte records is 1 MiB copied and allocated per thread per read, and at
the 10 ms cadence a fast producer needs, on eight workers, that is 800 MiB/s
of copying and garbage to deliver a few hundred entries. A 16M-entry ring is
2 GiB per thread per read and cannot be polled at all.
adapter.RingBufferWindowStat copies only what the producers appended since
the previous refresh, into buffers it already owns. On an 8192-entry ring
delivering 256 entries:
RingBufferStat 483 us, 1 MiB, 5 allocations
RingBufferWindowStat 4.1 us, none
118x, and the ratio grows with the ring: ring size no longer appears in the
cost of a read.
The read cursor is consumer-side state, a field on the RingBufferWindowStat the
caller holds rather than anything in the segment, which is mapped read-only. So
a window refreshes through UpdateDir like any other prepared entry - the switch
in UpdateEntryData already dispatches on the stat's dynamic type. CopyEntryData
deliberately never produces one: windowing needs a cursor carried between calls,
and only the consumer has anywhere to keep it.
Entries are located from the sequence and not from head. The two are
congruent - both start at zero and advance together on every commit - but a
producer stores head plainly and then publishes the sequence with a release
store, so a consumer reading the pair astride a commit sees a head one slot
ahead of the sequence that explains it. Positioning off head then delivers
the slot a worker is writing and skips the oldest live entry, silently and
only under load. The sequence is the only field of the two that carries an
ordering guarantee, and it is also the one that publishes the entry bytes.
A copy is validated against the producer afterwards. The segment's optimistic
lock covers directory changes, not ring data, so a worker is free to
overwrite the slots being copied while the copy runs - most easily in the
configuration this is for, a large window on a fast producer. Re-reading the
sequence after the copy identifies the entries that were overwritten
underneath it; they are the oldest of the window, so dropping them from the
front leaves exactly the ones that are still whole, and they are counted as
lost. Delivering a record that is half one entry and half another while
reporting no loss would be worse than losing it, because loss is visible and
tearing is not.
Lost and Pending are separate. Lost counts entries the producer overwrote
before the reader reached them, which are gone; Pending counts entries still
in the ring that MaxEntries held back, which the next read delivers. They
call for opposite responses - read again now, against the reader is not
keeping up - so a single "missed" figure would tell a rate-limited reader it
was losing data.
PrepareRingBuffer exists because PrepareDir cannot express this: it populates
every entry by way of CopyEntryData, so preparing a ring copies it once even
if every later read is windowed, and that copy repeats on every epoch change.
It is reached through adapter.RingBufferAPI rather than StatsAPI, because
serving a window means carrying that cursor between refreshes and a mock or a
v1 segment has nowhere to keep one; callers type-assert for it.
The three region bounds checks are now in one helper that both the windowed
and the whole-ring path use. They are what stands between a racing header and
an out-of-bounds read of the mapped segment, so a second copy of them is a
second chance to get one subtly wrong; go vet's unsafe.Pointer warnings for
this file drop from three to none as a side effect.
Tested against the synthetic v2 segment: first-refresh positioning with and
without SkipBacklog, delivering only what is new, unwrapping across the
ring's seam, exact loss on a lap, the cap and the drain loop that empties it,
re-sync on a sequence rewind, per-thread independence, a head published ahead
of its sequence, entries overwritten while the copy runs, and that a steady
refresh allocates nothing. The last two are reachable only by racing a real
producer, so an unexported hook between the copy and its validation stands in
for the worker; it is nil in production and costs one nil check per thread
per refresh.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent dcc9074 commit 5ba3d7c
4 files changed
Lines changed: 993 additions & 72 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
188 | 188 | | |
189 | 189 | | |
190 | 190 | | |
191 | | - | |
192 | | - | |
193 | | - | |
194 | | - | |
195 | | - | |
196 | | - | |
197 | | - | |
198 | | - | |
199 | | - | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
200 | 296 | | |
201 | 297 | | |
202 | 298 | | |
| |||
352 | 448 | | |
353 | 449 | | |
354 | 450 | | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
355 | 470 | | |
356 | 471 | | |
357 | 472 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
769 | 769 | | |
770 | 770 | | |
771 | 771 | | |
| 772 | + | |
| 773 | + | |
| 774 | + | |
| 775 | + | |
| 776 | + | |
| 777 | + | |
| 778 | + | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
| 803 | + | |
| 804 | + | |
| 805 | + | |
| 806 | + | |
| 807 | + | |
| 808 | + | |
| 809 | + | |
| 810 | + | |
| 811 | + | |
| 812 | + | |
| 813 | + | |
| 814 | + | |
| 815 | + | |
| 816 | + | |
| 817 | + | |
| 818 | + | |
| 819 | + | |
| 820 | + | |
| 821 | + | |
| 822 | + | |
| 823 | + | |
| 824 | + | |
| 825 | + | |
| 826 | + | |
| 827 | + | |
| 828 | + | |
| 829 | + | |
| 830 | + | |
| 831 | + | |
| 832 | + | |
| 833 | + | |
| 834 | + | |
| 835 | + | |
| 836 | + | |
| 837 | + | |
| 838 | + | |
| 839 | + | |
| 840 | + | |
| 841 | + | |
| 842 | + | |
| 843 | + | |
| 844 | + | |
| 845 | + | |
0 commit comments