mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-05 21:02:54 +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>
107 lines
3.3 KiB
Go
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"])
|
|
}
|
|
}
|