mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-04 21:01:23 +00:00
f4cf2acbc0
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>
80 lines
2.7 KiB
Go
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
|
|
}
|