Files
gbrain/tests/heavy/_measure_rss_workload.ts
T
9a3ef3cda7 feat: pgGraph-inspired CI scaffolding wave (v0.37.4.0) (#1228)
Schema-migration matrix + fuzz harness + RSS budget gate + read-latency
under sync + sync lock regression + tests/heavy convention + nightly CI
workflow + BFS frontier cap on traverseGraph.

CI infra (T1-T7):
- tests/heavy/ directory convention + scripts/run-heavy.sh + bun run test:heavy
- tests/heavy/pg_upgrade_matrix.sh: walk pre-v0.13 + pre-v0.18 brain shapes
  forward to head via bootstrap → SCHEMA_SQL → migrations → verifySchema
- test/fuzz/{pure,mixed,filesystem}-validators.test.ts: 1000-run fast-check
  property tests across 8 trust-boundary validators
- scripts/check-fuzz-purity.sh: bun-bundle + grep guard, wired into verify
- tests/heavy/measure_rss.sh: in-memory PGLite workload + peak RSS measurement
  via /proc/self/status (Linux) or process.memoryUsage().rss fallback (macOS,
  refuses to write baseline)
- tests/heavy/read_latency_under_sync.sh: phase A baseline + phase B under
  parallel writer load, reports p50/p95/p99 + delta_pct
- tests/heavy/sync_lock_regression.sh: N concurrent gbrain sync against one
  DB, asserts 1 winner + N-1 lock-busy + zero leaked gbrain_cycle_locks rows
- .github/workflows/heavy-tests.yml: cron '17 8 * * *' + heavy-tests label
  trigger + Postgres service + artifact upload on failure

Engine (T8):
- BrainEngine.traverseGraph opts gain frontierCap?: number + onTruncation?:
  (info: TruncationInfo) => void callback. Return shape preserved
  (Promise<GraphNode[]>) for MCP wire stability.
- Postgres CTE: parenthesized LIMIT N ORDER BY (slug, id) inside recursive term.
- PGLite: same SQL with positional params.
- Per-call callback closure — not engine-instance state — so concurrent
  traversals on the same engine don't cross-talk. 5 contracts pinned in
  test/regressions/v0_36_frontier_cap.test.ts.

Three plan-review passes ran before any code: CEO scope review (Approach C),
Eng dual-voice review (Claude subagent + Codex), and Codex 2nd-pass against
the revised plan. The 2nd pass caught issues the first two missed (Bun ESM
vs require.cache; engine-instance metadata stomping under concurrency;
fixture-size inconsistency). All addressed.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:25:41 -07:00

221 lines
7.2 KiB
TypeScript

/**
* tests/heavy/_measure_rss_workload.ts
*
* Single-process RSS measurement workload. Builds a synthetic brain
* in-memory, runs a representative read workload, self-reports peak RSS
* as JSON on stdout. No cross-process state — the brain lives only inside
* this process, which keeps the measurement honest.
*
* Self-measurement (Linux): polls `/proc/self/status` every 100ms, keeps
* the max RssAnon+RssShmem. Same metric as `getAccurateRss` in
* src/core/minions/worker.ts so "what CI measures" matches "what the
* runtime watchdog measures."
*
* Self-measurement (non-Linux): falls back to `process.memoryUsage().rss`
* (VmRSS). The orchestrator (measure_rss.sh) refuses to write a CI baseline
* from a macOS run because of this.
*
* Env:
* BRAIN_PAGES number of synthetic pages to insert (default 1000)
* NUM_QUERIES number of search queries to run (default 100)
*
* Output JSON shape (stable contract for the orchestrator):
* {
* ok: boolean,
* platform: string,
* measurement_path: 'proc' | 'fallback',
* queries_run: number,
* peak_rss_kb: number,
* elapsed_ms: number,
* brain_page_count: number,
* note?: string,
* error?: string
* }
*/
import { readFileSync } from 'node:fs';
import { platform } from 'node:os';
interface Result {
ok: boolean;
platform: string;
measurement_path: 'proc' | 'fallback';
queries_run: number;
peak_rss_kb: number;
elapsed_ms: number;
brain_page_count: number;
note?: string;
error?: string;
}
function readProcRss(): number | null {
try {
const status = readFileSync('/proc/self/status', 'utf8');
let anon = 0;
let shmem = 0;
let found = false;
for (const line of status.split('\n')) {
if (line.startsWith('RssAnon:')) {
const m = line.match(/(\d+)/);
if (m) {
anon = parseInt(m[1]!, 10);
found = true;
}
} else if (line.startsWith('RssShmem:')) {
const m = line.match(/(\d+)/);
if (m) {
shmem = parseInt(m[1]!, 10);
found = true;
}
}
}
return found ? anon + shmem : null;
} catch {
return null;
}
}
function readFallbackRss(): number {
return Math.floor(process.memoryUsage().rss / 1024);
}
function generatePage(i: number): { slug: string; title: string; body: string } {
const pad = String(i).padStart(5, '0');
const next1 = String(i + 1).padStart(5, '0');
const next7 = String(i + 7).padStart(5, '0');
// Deterministic body — stable across runs so measurement compares apples to
// apples. ~500 chars; realistic for the workload.
const body =
`# Synthetic Page ${i}\n\n` +
`This is a deterministic synthetic page for RSS measurement runs. Page ` +
`number ${i}. The body is stable so that successive runs produce ` +
`identical chunks and identical search-result orderings.\n\n` +
`Section 1: lorem ipsum dolor sit amet, consectetur adipiscing elit, sed ` +
`do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ` +
`ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut.\n\n` +
`Section 2: sed ut perspiciatis unde omnis iste natus error sit ` +
`voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ` +
`ipsa quae ab illo inventore veritatis et quasi architecto beatae.\n\n` +
`Page ${i} references page-${next1} and page-${next7}.`;
return {
slug: `synthetic/page-${pad}`,
title: `Synthetic Page ${i}`,
body,
};
}
async function main(): Promise<void> {
const t0 = Date.now();
const plat = platform();
const linuxProcWorks = plat === 'linux' && readProcRss() !== null;
const measurementPath: 'proc' | 'fallback' = linuxProcWorks ? 'proc' : 'fallback';
const readRss = linuxProcWorks ? () => readProcRss() ?? 0 : readFallbackRss;
const PAGES = parseInt(process.env.BRAIN_PAGES ?? '1000', 10);
const QUERIES = parseInt(process.env.NUM_QUERIES ?? '100', 10);
let peakRss = 0;
let pollTimer: ReturnType<typeof setInterval> | null = null;
function startPolling() {
pollTimer = setInterval(() => {
const rss = readRss();
if (rss > peakRss) peakRss = rss;
}, 100);
}
function stopPolling() {
if (pollTimer !== null) {
clearInterval(pollTimer);
pollTimer = null;
}
}
let result: Result;
try {
startPolling();
// Lazy imports so PGLite WASM cold-start is counted in the peak.
const { PGLiteEngine } = await import('../../src/core/pglite-engine.ts');
const { hybridSearch } = await import('../../src/core/search/hybrid.ts');
const engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
// Build the brain in-memory by inserting N synthetic pages directly via
// engine.putPage. This is the same code path the import command uses;
// we just skip the markdown file parsing.
process.stderr.write(`[_measure_rss_workload] inserting ${PAGES} pages...\n`);
for (let i = 0; i < PAGES; i += 1) {
const page = generatePage(i);
await engine.putPage(page.slug, {
type: 'note',
title: page.title,
compiled_truth: page.body,
timeline: '',
frontmatter: { synthetic: true, tags: ['synthetic', 'rss-fixture'] },
});
if (i > 0 && i % 500 === 0) {
process.stderr.write(`[_measure_rss_workload] inserted ${i}\n`);
}
}
const pageCountRows = await engine.executeRaw('SELECT count(*)::int AS c FROM pages', []);
const brainPageCount =
pageCountRows && pageCountRows[0] && typeof (pageCountRows[0] as { c: number }).c === 'number'
? (pageCountRows[0] as { c: number }).c
: 0;
process.stderr.write(`[_measure_rss_workload] running ${QUERIES} queries...\n`);
const queries = [
'synthetic page', 'lorem ipsum', 'consequat', 'voluptatem', 'aspernatur',
'magna aliqua', 'reprehenderit', 'commodo', 'inventore', 'architecto',
'page 100', 'page 500', 'rss-fixture', 'section 1', 'section 2',
];
let queriesRun = 0;
for (let i = 0; i < QUERIES; i += 1) {
const q = queries[i % queries.length]!;
try {
await hybridSearch(engine, q, { limit: 10 });
} catch {
/* keyword-only fallback if no embeddings — fine for the measurement */
}
queriesRun += 1;
}
await engine.disconnect();
const finalRss = readRss();
if (finalRss > peakRss) peakRss = finalRss;
result = {
ok: true,
platform: plat,
measurement_path: measurementPath,
queries_run: queriesRun,
peak_rss_kb: peakRss,
elapsed_ms: Date.now() - t0,
brain_page_count: brainPageCount,
...(measurementPath === 'fallback'
? { note: 'macOS/non-Linux fallback path (VmRSS, mmap-inflated)' }
: {}),
};
} catch (err) {
result = {
ok: false,
platform: plat,
measurement_path: measurementPath,
queries_run: 0,
peak_rss_kb: peakRss,
elapsed_ms: Date.now() - t0,
brain_page_count: 0,
error: err instanceof Error ? `${err.name}: ${err.message}` : String(err),
};
} finally {
stopPolling();
}
process.stdout.write(JSON.stringify(result) + '\n');
process.exit(result.ok ? 0 : 1);
}
void main();