Files
meshcore-analyzer/internal/perfio/perfio.go
T
Kpa-clawbot f4cf2acbc0 perf: cancelled writes + ingestor I/O + threshold tests (#1120 follow-up) (#1167)
Red commit: e964ec9c46 (CI run: pending —
workflow only triggers on PR open)

Partial fix for #1120 — finishes the four follow-up items left open
after PR #1123 (cancelled writes, ingestor I/O, threshold-flag tests,
docs).

## What's done

- **`cancelledWriteBytesPerSec`** — server `/proc/self/io` parser
handles `cancelled_write_bytes`; `/api/perf/io` exposes the per-second
rate; Perf page renders it next to Read/Write with ⚠️ when sustained >1
MB/s.
- **Ingestor `/proc/<pid>/io`** — `cmd/ingestor/stats_file.go` samples
its own `/proc/self/io` each tick and includes `procIO` in the snapshot.
The server's `/api/perf/io` reads it and surfaces `.ingestor`. Frontend
renders an `Ingestor process` Disk I/O block alongside the existing
`server process` block (issue mockup: "Both ingestor and server").
- **Threshold + anomaly tests** — `test-perf-disk-io-1120.js` now
asserts ⚠️ fires/suppresses on WAL>100MB, cache_hit<90%, and the
backfill-rate-vs-tx-rate guard with the `tx_inserted >= 100` baseline
floor. Drops the tautological `|| ... === false` short-circuits flagged
in MINOR m4.
- **Docs (m8)** — `config.example.json` adds `_comment_ingestorStats`
(env var, default path, shared-tmp security note);
`cmd/ingestor/README.md` adds `CORESCOPE_INGESTOR_STATS` to the env-var
table plus a `Stats file` section.

## What's NOT done (deferred)

m1 sync.Map → map+RWMutex, m2 perfIOMu rate caching, m3 negative
cacheSize translation, m5 deterministic-write test, m7 ctx-aware
shutdown — pure polish; will file a follow-up issue if the operator
wants them tracked.

## TDD

- Red: `e964ec9` — adds failing tests + stub field/handler shape
(cancelled missing from struct, ingestor stub returns nil, ingestor
procIO absent).
- Green: `1240703` — wires up the parser case, ingestor sampler,
frontend rendering, docs.

E2E assertion added: test-perf-disk-io-1120.js:108

---------

Co-authored-by: clawbot <clawbot@users.noreply.github.com>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot.local>
Co-authored-by: Kpa-clawbot <bot@kpa-clawbot>
2026-05-08 16:29:23 -07:00

80 lines
2.7 KiB
Go

// Package perfio holds the canonical PerfIOSample type shared between the
// ingestor (which publishes /proc/self/io rate samples to its on-disk stats
// file) and the server (which reads that file and surfaces the sample under
// /api/perf/io's `ingestor` block). Sharing the type prevents silent JSON
// contract drift if a field is added on one side only.
//
// The /proc/self/io key:value parser also lives here (Carmack #1167
// must-fix #7) so the two binaries don't carry divergent copies of the
// same parser — past divergence already produced a real bug (see must-fix
// #6: the parsedAny empty-key gate was added on one side only).
package perfio
import (
"bufio"
"strconv"
"strings"
)
// Sample is the per-process I/O rate sample written by the ingestor and
// consumed by the server. Field names + json tags MUST be considered the
// stable on-disk contract — adding/renaming a field is a breaking change.
type Sample struct {
ReadBytesPerSec float64 `json:"readBytesPerSec"`
WriteBytesPerSec float64 `json:"writeBytesPerSec"`
CancelledWriteBytesPerSec float64 `json:"cancelledWriteBytesPerSec"`
SyscallsRead float64 `json:"syscallsRead"`
SyscallsWrite float64 `json:"syscallsWrite"`
SampledAt string `json:"sampledAt,omitempty"`
}
// Counters is the raw /proc/self/io counter snapshot. Both the ingestor's
// procIOSnapshot and the server's procIOSample are thin wrappers around
// these fields plus a sampled-at timestamp; the parser populates Counters
// directly so there's exactly ONE implementation of the key:value walker.
type Counters struct {
ReadBytes int64
WriteBytes int64
CancelledWriteBytes int64
SyscR int64
SyscW int64
}
// ParseProcIO reads /proc/self/io-shaped key:value lines from sc and
// populates c. Returns true iff at least one recognised key was
// successfully parsed (Carmack must-fix #6 — empty / no-known-keys input
// must NOT be treated as a valid sample, otherwise the next tick computes
// a phantom delta against zero counters).
func ParseProcIO(sc *bufio.Scanner, c *Counters) bool {
parsedAny := false
for sc.Scan() {
parts := strings.SplitN(sc.Text(), ":", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val, err := strconv.ParseInt(strings.TrimSpace(parts[1]), 10, 64)
if err != nil {
continue
}
switch key {
case "read_bytes":
c.ReadBytes = val
parsedAny = true
case "write_bytes":
c.WriteBytes = val
parsedAny = true
case "cancelled_write_bytes":
c.CancelledWriteBytes = val
parsedAny = true
case "syscr":
c.SyscR = val
parsedAny = true
case "syscw":
c.SyscW = val
parsedAny = true
}
}
return parsedAny
}