Compare commits

2 Commits

Author SHA1 Message Date
Croissant Le Doux
f08c4935dc Performance pass: benchmark the deterministic compute path (#32)
Lock in the PLAN.md compute targets so a regression that slips an O(n²) into the
scheduler or forecast fails the suite:
- scheduler + capacity layout + Monte Carlo forecast < 1s @ 200 open issues —
  measured 232ms, comfortable headroom.
- scaling stays ~linear (400 issues ≈ 3.9x the 100-issue time; asserts < 8x to
  rule out O(n²) while tolerating jitter).

Representative fixture: 200 open issues with varied estimates/priorities/assignees
across 3 capacity lanes + a light acyclic dependency web. Bounds are the real
targets with margin so timing jitter can't flake CI; actuals are logged.

Reconcile-<5s@500 is network-bound (~2N gitea calls) and stays covered by the live
reconcile — this benchmarks the pure compute the app runs each turn. +2 core tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 15:41:56 -04:00
2a6413821a Merge pull request 'calibration: count same-day closes honestly (#34)' (#54) from feat/calibration-honesty into main
Reviewed-on: #54
2026-07-09 19:23:59 +00:00

View File

@@ -0,0 +1,75 @@
/**
* Performance pass (#32). The deterministic compute path must stay well under the
* PLAN.md targets on representative fixtures:
* - scheduler + Monte Carlo forecast < 1s @ 200 open issues.
* - scaling stays roughly linear (no accidental O(n²) in the hot path).
*
* Reconcile-<5s@500 is network-bound (~2N gitea calls) and is covered by the live
* reconcile, not here — this file benchmarks the pure compute the app runs each
* turn. Bounds are the actual targets with comfortable headroom so timing jitter
* can't flake the suite; actuals are logged.
*/
import { describe, expect, it } from 'vitest'
import { forecast } from '../forecast/forecast-v0.js'
import { type DependencyEdge, schedule, type SchedulableIssue } from '../scheduler/scheduler-v0.js'
import { scheduleWithCapacity, type Worker } from '../scheduler/scheduler-capacity-v0.js'
const EST = [1, 2, 3, 5, 8]
const WORKERS: Worker[] = [
{ person: 'a', speed: 0.8 },
{ person: 'b', speed: 0.6 },
{ person: 'c', speed: 1.0 },
]
/** A representative open backlog: varied estimates/priorities/assignees + a light dependency web. */
function backlog(n: number): { issues: SchedulableIssue[]; edges: DependencyEdge[] } {
const issues: SchedulableIssue[] = Array.from({ length: n }, (_, i) => ({
number: i + 1,
title: `Issue ${i + 1} with a representative title of some length`,
labels: [`est/${EST[i % EST.length]}d`, `p/${(i % 4) + 1}`],
estimateDays: EST[i % EST.length],
priority: (i % 4) + 1,
assignee: WORKERS[i % WORKERS.length].person,
}))
// ~1 dependency per 3 issues, always on a lower-numbered issue (acyclic)
const edges: DependencyEdge[] = []
for (let i = 3; i < n; i += 3) edges.push({ issue: i + 1, dependsOn: i - 1 })
return { issues, edges }
}
function ms(fn: () => void): number {
const t0 = performance.now()
fn()
return performance.now() - t0
}
describe('perf (#32)', () => {
it('scheduler + Monte Carlo forecast < 1s @ 200 open issues', () => {
const { issues, edges } = backlog(200)
const elapsed = ms(() => {
schedule(issues, edges)
scheduleWithCapacity(issues, edges, WORKERS)
forecast(issues, edges, { workers: WORKERS }) // 2000 trials (default)
})
// eslint-disable-next-line no-console
console.log(`[perf] schedule+capacity+forecast @200 = ${elapsed.toFixed(1)}ms`)
expect(elapsed).toBeLessThan(1000)
})
it('scales roughly linearly — 400 issues is well under 4x the 100-issue time', () => {
const small = backlog(100)
const big = backlog(400)
const run = (b: typeof small) => () => {
schedule(b.issues, b.edges)
forecast(b.issues, b.edges, { workers: WORKERS })
}
// warm up (JIT) so the ratio reflects steady state
run(small)()
const t100 = Math.max(ms(run(small)), 0.1)
const t400 = ms(run(big))
// eslint-disable-next-line no-console
console.log(`[perf] @100 = ${t100.toFixed(1)}ms · @400 = ${t400.toFixed(1)}ms · ratio ${(t400 / t100).toFixed(1)}x`)
expect(t400).toBeLessThan(t100 * 8) // generous: rules out O(n²), tolerant of jitter
})
})