Skip to content

Add a reusable Graphile performance harness - #1716

Open
Zetazzz wants to merge 5 commits into
mainfrom
test/performance-harness-core
Open

Add a reusable Graphile performance harness#1716
Zetazzz wants to merge 5 commits into
mainfrom
test/performance-harness-core

Conversation

@Zetazzz

@Zetazzz Zetazzz commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Responsibility

Adds reusable fresh-process Graphile benchmarking infrastructure. The runner accepts arbitrary serializable case definitions and delegates case-specific build and lifecycle validation to a dedicated worker entry.

Included here:

  • fresh Node process and unique PID per measurement
  • configurable worker deadlines (five minutes by default), confirmed process closure before continuing, and bounded cleanup that stops the suite if closure cannot be confirmed
  • --expose-gc with deterministic GC calls
  • build-only wall-clock timing
  • memory snapshots, deltas, and peak RSS
  • seeded scheduling and repetitions
  • schema groups and schema-hash validation
  • runtime query and case-specific validation contracts
  • PostgreSQL fixture preparation with pool cleanup on connection, SQL, and client-release failures; primary and cleanup errors are preserved together
  • atomic JSON reports and database URL redaction, including errors returned by custom workers
  • explicit --database-url and --worker-config worker arguments
  • a minimal upstream Graphile baseline worker

Stack

This is the first PR in the new performance stack and targets main. Later stacked PRs can add their own benchmark suites without changing the core runner.

Not included

No product-specific benchmark suite, application preset, application plugin integration, dependency patch, or optimization conclusion is included.

Validation

  • Full CI on commit 3477f9371996: 17/17 jobs passed (Linux, Windows, PostgreSQL, MinIO integration, and Ollama).
  • unit tests: 46 passed across 5 suites, including real child-process timeouts, deadline/close races, failed partial reports, database URL redaction, and fixture cleanup failures
  • CJS and ESM build: passed
  • ESLint: passed
  • Prettier and git diff --check: passed
  • frozen-lockfile install: passed

@Zetazzz
Zetazzz marked this pull request as ready for review September 7, 2026 03:22
@tenki-reviewer

tenki-reviewer Bot commented Sep 7, 2026

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)

🧹 Nitpicks (1) — 🟢 1 low
  • 🟢 Close the pool when connect fails (fixture.ts:50) — In prepareFixture, pool.connect() at packages/perf-harness/src/fixture.ts:50 runs before the try/finally, so a rejected connection skips pool.end() and leaks the pg.Pool.

This PR introduces packages/perf-harness, a new performance benchmarking harness for Graphile schema builds. It seeds a PostgreSQL fixture schema, spawns worker processes with GC enabled, collects timing and memory metrics, and emits a JSON report, plus a CI workflow batch and package/tsconfig wiring.

Files Change
src/process.ts, src/run.ts, src/index.ts, src/stock-worker.ts Implement child-process spawning, arg parsing, result-line parsing, and the benchmark runner entry points.
src/fixture.ts, src/types.ts Build the PostgreSQL fixture schema (validated identifiers, transaction handling) and shared metric/result types.
src/metrics.ts, src/report.ts, src/schedule.ts Compute median/min/max, memory deltas, percent change, and deterministic scheduling of benchmark groups.
__tests__/*, jest.config.js Add unit tests with a fake worker fixture covering process, run, report, schedule, and fixture behavior.
package.json, tsconfig*.json, .github/workflows/run-tests.yaml Wire up the package build, module resolution, and CI test batch inclusion.

Reviewed commit: 658736e

@Zetazzz
Zetazzz force-pushed the test/performance-harness-core branch from 658736e to f7c63ec Compare September 7, 2026 03:22

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds a new packages/perf-harness package: a performance benchmarking harness that spawns worker processes against a seeded PostgreSQL schema and reports timing/memory metrics, wired into CI.

Key findings

Comment on lines +70 to +162
export const runWorkerProcess = (
workerPath: string,
databaseUrl: string,
definition: BenchmarkCaseDefinition
): Promise<SpawnedWorkerResult> =>
new Promise((resolve, reject) => {
const config: WorkerConfigEnvelope = {
caseName: definition.name,
workerConfig: definition.workerConfig,
};
const child = spawn(
process.execPath,
[
'--expose-gc',
workerPath,
`--${DATABASE_URL_ARGUMENT}`,
databaseUrl,
`--${WORKER_CONFIG_ARGUMENT}`,
Buffer.from(JSON.stringify(config)).toString('base64url'),
],
{
env: {
...process.env,
NODE_ENV: 'production',
GRAPHILE_ENV: 'production',
},
stdio: ['ignore', 'pipe', 'pipe'],
}
);
const pid = child.pid;
let stdout = '';
let stderr = '';
child.stdout.setEncoding('utf8');
child.stderr.setEncoding('utf8');
child.stdout.on('data', (chunk: string) => {
stdout += chunk;
});
child.stderr.on('data', (chunk: string) => {
stderr += chunk;
});
child.once('error', reject);
child.once('close', (code, signal) => {
const resultLine = stdout
.split('\n')
.reverse()
.find((line) => line.startsWith(WORKER_RESULT_PREFIX));
if (!resultLine) {
reject(
new Error(
redactSecret(
`benchmark worker ${pid ?? 'unknown'} exited without a result ` +
`(code=${String(code)}, signal=${String(signal)})` +
(stderr.trim() ? `\n${lastLines(stderr)}` : ''),
databaseUrl
)
)
);
return;
}
try {
const result = JSON.parse(
resultLine.slice(WORKER_RESULT_PREFIX.length)
) as WorkerResult;
if (typeof pid !== 'number' || result.pid !== pid) {
throw new Error(
`worker PID mismatch: spawned ${String(pid)}, reported ${String(
result.pid
)}`
);
}
if (result.caseName !== definition.name) {
throw new Error(
`worker case mismatch: expected ${definition.name}, reported ${result.caseName}`
);
}
if (result.status === 'ok' && code !== 0) {
throw new Error(`successful worker exited with code ${String(code)}`);
}
resolve({ pid, result });
} catch (error) {
reject(
new Error(
redactSecret(
`invalid result from benchmark worker ${String(pid)}: ${String(
error instanceof Error ? error.message : error
)}`,
databaseUrl
)
)
);
}
});
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

Add a timeout to runWorkerProcess

runWorkerProcess (packages/perf-harness/src/process.ts:70-162) spawns a child and only settles its promise on the child's error or close events, with no timeout or child.kill() path. A worker that hangs (an infinite schema-build loop or a stalled DB connection) leaves the promise pending forever, and runBenchmarkSuite awaits each worker sequentially, so the entire cperf run hangs with no report written and no way to recover.

📋 Prompt for AI Agents

In packages/perf-harness/src/process.ts, runWorkerProcess (lines 70-162): add a configurable timeout that races the spawned child. On expiry call child.kill() (e.g. SIGKILL) and reject the promise with a clear, redacted 'worker timed out' error; clear the timer in both the error and close handlers so a normal completion does not reject after the fact. This prevents a hung worker from leaving runBenchmarkSuite pending forever.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants