Files
meshcore-analyzer/cmd/server/perf_io_followup_test.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

107 lines
3.3 KiB
Go

package main
import (
"bufio"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
// TestParseProcIO_CancelledWriteBytes verifies the parser populates
// cancelled_write_bytes from a synthetic /proc/self/io string. Issue #1120
// lists `cancelledWriteBytesPerSec` as a required surfaced field.
func TestParseProcIO_CancelledWriteBytes(t *testing.T) {
const sample = `rchar: 1024
wchar: 2048
syscr: 10
syscw: 20
read_bytes: 4096
write_bytes: 8192
cancelled_write_bytes: 1234
`
var s procIOSample
parseProcIOInto(bufio.NewScanner(strings.NewReader(sample)), &s)
if s.cancelledWrite != 1234 {
t.Errorf("expected cancelledWrite=1234, got %d", s.cancelledWrite)
}
if s.readBytes != 4096 {
t.Errorf("expected readBytes=4096, got %d", s.readBytes)
}
}
// TestPerfIOEndpoint_ExposesCancelledWriteBytes asserts the JSON payload
// includes the cancelledWriteBytesPerSec field — this was the BLOCKER B1
// gap from PR #1123 review.
func TestPerfIOEndpoint_ExposesCancelledWriteBytes(t *testing.T) {
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/perf/io", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Fatalf("expected 200, got %d", w.Code)
}
var body map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
if _, ok := body["cancelledWriteBytesPerSec"]; !ok {
t.Errorf("missing field cancelledWriteBytesPerSec; got: %v", body)
}
}
// TestPerfIOEndpoint_ExposesIngestorBlock writes a stub ingestor stats file
// containing a procIO block and asserts /api/perf/io surfaces it under
// `ingestor`. Issue #1120: "Both ingestor and server."
func TestPerfIOEndpoint_ExposesIngestorBlock(t *testing.T) {
dir := t.TempDir()
statsPath := filepath.Join(dir, "ingestor-stats.json")
// Use a fresh sampledAt — the GREEN commit added a freshness guard
// (#1167 must-fix #1) that drops snapshots older than ~5s. A fixed
// date string would now incorrectly exercise the stale path.
freshAt := time.Now().UTC().Format(time.RFC3339)
stub := `{
"sampledAt": "` + freshAt + `",
"tx_inserted": 42,
"obs_inserted": 1,
"backfillUpdates": {},
"procIO": {
"readBytesPerSec": 100,
"writeBytesPerSec": 200,
"cancelledWriteBytesPerSec": 50,
"syscallsRead": 5,
"syscallsWrite": 6,
"sampledAt": "` + freshAt + `"
}
}`
if err := os.WriteFile(statsPath, []byte(stub), 0o600); err != nil {
t.Fatal(err)
}
t.Setenv("CORESCOPE_INGESTOR_STATS", statsPath)
_, router := setupTestServer(t)
req := httptest.NewRequest("GET", "/api/perf/io", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
var body map[string]interface{}
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
t.Fatalf("invalid JSON: %v", err)
}
ing, ok := body["ingestor"].(map[string]interface{})
if !ok {
t.Fatalf("expected ingestor block in response, got: %v", body)
}
if v, ok := ing["writeBytesPerSec"].(float64); !ok || v != 200 {
t.Errorf("expected ingestor.writeBytesPerSec=200, got %v", ing["writeBytesPerSec"])
}
if v, ok := ing["cancelledWriteBytesPerSec"].(float64); !ok || v != 50 {
t.Errorf("expected ingestor.cancelledWriteBytesPerSec=50, got %v", ing["cancelledWriteBytesPerSec"])
}
}