@kiwa-test/perf-harness 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +422 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +169 -1
- package/dist/index.d.ts +169 -1
- package/dist/index.js +407 -5
- package/dist/index.js.map +1 -1
- package/package.json +15 -16
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/measure.ts","../src/regression.ts","../src/baseline.ts","../src/gate.ts","../src/report.ts"],"sourcesContent":["export type {\n MeasureInput,\n MeasureResult,\n PerfGateInput,\n PerfGateResult,\n RegressionInput,\n RegressionResult,\n Thresholds,\n} from './types.js';\n\nexport { buildMeasureResult, measure } from './measure.js';\nexport { detectRegression } from './regression.js';\nexport { defaultBaselinePath, loadBaseline, saveBaseline } from './baseline.js';\nexport { evaluatePerfGate } from './gate.js';\nexport { emitPerfReport } from './report.js';\n","import type { MeasureInput, MeasureResult } from './types.js';\n\nexport async function measure(input: MeasureInput): Promise<MeasureResult> {\n const warmup = input.warmup ?? 0;\n if (input.iterations < 1) {\n throw new Error(`measure: iterations must be >= 1, got ${input.iterations}`);\n }\n if (warmup < 0) {\n throw new Error(`measure: warmup must be >= 0, got ${warmup}`);\n }\n\n for (let index = 0; index < warmup; index += 1) {\n await input.fn();\n }\n\n const samples: number[] = [];\n for (let index = 0; index < input.iterations; index += 1) {\n const start = process.hrtime.bigint();\n await input.fn();\n const end = process.hrtime.bigint();\n samples.push(Number(end - start) / 1_000_000);\n }\n\n return buildMeasureResult(input.name, input.iterations, warmup, samples);\n}\n\nexport function buildMeasureResult(\n name: string,\n iterations: number,\n warmup: number,\n samples: number[],\n): MeasureResult {\n const sorted = [...samples].sort((left, right) => left - right);\n const totalMs = samples.reduce((sum, sample) => sum + sample, 0);\n const mean = totalMs / samples.length;\n const variance = samples.length > 1\n ? samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1)\n : 0;\n\n return {\n name,\n iterations,\n warmup,\n samples,\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n mean,\n stdev: Math.sqrt(variance),\n minMs: sorted[0] ?? 0,\n maxMs: sorted[sorted.length - 1] ?? 0,\n totalMs,\n };\n}\n\nfunction percentile(sorted: number[], ratio: number): number {\n if (sorted.length === 0) return 0;\n const rank = Math.max(0, Math.ceil(sorted.length * ratio) - 1);\n return sorted[rank] ?? sorted[sorted.length - 1] ?? 0;\n}\n","import { buildMeasureResult } from './measure.js';\nimport type {\n MeasureResult,\n RegressionInput,\n RegressionResult,\n} from './types.js';\n\nexport function detectRegression(input: RegressionInput): RegressionResult {\n const threshold = input.threshold ?? 0.2;\n const current = normalize(input.current);\n const baseline = normalize(input.baseline);\n\n const deltaPct = baseline.p95 === 0\n ? current.p95 === 0\n ? 0\n : Number.POSITIVE_INFINITY\n : (current.p95 - baseline.p95) / baseline.p95;\n const welchT = welchTScore(current.samples, baseline.samples);\n const significant = Math.abs(welchT) > 2;\n\n let verdict: RegressionResult['verdict'] = 'stable';\n if (significant && deltaPct >= threshold) {\n verdict = 'regressed';\n } else if (significant && deltaPct <= -threshold) {\n verdict = 'improved';\n }\n\n return {\n regressed: verdict === 'regressed',\n deltaPct,\n welchT,\n significant,\n verdict,\n };\n}\n\nfunction normalize(result: MeasureResult): MeasureResult {\n if (result.samples.length === 0) {\n return result;\n }\n return buildMeasureResult(result.name, result.iterations, result.warmup, result.samples);\n}\n\nfunction welchTScore(current: number[], baseline: number[]): number {\n if (current.length < 2 || baseline.length < 2) {\n return 0;\n }\n\n const currentStats = sampleStats(current);\n const baselineStats = sampleStats(baseline);\n const numerator = currentStats.mean - baselineStats.mean;\n const denominator = Math.sqrt(\n (currentStats.variance / current.length) +\n (baselineStats.variance / baseline.length),\n );\n\n if (!Number.isFinite(denominator) || denominator === 0) {\n return 0;\n }\n return numerator / denominator;\n}\n\nfunction sampleStats(samples: number[]): { mean: number; variance: number } {\n const mean = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;\n const variance = samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1);\n return { mean, variance };\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { MeasureResult } from './types.js';\n\nexport async function loadBaseline(path: string): Promise<MeasureResult | null> {\n try {\n const body = await readFile(path, 'utf8');\n return JSON.parse(body) as MeasureResult;\n } catch (error) {\n if (isMissingFile(error)) {\n return null;\n }\n throw error;\n }\n}\n\nexport async function saveBaseline(path: string, result: MeasureResult): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(result, null, 2)}\\n`, 'utf8');\n}\n\nexport function defaultBaselinePath(moduleName: string): string {\n return `${process.cwd()}/.perf-baseline/${moduleName}.json`;\n}\n\nfunction isMissingFile(error: unknown): error is NodeJS.ErrnoException {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n","import {\n evaluateReleaseGate,\n type QualityReport,\n type ReleaseGateBlocker,\n type ReleaseGateVerdict,\n} from '@kiwa-test/quality-metrics';\nimport type {\n PerfGateInput,\n PerfGateResult,\n Thresholds,\n} from './types.js';\n\nexport function evaluatePerfGate(input: PerfGateInput): PerfGateResult {\n const thresholds = input.thresholds ?? {};\n const enabledAxes = countThresholds(thresholds);\n const report = buildReport(input, thresholds, enabledAxes === 0);\n const relaxedVerdict = evaluateReleaseGate(report, {\n coverageLine: 0,\n coverageBranch: 0,\n coverageFunction: 0,\n fidelityRatio: 0,\n perfP95Ms: thresholds.p95Ms ?? Number.POSITIVE_INFINITY,\n mutationKillRate: 0,\n behaviorTests: 0,\n });\n\n if (enabledAxes === 0) {\n return {\n report,\n verdict: { passed: true, blockers: [], axesEvaluated: 0 },\n breaches: [],\n };\n }\n\n const breaches: ReleaseGateBlocker[] = [];\n if (thresholds.p95Ms !== undefined && relaxedVerdict.blockers.some((blocker) => blocker.axis === 'perf.p95Ms')) {\n breaches.push({\n axis: 'perf.p95Ms',\n threshold: thresholds.p95Ms,\n actual: input.result.p95,\n op: '<=',\n });\n }\n\n appendOptionalBreach(\n breaches,\n 'cost.perRequestUsd',\n '<=',\n thresholds.costUsd,\n input.metrics?.costUsd,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'token.totalTokens',\n '<=',\n thresholds.tokens,\n input.metrics?.tokens,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'accuracy.score',\n '>=',\n thresholds.accuracy,\n input.metrics?.accuracy,\n Number.NEGATIVE_INFINITY,\n );\n\n const verdict: ReleaseGateVerdict = {\n passed: breaches.length === 0,\n blockers: breaches,\n axesEvaluated: enabledAxes,\n };\n\n return { report, verdict, breaches };\n}\n\nfunction buildReport(\n input: PerfGateInput,\n thresholds: Thresholds,\n empty: boolean,\n): QualityReport {\n const perf = empty || thresholds.p95Ms === undefined\n ? { p50Ms: 0, p95Ms: 0, p99Ms: 0, samples: 0 }\n : {\n p50Ms: input.result.p50,\n p95Ms: input.result.p95,\n p99Ms: input.result.p99,\n samples: input.result.samples.length,\n };\n const report: QualityReport = {\n provider: `@kiwa-test/perf-harness/${input.result.name}`,\n version: '0.1.0',\n reportedAt: new Date().toISOString(),\n coverage: { line: 0, branch: 0, function: 0 },\n testCount: { behavior: 0, integration: 0, e2e: 0, total: 0 },\n fidelity: { mockCoveredMethods: 0, realTotalMethods: 0, ratio: 0 },\n perf,\n mutation: { mutations: 0, killed: 0, survived: 0, killRate: 0 },\n };\n if (!empty && thresholds.costUsd !== undefined) {\n const actual = input.metrics?.costUsd ?? 0;\n report.cost = { perRequestUsd: actual, totalUsd: actual, requests: 1 };\n }\n if (!empty && thresholds.tokens !== undefined) {\n const actual = input.metrics?.tokens ?? 0;\n report.token = {\n promptTokens: actual,\n completionTokens: 0,\n totalTokens: actual,\n requests: 1,\n };\n }\n if (!empty && thresholds.accuracy !== undefined) {\n report.accuracy = {\n score: input.metrics?.accuracy ?? 0,\n samples: 1,\n method: 'provided',\n };\n }\n return report;\n}\n\nfunction appendOptionalBreach(\n breaches: ReleaseGateBlocker[],\n axis: string,\n op: '>=' | '<=',\n threshold: number | undefined,\n actual: number | undefined,\n missingFallback: number,\n): void {\n if (threshold === undefined) {\n return;\n }\n const resolvedActual = actual ?? missingFallback;\n const passed = op === '<=' ? resolvedActual <= threshold : resolvedActual >= threshold;\n if (!passed) {\n breaches.push({\n axis,\n threshold,\n actual: resolvedActual,\n op,\n });\n }\n}\n\nfunction countThresholds(thresholds: Thresholds): number {\n return [\n thresholds.p95Ms,\n thresholds.costUsd,\n thresholds.tokens,\n thresholds.accuracy,\n ].filter((value) => value !== undefined).length;\n}\n","import type { MeasureResult } from './types.js';\n\nexport function emitPerfReport(\n result: MeasureResult,\n opts: {\n baseline?: MeasureResult;\n includeSamples?: boolean;\n } = {},\n): string {\n const lines: string[] = [];\n lines.push(`# Perf Report — ${result.name}`);\n lines.push('');\n lines.push('| metric | value |');\n lines.push('|---|---|');\n lines.push(`| iterations | ${result.iterations} |`);\n lines.push(`| warmup | ${result.warmup} |`);\n lines.push(`| p50 | ${formatMs(result.p50)} |`);\n lines.push(`| p95 | ${formatMs(result.p95)} |`);\n lines.push(`| p99 | ${formatMs(result.p99)} |`);\n lines.push(`| mean | ${formatMs(result.mean)} |`);\n lines.push(`| stdev | ${formatMs(result.stdev)} |`);\n lines.push(`| min | ${formatMs(result.minMs)} |`);\n lines.push(`| max | ${formatMs(result.maxMs)} |`);\n lines.push(`| total | ${formatMs(result.totalMs)} |`);\n lines.push('');\n\n if (opts.baseline) {\n const metrics = [\n { label: 'p50', current: result.p50, baseline: opts.baseline.p50 },\n { label: 'p95', current: result.p95, baseline: opts.baseline.p95 },\n { label: 'p99', current: result.p99, baseline: opts.baseline.p99 },\n { label: 'mean', current: result.mean, baseline: opts.baseline.mean },\n { label: 'min', current: result.minMs, baseline: opts.baseline.minMs },\n { label: 'max', current: result.maxMs, baseline: opts.baseline.maxMs },\n { label: 'total', current: result.totalMs, baseline: opts.baseline.totalMs },\n ];\n lines.push('## Baseline diff');\n lines.push('');\n lines.push('| metric | current | baseline | delta ms | delta % |');\n lines.push('|---|---|---|---|---|');\n for (const metric of metrics) {\n const deltaMs = metric.current - metric.baseline;\n const deltaPct = metric.baseline === 0 ? 0 : (deltaMs / metric.baseline) * 100;\n lines.push(\n `| ${metric.label} | ${formatMs(metric.current)} | ${formatMs(metric.baseline)} | ${formatSignedMs(deltaMs)} | ${formatSignedPct(deltaPct)} |`,\n );\n }\n lines.push('');\n }\n\n if (opts.includeSamples) {\n lines.push('## Samples histogram');\n lines.push('');\n lines.push('| bin | range ms | count | bar |');\n lines.push('|---|---|---|---|');\n for (const row of histogramRows(result.samples, 10)) {\n lines.push(`| ${row.index} | ${row.range} | ${row.count} | ${row.bar} |`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction histogramRows(samples: number[], bins: number): Array<{\n index: number;\n range: string;\n count: number;\n bar: string;\n}> {\n if (samples.length === 0) {\n return [];\n }\n\n const min = Math.min(...samples);\n const max = Math.max(...samples);\n const width = max === min ? 1 : (max - min) / bins;\n const counts = new Array<number>(bins).fill(0);\n\n for (const sample of samples) {\n const rawIndex = width === 0 ? 0 : Math.floor((sample - min) / width);\n const index = Math.min(bins - 1, Math.max(0, rawIndex));\n counts[index] = (counts[index] ?? 0) + 1;\n }\n\n const peak = Math.max(...counts, 1);\n return counts.map((count, index) => {\n const start = min + (index * width);\n const end = index === bins - 1 ? max : start + width;\n return {\n index: index + 1,\n range: `${start.toFixed(2)}-${end.toFixed(2)}`,\n count,\n bar: '#'.repeat(count === 0 ? 0 : Math.max(1, Math.round((count / peak) * 10))),\n };\n });\n}\n\nfunction formatMs(value: number): string {\n return `${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedMs(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedPct(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}%`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/measure.ts","../src/concurrent.ts","../src/memory.ts","../src/regression.ts","../src/baseline.ts","../src/gate.ts","../src/report.ts","../src/three-layer.ts","../src/live.ts"],"sourcesContent":["export type {\n MeasureInput,\n MeasureResult,\n PerfGateInput,\n PerfGateResult,\n RegressionInput,\n RegressionResult,\n Thresholds,\n} from './types.js';\n\nexport { buildMeasureResult, measure } from './measure.js';\nexport { measureConcurrent, type ConcurrentInput } from './concurrent.js';\nexport { measureMemory, type MemoryInput, type MemorySample } from './memory.js';\nexport { detectRegression } from './regression.js';\nexport { defaultBaselinePath, loadBaseline, saveBaseline } from './baseline.js';\nexport { evaluatePerfGate } from './gate.js';\nexport { emitPerfReport } from './report.js';\nexport {\n runPerf3Layer,\n resolveKiwaRepoRoot,\n type PerfOpSpec,\n type RunPerf3LayerInput,\n type RunPerf3LayerResult,\n type OpOutcome,\n} from './three-layer.js';\nexport {\n runPerf3LayerLive,\n type LivePerfOpSpec,\n type LiveOpOutcome,\n type RunPerf3LayerLiveInput,\n type RunPerf3LayerLiveResult,\n} from './live.js';\n","import type { MeasureInput, MeasureResult } from './types.js';\n\nexport async function measure(input: MeasureInput): Promise<MeasureResult> {\n const warmup = input.warmup ?? 0;\n if (input.iterations < 1) {\n throw new Error(`measure: iterations must be >= 1, got ${input.iterations}`);\n }\n if (warmup < 0) {\n throw new Error(`measure: warmup must be >= 0, got ${warmup}`);\n }\n\n for (let index = 0; index < warmup; index += 1) {\n await input.fn();\n }\n\n const samples: number[] = [];\n for (let index = 0; index < input.iterations; index += 1) {\n const start = process.hrtime.bigint();\n await input.fn();\n const end = process.hrtime.bigint();\n samples.push(Number(end - start) / 1_000_000);\n }\n\n return buildMeasureResult(input.name, input.iterations, warmup, samples);\n}\n\nexport function buildMeasureResult(\n name: string,\n iterations: number,\n warmup: number,\n samples: number[],\n): MeasureResult {\n const sorted = [...samples].sort((left, right) => left - right);\n const totalMs = samples.reduce((sum, sample) => sum + sample, 0);\n const mean = totalMs / samples.length;\n const variance = samples.length > 1\n ? samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1)\n : 0;\n\n return {\n name,\n iterations,\n warmup,\n samples,\n p50: percentile(sorted, 0.5),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n mean,\n stdev: Math.sqrt(variance),\n minMs: sorted[0] ?? 0,\n maxMs: sorted[sorted.length - 1] ?? 0,\n totalMs,\n };\n}\n\nfunction percentile(sorted: number[], ratio: number): number {\n if (sorted.length === 0) return 0;\n const rank = Math.max(0, Math.ceil(sorted.length * ratio) - 1);\n return sorted[rank] ?? sorted[sorted.length - 1] ?? 0;\n}\n","import { buildMeasureResult } from './measure.js';\nimport type { MeasureResult } from './types.js';\n\n/**\n * measureConcurrent — drive `fn` under a fixed concurrency load and record\n * per-call latency.\n *\n * Real production traffic is not serial. A p95 that looks fine at\n * `iterations = 200, concurrency = 1` (the default `measure`) can collapse\n * once N clients hit the same code path at once because contention on the\n * shared engine / recorder / queue kicks in.\n *\n * This helper spawns `concurrency` parallel workers, each of which loops\n * `iterationsPerWorker` times. Total sample count = concurrency ×\n * iterationsPerWorker. Every sample is a wall-clock per-call latency (from\n * `process.hrtime.bigint()` around each `fn()` invocation).\n *\n * Returned {@link MeasureResult} has the same shape as `measure` so\n * downstream regression / gate / report code does not need to branch.\n *\n * @param input.name identifier for the report\n * @param input.fn the async unit to exercise\n * @param input.concurrency number of parallel workers (must be >= 1)\n * @param input.iterationsPerWorker per-worker loop count (must be >= 1)\n * @param input.warmup discarded warmup iterations per worker (default 0)\n */\nexport interface ConcurrentInput {\n name: string;\n fn: () => Promise<unknown> | unknown;\n concurrency: number;\n iterationsPerWorker: number;\n warmup?: number;\n}\n\nexport async function measureConcurrent(input: ConcurrentInput): Promise<MeasureResult> {\n if (input.concurrency < 1) {\n throw new Error(`measureConcurrent: concurrency must be >= 1, got ${input.concurrency}`);\n }\n if (input.iterationsPerWorker < 1) {\n throw new Error(\n `measureConcurrent: iterationsPerWorker must be >= 1, got ${input.iterationsPerWorker}`,\n );\n }\n const warmup = input.warmup ?? 0;\n if (warmup < 0) {\n throw new Error(`measureConcurrent: warmup must be >= 0, got ${warmup}`);\n }\n\n const worker = async (): Promise<number[]> => {\n for (let index = 0; index < warmup; index += 1) {\n await input.fn();\n }\n const local: number[] = [];\n for (let index = 0; index < input.iterationsPerWorker; index += 1) {\n const start = process.hrtime.bigint();\n await input.fn();\n const end = process.hrtime.bigint();\n local.push(Number(end - start) / 1_000_000);\n }\n return local;\n };\n\n const workers = Array.from({ length: input.concurrency }, () => worker());\n const perWorkerSamples = await Promise.all(workers);\n const samples = perWorkerSamples.flat();\n const totalIterations = input.concurrency * input.iterationsPerWorker;\n return buildMeasureResult(input.name, totalIterations, warmup * input.concurrency, samples);\n}\n","/**\n * measureMemory — capture heap deltas around a target function.\n *\n * Real production concerns include memory growth per call. A p95 of 5ms is\n * useless if every call leaks 100KB of retained heap. This helper wraps a\n * target function with a global.gc() + process.memoryUsage() bracket so\n * tests can assert on `heapUsedDelta` / `rssUsedDelta` per call.\n *\n * Requires Node to be launched with `--expose-gc` for stable readings.\n * When GC is not exposed we fall back to a delta without forced GC — the\n * numbers are noisier but the trend still catches egregious leaks.\n */\nexport interface MemorySample {\n iterationCount: number;\n heapUsedDeltaBytes: number;\n heapUsedDeltaPerIterationBytes: number;\n rssDeltaBytes: number;\n externalDeltaBytes: number;\n arrayBuffersDeltaBytes: number;\n gcExposed: boolean;\n}\n\nexport interface MemoryInput {\n fn: () => Promise<unknown> | unknown;\n iterations: number;\n}\n\nexport async function measureMemory(input: MemoryInput): Promise<MemorySample> {\n if (input.iterations < 1) {\n throw new Error(`measureMemory: iterations must be >= 1, got ${input.iterations}`);\n }\n\n const gcRef = (globalThis as { gc?: () => void }).gc;\n const gcExposed = typeof gcRef === 'function';\n\n if (gcExposed) gcRef!();\n const before = process.memoryUsage();\n\n for (let index = 0; index < input.iterations; index += 1) {\n await input.fn();\n }\n\n if (gcExposed) gcRef!();\n const after = process.memoryUsage();\n\n const heapUsedDelta = after.heapUsed - before.heapUsed;\n return {\n iterationCount: input.iterations,\n heapUsedDeltaBytes: heapUsedDelta,\n heapUsedDeltaPerIterationBytes: heapUsedDelta / input.iterations,\n rssDeltaBytes: after.rss - before.rss,\n externalDeltaBytes: after.external - before.external,\n arrayBuffersDeltaBytes: after.arrayBuffers - before.arrayBuffers,\n gcExposed,\n };\n}\n","import { buildMeasureResult } from './measure.js';\nimport type {\n MeasureResult,\n RegressionInput,\n RegressionResult,\n} from './types.js';\n\nexport function detectRegression(input: RegressionInput): RegressionResult {\n const threshold = input.threshold ?? 0.2;\n const current = normalize(input.current);\n const baseline = normalize(input.baseline);\n\n const deltaPct = baseline.p95 === 0\n ? current.p95 === 0\n ? 0\n : Number.POSITIVE_INFINITY\n : (current.p95 - baseline.p95) / baseline.p95;\n const welchT = welchTScore(current.samples, baseline.samples);\n const significant = Math.abs(welchT) > 2;\n\n let verdict: RegressionResult['verdict'] = 'stable';\n if (significant && deltaPct >= threshold) {\n verdict = 'regressed';\n } else if (significant && deltaPct <= -threshold) {\n verdict = 'improved';\n }\n\n return {\n regressed: verdict === 'regressed',\n deltaPct,\n welchT,\n significant,\n verdict,\n };\n}\n\nfunction normalize(result: MeasureResult): MeasureResult {\n if (result.samples.length === 0) {\n return result;\n }\n return buildMeasureResult(result.name, result.iterations, result.warmup, result.samples);\n}\n\nfunction welchTScore(current: number[], baseline: number[]): number {\n if (current.length < 2 || baseline.length < 2) {\n return 0;\n }\n\n const currentStats = sampleStats(current);\n const baselineStats = sampleStats(baseline);\n const numerator = currentStats.mean - baselineStats.mean;\n const denominator = Math.sqrt(\n (currentStats.variance / current.length) +\n (baselineStats.variance / baseline.length),\n );\n\n if (!Number.isFinite(denominator) || denominator === 0) {\n return 0;\n }\n return numerator / denominator;\n}\n\nfunction sampleStats(samples: number[]): { mean: number; variance: number } {\n const mean = samples.reduce((sum, sample) => sum + sample, 0) / samples.length;\n const variance = samples.reduce((sum, sample) => {\n const delta = sample - mean;\n return sum + (delta * delta);\n }, 0) / (samples.length - 1);\n return { mean, variance };\n}\n","import { mkdir, readFile, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { MeasureResult } from './types.js';\n\nexport async function loadBaseline(path: string): Promise<MeasureResult | null> {\n try {\n const body = await readFile(path, 'utf8');\n return JSON.parse(body) as MeasureResult;\n } catch (error) {\n if (isMissingFile(error)) {\n return null;\n }\n throw error;\n }\n}\n\nexport async function saveBaseline(path: string, result: MeasureResult): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n await writeFile(path, `${JSON.stringify(result, null, 2)}\\n`, 'utf8');\n}\n\nexport function defaultBaselinePath(moduleName: string): string {\n return `${process.cwd()}/.perf-baseline/${moduleName}.json`;\n}\n\nfunction isMissingFile(error: unknown): error is NodeJS.ErrnoException {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';\n}\n","import {\n evaluateReleaseGate,\n type QualityReport,\n type ReleaseGateBlocker,\n type ReleaseGateVerdict,\n} from '@kiwa-test/quality-metrics';\nimport type {\n PerfGateInput,\n PerfGateResult,\n Thresholds,\n} from './types.js';\n\nexport function evaluatePerfGate(input: PerfGateInput): PerfGateResult {\n const thresholds = input.thresholds ?? {};\n const enabledAxes = countThresholds(thresholds);\n const report = buildReport(input, thresholds, enabledAxes === 0);\n const relaxedVerdict = evaluateReleaseGate(report, {\n coverageLine: 0,\n coverageBranch: 0,\n coverageFunction: 0,\n fidelityRatio: 0,\n perfP95Ms: thresholds.p95Ms ?? Number.POSITIVE_INFINITY,\n mutationKillRate: 0,\n behaviorTests: 0,\n });\n\n if (enabledAxes === 0) {\n return {\n report,\n verdict: { passed: true, blockers: [], axesEvaluated: 0 },\n breaches: [],\n };\n }\n\n const breaches: ReleaseGateBlocker[] = [];\n if (thresholds.p95Ms !== undefined && relaxedVerdict.blockers.some((blocker) => blocker.axis === 'perf.p95Ms')) {\n breaches.push({\n axis: 'perf.p95Ms',\n threshold: thresholds.p95Ms,\n actual: input.result.p95,\n op: '<=',\n });\n }\n\n appendOptionalBreach(\n breaches,\n 'cost.perRequestUsd',\n '<=',\n thresholds.costUsd,\n input.metrics?.costUsd,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'token.totalTokens',\n '<=',\n thresholds.tokens,\n input.metrics?.tokens,\n Number.POSITIVE_INFINITY,\n );\n appendOptionalBreach(\n breaches,\n 'accuracy.score',\n '>=',\n thresholds.accuracy,\n input.metrics?.accuracy,\n Number.NEGATIVE_INFINITY,\n );\n\n const verdict: ReleaseGateVerdict = {\n passed: breaches.length === 0,\n blockers: breaches,\n axesEvaluated: enabledAxes,\n };\n\n return { report, verdict, breaches };\n}\n\nfunction buildReport(\n input: PerfGateInput,\n thresholds: Thresholds,\n empty: boolean,\n): QualityReport {\n const perf = empty || thresholds.p95Ms === undefined\n ? { p50Ms: 0, p95Ms: 0, p99Ms: 0, samples: 0 }\n : {\n p50Ms: input.result.p50,\n p95Ms: input.result.p95,\n p99Ms: input.result.p99,\n samples: input.result.samples.length,\n };\n const report: QualityReport = {\n provider: `@kiwa-test/perf-harness/${input.result.name}`,\n version: '0.1.0',\n reportedAt: new Date().toISOString(),\n coverage: { line: 0, branch: 0, function: 0 },\n testCount: { behavior: 0, integration: 0, e2e: 0, total: 0 },\n fidelity: { mockCoveredMethods: 0, realTotalMethods: 0, ratio: 0 },\n perf,\n mutation: { mutations: 0, killed: 0, survived: 0, killRate: 0 },\n };\n if (!empty && thresholds.costUsd !== undefined) {\n const actual = input.metrics?.costUsd ?? 0;\n report.cost = { perRequestUsd: actual, totalUsd: actual, requests: 1 };\n }\n if (!empty && thresholds.tokens !== undefined) {\n const actual = input.metrics?.tokens ?? 0;\n report.token = {\n promptTokens: actual,\n completionTokens: 0,\n totalTokens: actual,\n requests: 1,\n };\n }\n if (!empty && thresholds.accuracy !== undefined) {\n report.accuracy = {\n score: input.metrics?.accuracy ?? 0,\n samples: 1,\n method: 'provided',\n };\n }\n return report;\n}\n\nfunction appendOptionalBreach(\n breaches: ReleaseGateBlocker[],\n axis: string,\n op: '>=' | '<=',\n threshold: number | undefined,\n actual: number | undefined,\n missingFallback: number,\n): void {\n if (threshold === undefined) {\n return;\n }\n const resolvedActual = actual ?? missingFallback;\n const passed = op === '<=' ? resolvedActual <= threshold : resolvedActual >= threshold;\n if (!passed) {\n breaches.push({\n axis,\n threshold,\n actual: resolvedActual,\n op,\n });\n }\n}\n\nfunction countThresholds(thresholds: Thresholds): number {\n return [\n thresholds.p95Ms,\n thresholds.costUsd,\n thresholds.tokens,\n thresholds.accuracy,\n ].filter((value) => value !== undefined).length;\n}\n","import type { MeasureResult } from './types.js';\n\nexport function emitPerfReport(\n result: MeasureResult,\n opts: {\n baseline?: MeasureResult;\n includeSamples?: boolean;\n } = {},\n): string {\n const lines: string[] = [];\n lines.push(`# Perf Report — ${result.name}`);\n lines.push('');\n lines.push('| metric | value |');\n lines.push('|---|---|');\n lines.push(`| iterations | ${result.iterations} |`);\n lines.push(`| warmup | ${result.warmup} |`);\n lines.push(`| p50 | ${formatMs(result.p50)} |`);\n lines.push(`| p95 | ${formatMs(result.p95)} |`);\n lines.push(`| p99 | ${formatMs(result.p99)} |`);\n lines.push(`| mean | ${formatMs(result.mean)} |`);\n lines.push(`| stdev | ${formatMs(result.stdev)} |`);\n lines.push(`| min | ${formatMs(result.minMs)} |`);\n lines.push(`| max | ${formatMs(result.maxMs)} |`);\n lines.push(`| total | ${formatMs(result.totalMs)} |`);\n lines.push('');\n\n if (opts.baseline) {\n const metrics = [\n { label: 'p50', current: result.p50, baseline: opts.baseline.p50 },\n { label: 'p95', current: result.p95, baseline: opts.baseline.p95 },\n { label: 'p99', current: result.p99, baseline: opts.baseline.p99 },\n { label: 'mean', current: result.mean, baseline: opts.baseline.mean },\n { label: 'min', current: result.minMs, baseline: opts.baseline.minMs },\n { label: 'max', current: result.maxMs, baseline: opts.baseline.maxMs },\n { label: 'total', current: result.totalMs, baseline: opts.baseline.totalMs },\n ];\n lines.push('## Baseline diff');\n lines.push('');\n lines.push('| metric | current | baseline | delta ms | delta % |');\n lines.push('|---|---|---|---|---|');\n for (const metric of metrics) {\n const deltaMs = metric.current - metric.baseline;\n const deltaPct = metric.baseline === 0 ? 0 : (deltaMs / metric.baseline) * 100;\n lines.push(\n `| ${metric.label} | ${formatMs(metric.current)} | ${formatMs(metric.baseline)} | ${formatSignedMs(deltaMs)} | ${formatSignedPct(deltaPct)} |`,\n );\n }\n lines.push('');\n }\n\n if (opts.includeSamples) {\n lines.push('## Samples histogram');\n lines.push('');\n lines.push('| bin | range ms | count | bar |');\n lines.push('|---|---|---|---|');\n for (const row of histogramRows(result.samples, 10)) {\n lines.push(`| ${row.index} | ${row.range} | ${row.count} | ${row.bar} |`);\n }\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\nfunction histogramRows(samples: number[], bins: number): Array<{\n index: number;\n range: string;\n count: number;\n bar: string;\n}> {\n if (samples.length === 0) {\n return [];\n }\n\n const min = Math.min(...samples);\n const max = Math.max(...samples);\n const width = max === min ? 1 : (max - min) / bins;\n const counts = new Array<number>(bins).fill(0);\n\n for (const sample of samples) {\n const rawIndex = width === 0 ? 0 : Math.floor((sample - min) / width);\n const index = Math.min(bins - 1, Math.max(0, rawIndex));\n counts[index] = (counts[index] ?? 0) + 1;\n }\n\n const peak = Math.max(...counts, 1);\n return counts.map((count, index) => {\n const start = min + (index * width);\n const end = index === bins - 1 ? max : start + width;\n return {\n index: index + 1,\n range: `${start.toFixed(2)}-${end.toFixed(2)}`,\n count,\n bar: '#'.repeat(count === 0 ? 0 : Math.max(1, Math.round((count / peak) * 10))),\n };\n });\n}\n\nfunction formatMs(value: number): string {\n return `${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedMs(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}ms`;\n}\n\nfunction formatSignedPct(value: number): string {\n const sign = value > 0 ? '+' : '';\n return `${sign}${value.toFixed(2)}%`;\n}\n","/**\n * runPerf3Layer — the reusable 3-layer perf harness landed in v1.14-post.\n *\n * v1.13-1 shipped `measure` alone which caps at \"serial mock is fast\" — not\n * a real perf guarantee. #739 introduced 3-layer coverage:\n *\n * - **serial** — `measure` at concurrency 1 (baseline latency)\n * - **concurrent** — `measureConcurrent` at N workers (contention / bottleneck)\n * - **memory** — `measureMemory` (arrayBuffers axis, GC-independent)\n *\n * Plus baseline auto-seed + regression detection (20 % delta + Welch t-test)\n * + a markdown report with SSOT threshold references.\n *\n * Callers declare their target ops + thresholds and this helper drives the\n * whole pipeline. See docs/quality/perf-thresholds.md for the threshold\n * SSOT.\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { measure } from './measure.js';\nimport { measureConcurrent } from './concurrent.js';\nimport { measureMemory, type MemorySample } from './memory.js';\nimport { detectRegression } from './regression.js';\nimport { defaultBaselinePath, loadBaseline, saveBaseline } from './baseline.js';\nimport { evaluatePerfGate } from './gate.js';\nimport { emitPerfReport } from './report.js';\nimport type { MeasureResult } from './types.js';\n\nexport interface PerfOpSpec {\n name: string;\n fn: () => Promise<unknown> | unknown;\n /**\n * Serial p95 hard cap (ms). Source: docs/quality/perf-thresholds.md.\n */\n serialP95CapMs: number;\n /**\n * Optional override for concurrent cap. Default = 2 × serial cap per SSOT.\n */\n concurrentP95CapMs?: number;\n /**\n * Optional override for memory arrayBuffers cap.\n * Default = 100 KB across 200 iterations.\n */\n memoryArrayBuffersCapBytes?: number;\n}\n\nexport interface RunPerf3LayerInput {\n moduleName: string;\n ops: PerfOpSpec[];\n /**\n * Absolute path to the markdown report file. Overwritten each run.\n */\n reportPath: string;\n /**\n * Optional override for baseline path. Default = defaultBaselinePath(moduleName).\n */\n baselinePath?: string;\n /**\n * Iterations for the serial phase. Default 200.\n */\n serialIterations?: number;\n /**\n * Warmup iterations for the serial phase (discarded). Default 5.\n */\n serialWarmup?: number;\n /**\n * Worker count for the concurrent phase. Default 10.\n */\n concurrency?: number;\n /**\n * Per-worker iterations for the concurrent phase. Default 50.\n */\n iterationsPerWorker?: number;\n /**\n * Iterations for the memory phase. Default 200.\n */\n memoryIterations?: number;\n /**\n * Path (relative to reportPath's directory tree) that the report references\n * as the threshold SSOT. Default: '../../quality/perf-thresholds'.\n */\n thresholdDocLink?: string;\n}\n\nexport interface OpOutcome {\n name: string;\n serial: MeasureResult;\n concurrent: MeasureResult;\n memory: MemorySample;\n serialGatePassed: boolean;\n concurrentGatePassed: boolean;\n memoryGatePassed: boolean;\n regressionVerdict: 'stable' | 'improved' | 'regressed' | 'n/a (baseline seeded)';\n}\n\nexport interface RunPerf3LayerResult {\n outcomes: OpOutcome[];\n allPassed: boolean;\n baselineSeeded: boolean;\n}\n\nexport async function runPerf3Layer(input: RunPerf3LayerInput): Promise<RunPerf3LayerResult> {\n const serialIterations = input.serialIterations ?? 200;\n const serialWarmup = input.serialWarmup ?? 5;\n const concurrency = input.concurrency ?? 10;\n const iterationsPerWorker = input.iterationsPerWorker ?? 50;\n const memoryIterations = input.memoryIterations ?? 200;\n const memoryCapDefault = 100 * 1024;\n const baselinePath = input.baselinePath ?? defaultBaselinePath(input.moduleName);\n const thresholdDocLink = input.thresholdDocLink ?? '../../quality/perf-thresholds';\n\n const priorBaseline = (await loadBaseline(baselinePath)) as unknown as Record<string, MeasureResult> | null;\n const combinedForBaseline: Record<string, MeasureResult> = {};\n const outcomes: OpOutcome[] = [];\n\n for (const op of input.ops) {\n const serial = await measure({\n name: `${op.name}.serial`,\n iterations: serialIterations,\n warmup: serialWarmup,\n fn: async () => {\n await op.fn();\n },\n });\n const concurrent = await measureConcurrent({\n name: `${op.name}.concurrent`,\n concurrency,\n iterationsPerWorker,\n warmup: 2,\n fn: async () => {\n await op.fn();\n },\n });\n const memory = await measureMemory({\n fn: async () => {\n await op.fn();\n },\n iterations: memoryIterations,\n });\n\n const concurrentCap = op.concurrentP95CapMs ?? op.serialP95CapMs * 2;\n const memoryCap = op.memoryArrayBuffersCapBytes ?? memoryCapDefault;\n\n const serialGate = evaluatePerfGate({\n result: serial,\n thresholds: { p95Ms: op.serialP95CapMs },\n });\n const concurrentGate = evaluatePerfGate({\n result: concurrent,\n thresholds: { p95Ms: concurrentCap },\n });\n const memoryGatePassed = memory.arrayBuffersDeltaBytes < memoryCap;\n\n const priorSerial = priorBaseline?.[`${op.name}.serial`];\n const regression = priorSerial\n ? detectRegression({ current: serial, baseline: priorSerial, threshold: 0.2 })\n : null;\n\n combinedForBaseline[`${op.name}.serial`] = serial;\n combinedForBaseline[`${op.name}.concurrent`] = concurrent;\n\n outcomes.push({\n name: op.name,\n serial,\n concurrent,\n memory,\n serialGatePassed: serialGate.verdict.passed,\n concurrentGatePassed: concurrentGate.verdict.passed,\n memoryGatePassed,\n regressionVerdict: regression ? regression.verdict : 'n/a (baseline seeded)',\n });\n }\n\n const baselineSeeded = priorBaseline === null;\n if (baselineSeeded) {\n await saveBaseline(\n baselinePath,\n combinedForBaseline as unknown as MeasureResult,\n );\n }\n\n const allPassed = outcomes.every(\n (o) => o.serialGatePassed && o.concurrentGatePassed && o.memoryGatePassed,\n );\n\n writeReport({\n reportPath: input.reportPath,\n moduleName: input.moduleName,\n outcomes,\n ops: input.ops,\n thresholdDocLink,\n priorBaseline,\n concurrency,\n iterationsPerWorker,\n memoryIterations,\n memoryCapDefault,\n });\n\n return { outcomes, allPassed, baselineSeeded };\n}\n\ninterface WriteReportInput {\n reportPath: string;\n moduleName: string;\n outcomes: OpOutcome[];\n ops: PerfOpSpec[];\n thresholdDocLink: string;\n priorBaseline: Record<string, MeasureResult> | null;\n concurrency: number;\n iterationsPerWorker: number;\n memoryIterations: number;\n memoryCapDefault: number;\n}\n\nfunction writeReport(input: WriteReportInput): void {\n const lines: string[] = [\n `# Perf Suite — ${input.moduleName}`,\n '',\n `Threshold source: [docs/quality/perf-thresholds.md](${input.thresholdDocLink})`,\n '',\n '## Serial p95 (concurrency = 1)',\n '',\n '| op | p95 | cap | gate | regression |',\n '|---|---|---|---|---|',\n ];\n input.ops.forEach((op, idx) => {\n const out = input.outcomes[idx]!;\n lines.push(\n `| ${op.name} | ${out.serial.p95.toFixed(2)}ms | ${op.serialP95CapMs}ms | ${out.serialGatePassed ? 'PASS' : 'FAIL'} | ${out.regressionVerdict} |`,\n );\n });\n\n lines.push(\n '',\n `## Concurrent p95 (concurrency = ${input.concurrency}, ${input.iterationsPerWorker} iter each)`,\n '',\n '| op | p95 | cap | gate |',\n '|---|---|---|---|',\n );\n input.ops.forEach((op, idx) => {\n const out = input.outcomes[idx]!;\n const cap = op.concurrentP95CapMs ?? op.serialP95CapMs * 2;\n lines.push(\n `| ${op.name} | ${out.concurrent.p95.toFixed(2)}ms | ${cap}ms | ${out.concurrentGatePassed ? 'PASS' : 'FAIL'} |`,\n );\n });\n\n lines.push(\n '',\n `## Memory retention (${input.memoryIterations} iter, arrayBuffers axis is the gate; heap is informational)`,\n '',\n '| op | heapUsed Δ | arrayBuffers Δ | cap | verdict |',\n '|---|---|---|---|---|',\n );\n input.ops.forEach((op, idx) => {\n const out = input.outcomes[idx]!;\n const cap = op.memoryArrayBuffersCapBytes ?? input.memoryCapDefault;\n lines.push(\n `| ${op.name} | ${out.memory.heapUsedDeltaBytes} B | ${out.memory.arrayBuffersDeltaBytes} B | ${cap} B | ${out.memoryGatePassed ? 'PASS' : 'FAIL'} |`,\n );\n });\n\n lines.push('', '## Detailed serial reports', '');\n input.ops.forEach((op, idx) => {\n const out = input.outcomes[idx]!;\n lines.push(`### ${op.name}`);\n lines.push('');\n const priorSerial = input.priorBaseline?.[`${op.name}.serial`];\n lines.push(\n emitPerfReport(out.serial, {\n includeSamples: false,\n ...(priorSerial !== undefined ? { baseline: priorSerial } : {}),\n }),\n );\n });\n\n mkdirSync(path.dirname(input.reportPath), { recursive: true });\n writeFileSync(input.reportPath, `${lines.join('\\n')}\\n`, 'utf8');\n}\n\n/**\n * resolveKiwaRepoRoot — walk upward from `start` until finding a package.json\n * whose `name` matches `kiwa-monorepo`. Used by every kiwa perf test to\n * resolve the report path regardless of vitest cwd.\n */\nexport function resolveKiwaRepoRoot(start: string): string {\n let current = start;\n while (true) {\n const pkgPath = path.join(current, 'package.json');\n if (existsSync(pkgPath)) {\n const m = JSON.parse(readFileSync(pkgPath, 'utf8')) as { name?: string };\n if (m.name === 'kiwa-monorepo') return current;\n }\n const parent = path.dirname(current);\n if (parent === current) throw new Error(`Could not resolve repo root from ${start}`);\n current = parent;\n }\n}\n","/**\n * runPerf3LayerLive — 3-layer perf against a live third-party API.\n *\n * Companion to {@link runPerf3Layer}. Same shape, same reporting, same\n * baseline / regression semantics. Two behavioural differences:\n *\n * 1. **env-skip contract** — the caller declares which env vars are required\n * to reach the live API. When any required var is unset, the helper skips\n * the run and emits a `LIVE_ENV_MISSING` marker report (so CI-independent\n * perf sweeps can attribute empty results to missing credentials, not\n * silent success).\n * 2. **live thresholds** — the default cap is the provider's public SLA\n * (see docs/quality/perf-thresholds.md § Real-API measurement mode).\n * Concurrent multiplier stays 2×.\n *\n * Live runs cost money and are slow. Iterations default to 10 (vs 200 for\n * mock) so a full pass fits inside a coffee break. Concurrency defaults to\n * 3 so we don't rate-limit ourselves.\n */\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { measure } from './measure.js';\nimport { measureConcurrent } from './concurrent.js';\nimport { measureMemory } from './memory.js';\nimport { detectRegression } from './regression.js';\nimport { defaultBaselinePath, loadBaseline, saveBaseline } from './baseline.js';\nimport { evaluatePerfGate } from './gate.js';\nimport { emitPerfReport } from './report.js';\nimport type { MeasureResult } from './types.js';\nimport type { OpOutcome, PerfOpSpec } from './three-layer.js';\n\nexport interface LivePerfOpSpec extends PerfOpSpec {\n /**\n * Env vars that must all be set for this op to reach the live API.\n * When any is missing the op is skipped and reported as LIVE_ENV_MISSING.\n */\n requiredEnv: string[];\n}\n\nexport interface RunPerf3LayerLiveInput {\n moduleName: string;\n ops: LivePerfOpSpec[];\n reportPath: string;\n baselinePath?: string;\n serialIterations?: number;\n serialWarmup?: number;\n concurrency?: number;\n iterationsPerWorker?: number;\n memoryIterations?: number;\n thresholdDocLink?: string;\n}\n\nexport interface LiveOpOutcome extends Partial<OpOutcome> {\n name: string;\n skipped: boolean;\n skipReason: string | null;\n}\n\nexport interface RunPerf3LayerLiveResult {\n outcomes: LiveOpOutcome[];\n allPassed: boolean;\n anySkipped: boolean;\n baselineSeeded: boolean;\n}\n\nexport async function runPerf3LayerLive(\n input: RunPerf3LayerLiveInput,\n): Promise<RunPerf3LayerLiveResult> {\n const serialIterations = input.serialIterations ?? 10;\n const serialWarmup = input.serialWarmup ?? 1;\n const concurrency = input.concurrency ?? 3;\n const iterationsPerWorker = input.iterationsPerWorker ?? 3;\n const memoryIterations = input.memoryIterations ?? 20;\n const memoryCapDefault = 100 * 1024;\n const baselinePath =\n input.baselinePath ?? defaultBaselinePath(`${input.moduleName}.live`);\n const thresholdDocLink = input.thresholdDocLink ?? '../../quality/perf-thresholds';\n\n const priorBaseline = (await loadBaseline(baselinePath)) as unknown as Record<\n string,\n MeasureResult\n > | null;\n const combinedForBaseline: Record<string, MeasureResult> = {};\n const outcomes: LiveOpOutcome[] = [];\n let baselineSeeded = false;\n let anySkipped = false;\n\n for (const op of input.ops) {\n const missing = op.requiredEnv.filter((key) => !process.env[key]);\n if (missing.length > 0) {\n anySkipped = true;\n outcomes.push({\n name: op.name,\n skipped: true,\n skipReason: `LIVE_ENV_MISSING: ${missing.join(', ')}`,\n });\n continue;\n }\n\n const serial = await measure({\n name: `${op.name}.live.serial`,\n iterations: serialIterations,\n warmup: serialWarmup,\n fn: async () => {\n await op.fn();\n },\n });\n const concurrent = await measureConcurrent({\n name: `${op.name}.live.concurrent`,\n concurrency,\n iterationsPerWorker,\n warmup: 1,\n fn: async () => {\n await op.fn();\n },\n });\n const memory = await measureMemory({\n fn: async () => {\n await op.fn();\n },\n iterations: memoryIterations,\n });\n\n const concurrentCap = op.concurrentP95CapMs ?? op.serialP95CapMs * 2;\n const memoryCap = op.memoryArrayBuffersCapBytes ?? memoryCapDefault;\n\n const serialGate = evaluatePerfGate({\n result: serial,\n thresholds: { p95Ms: op.serialP95CapMs },\n });\n const concurrentGate = evaluatePerfGate({\n result: concurrent,\n thresholds: { p95Ms: concurrentCap },\n });\n const memoryGatePassed = memory.arrayBuffersDeltaBytes < memoryCap;\n\n const priorSerial = priorBaseline?.[`${op.name}.live.serial`];\n const regression = priorSerial\n ? detectRegression({ current: serial, baseline: priorSerial, threshold: 0.2 })\n : null;\n\n combinedForBaseline[`${op.name}.live.serial`] = serial;\n combinedForBaseline[`${op.name}.live.concurrent`] = concurrent;\n\n outcomes.push({\n name: op.name,\n skipped: false,\n skipReason: null,\n serial,\n concurrent,\n memory,\n serialGatePassed: serialGate.verdict.passed,\n concurrentGatePassed: concurrentGate.verdict.passed,\n memoryGatePassed,\n regressionVerdict: regression ? regression.verdict : 'n/a (baseline seeded)',\n });\n }\n\n const anyMeasured = outcomes.some((o) => !o.skipped);\n if (anyMeasured && priorBaseline === null) {\n await saveBaseline(baselinePath, combinedForBaseline as unknown as MeasureResult);\n baselineSeeded = true;\n }\n\n const allPassed = outcomes\n .filter((o) => !o.skipped)\n .every((o) => o.serialGatePassed && o.concurrentGatePassed && o.memoryGatePassed);\n\n writeLiveReport({\n reportPath: input.reportPath,\n moduleName: input.moduleName,\n outcomes,\n ops: input.ops,\n thresholdDocLink,\n priorBaseline,\n concurrency,\n iterationsPerWorker,\n memoryIterations,\n memoryCapDefault,\n });\n\n return { outcomes, allPassed, anySkipped, baselineSeeded };\n}\n\ninterface WriteLiveReportInput {\n reportPath: string;\n moduleName: string;\n outcomes: LiveOpOutcome[];\n ops: LivePerfOpSpec[];\n thresholdDocLink: string;\n priorBaseline: Record<string, MeasureResult> | null;\n concurrency: number;\n iterationsPerWorker: number;\n memoryIterations: number;\n memoryCapDefault: number;\n}\n\nfunction writeLiveReport(input: WriteLiveReportInput): void {\n const lines: string[] = [\n `# Perf Suite — ${input.moduleName} (LIVE)`,\n '',\n `Threshold source: [docs/quality/perf-thresholds.md § Real-API measurement mode](${input.thresholdDocLink})`,\n '',\n ];\n\n const skippedOps = input.outcomes.filter((o) => o.skipped);\n const measuredOps = input.outcomes.filter((o) => !o.skipped);\n\n if (skippedOps.length > 0) {\n lines.push('## Skipped ops (missing env)', '', '| op | reason |', '|---|---|');\n for (const o of skippedOps) lines.push(`| ${o.name} | ${o.skipReason} |`);\n lines.push('');\n }\n\n if (measuredOps.length === 0) {\n lines.push('_No live ops ran this pass. Set the required env vars to enable._');\n } else {\n lines.push('## Serial p95 (LIVE)', '');\n lines.push('| op | p95 | cap | gate | regression |');\n lines.push('|---|---|---|---|---|');\n input.ops.forEach((op) => {\n const out = input.outcomes.find((o) => o.name === op.name);\n if (!out || out.skipped || !out.serial) return;\n lines.push(\n `| ${op.name} | ${out.serial.p95.toFixed(2)}ms | ${op.serialP95CapMs}ms | ${out.serialGatePassed ? 'PASS' : 'FAIL'} | ${out.regressionVerdict} |`,\n );\n });\n\n lines.push(\n '',\n `## Concurrent p95 (LIVE, concurrency = ${input.concurrency})`,\n '',\n '| op | p95 | cap | gate |',\n '|---|---|---|---|',\n );\n input.ops.forEach((op) => {\n const out = input.outcomes.find((o) => o.name === op.name);\n if (!out || out.skipped || !out.concurrent) return;\n const cap = op.concurrentP95CapMs ?? op.serialP95CapMs * 2;\n lines.push(\n `| ${op.name} | ${out.concurrent.p95.toFixed(2)}ms | ${cap}ms | ${out.concurrentGatePassed ? 'PASS' : 'FAIL'} |`,\n );\n });\n\n lines.push('', '## Memory retention (LIVE)', '');\n lines.push('| op | heapUsed Δ | arrayBuffers Δ | cap | verdict |');\n lines.push('|---|---|---|---|---|');\n input.ops.forEach((op) => {\n const out = input.outcomes.find((o) => o.name === op.name);\n if (!out || out.skipped || !out.memory) return;\n const cap = op.memoryArrayBuffersCapBytes ?? input.memoryCapDefault;\n lines.push(\n `| ${op.name} | ${out.memory.heapUsedDeltaBytes} B | ${out.memory.arrayBuffersDeltaBytes} B | ${cap} B | ${out.memoryGatePassed ? 'PASS' : 'FAIL'} |`,\n );\n });\n\n lines.push('', '## Detailed serial reports', '');\n input.ops.forEach((op) => {\n const out = input.outcomes.find((o) => o.name === op.name);\n if (!out || out.skipped || !out.serial) return;\n lines.push(`### ${op.name}`);\n lines.push('');\n const priorSerial = input.priorBaseline?.[`${op.name}.live.serial`];\n lines.push(\n emitPerfReport(out.serial, {\n includeSamples: false,\n ...(priorSerial !== undefined ? { baseline: priorSerial } : {}),\n }),\n );\n });\n }\n\n mkdirSync(path.dirname(input.reportPath), { recursive: true });\n writeFileSync(input.reportPath, `${lines.join('\\n')}\\n`, 'utf8');\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACEA,eAAsB,QAAQ,OAA6C;AACzE,QAAM,SAAS,MAAM,UAAU;AAC/B,MAAI,MAAM,aAAa,GAAG;AACxB,UAAM,IAAI,MAAM,yCAAyC,MAAM,UAAU,EAAE;AAAA,EAC7E;AACA,MAAI,SAAS,GAAG;AACd,UAAM,IAAI,MAAM,qCAAqC,MAAM,EAAE;AAAA,EAC/D;AAEA,WAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,UAAM,MAAM,GAAG;AAAA,EACjB;AAEA,QAAM,UAAoB,CAAC;AAC3B,WAAS,QAAQ,GAAG,QAAQ,MAAM,YAAY,SAAS,GAAG;AACxD,UAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,UAAM,MAAM,GAAG;AACf,UAAM,MAAM,QAAQ,OAAO,OAAO;AAClC,YAAQ,KAAK,OAAO,MAAM,KAAK,IAAI,GAAS;AAAA,EAC9C;AAEA,SAAO,mBAAmB,MAAM,MAAM,MAAM,YAAY,QAAQ,OAAO;AACzE;AAEO,SAAS,mBACd,MACA,YACA,QACA,SACe;AACf,QAAM,SAAS,CAAC,GAAG,OAAO,EAAE,KAAK,CAAC,MAAM,UAAU,OAAO,KAAK;AAC9D,QAAM,UAAU,QAAQ,OAAO,CAAC,KAAK,WAAW,MAAM,QAAQ,CAAC;AAC/D,QAAM,OAAO,UAAU,QAAQ;AAC/B,QAAM,WAAW,QAAQ,SAAS,IAC9B,QAAQ,OAAO,CAAC,KAAK,WAAW;AAC9B,UAAM,QAAQ,SAAS;AACvB,WAAO,MAAO,QAAQ;AAAA,EACxB,GAAG,CAAC,KAAK,QAAQ,SAAS,KAC1B;AAEJ,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,KAAK,WAAW,QAAQ,GAAG;AAAA,IAC3B,KAAK,WAAW,QAAQ,IAAI;AAAA,IAC5B,KAAK,WAAW,QAAQ,IAAI;AAAA,IAC5B;AAAA,IACA,OAAO,KAAK,KAAK,QAAQ;AAAA,IACzB,OAAO,OAAO,CAAC,KAAK;AAAA,IACpB,OAAO,OAAO,OAAO,SAAS,CAAC,KAAK;AAAA,IACpC;AAAA,EACF;AACF;AAEA,SAAS,WAAW,QAAkB,OAAuB;AAC3D,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,OAAO,KAAK,IAAI,GAAG,KAAK,KAAK,OAAO,SAAS,KAAK,IAAI,CAAC;AAC7D,SAAO,OAAO,IAAI,KAAK,OAAO,OAAO,SAAS,CAAC,KAAK;AACtD;;;AC5BA,eAAsB,kBAAkB,OAAgD;AACtF,MAAI,MAAM,cAAc,GAAG;AACzB,UAAM,IAAI,MAAM,oDAAoD,MAAM,WAAW,EAAE;AAAA,EACzF;AACA,MAAI,MAAM,sBAAsB,GAAG;AACjC,UAAM,IAAI;AAAA,MACR,4DAA4D,MAAM,mBAAmB;AAAA,IACvF;AAAA,EACF;AACA,QAAM,SAAS,MAAM,UAAU;AAC/B,MAAI,SAAS,GAAG;AACd,UAAM,IAAI,MAAM,+CAA+C,MAAM,EAAE;AAAA,EACzE;AAEA,QAAM,SAAS,YAA+B;AAC5C,aAAS,QAAQ,GAAG,QAAQ,QAAQ,SAAS,GAAG;AAC9C,YAAM,MAAM,GAAG;AAAA,IACjB;AACA,UAAM,QAAkB,CAAC;AACzB,aAAS,QAAQ,GAAG,QAAQ,MAAM,qBAAqB,SAAS,GAAG;AACjE,YAAM,QAAQ,QAAQ,OAAO,OAAO;AACpC,YAAM,MAAM,GAAG;AACf,YAAM,MAAM,QAAQ,OAAO,OAAO;AAClC,YAAM,KAAK,OAAO,MAAM,KAAK,IAAI,GAAS;AAAA,IAC5C;AACA,WAAO;AAAA,EACT;AAEA,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,MAAM,YAAY,GAAG,MAAM,OAAO,CAAC;AACxE,QAAM,mBAAmB,MAAM,QAAQ,IAAI,OAAO;AAClD,QAAM,UAAU,iBAAiB,KAAK;AACtC,QAAM,kBAAkB,MAAM,cAAc,MAAM;AAClD,SAAO,mBAAmB,MAAM,MAAM,iBAAiB,SAAS,MAAM,aAAa,OAAO;AAC5F;;;ACxCA,eAAsB,cAAc,OAA2C;AAC7E,MAAI,MAAM,aAAa,GAAG;AACxB,UAAM,IAAI,MAAM,+CAA+C,MAAM,UAAU,EAAE;AAAA,EACnF;AAEA,QAAM,QAAS,WAAmC;AAClD,QAAM,YAAY,OAAO,UAAU;AAEnC,MAAI,UAAW,OAAO;AACtB,QAAM,SAAS,QAAQ,YAAY;AAEnC,WAAS,QAAQ,GAAG,QAAQ,MAAM,YAAY,SAAS,GAAG;AACxD,UAAM,MAAM,GAAG;AAAA,EACjB;AAEA,MAAI,UAAW,OAAO;AACtB,QAAM,QAAQ,QAAQ,YAAY;AAElC,QAAM,gBAAgB,MAAM,WAAW,OAAO;AAC9C,SAAO;AAAA,IACL,gBAAgB,MAAM;AAAA,IACtB,oBAAoB;AAAA,IACpB,gCAAgC,gBAAgB,MAAM;AAAA,IACtD,eAAe,MAAM,MAAM,OAAO;AAAA,IAClC,oBAAoB,MAAM,WAAW,OAAO;AAAA,IAC5C,wBAAwB,MAAM,eAAe,OAAO;AAAA,IACpD;AAAA,EACF;AACF;;;AChDO,SAAS,iBAAiB,OAA0C;AACzE,QAAM,YAAY,MAAM,aAAa;AACrC,QAAM,UAAU,UAAU,MAAM,OAAO;AACvC,QAAM,WAAW,UAAU,MAAM,QAAQ;AAEzC,QAAM,WAAW,SAAS,QAAQ,IAC9B,QAAQ,QAAQ,IACd,IACA,OAAO,qBACR,QAAQ,MAAM,SAAS,OAAO,SAAS;AAC5C,QAAM,SAAS,YAAY,QAAQ,SAAS,SAAS,OAAO;AAC5D,QAAM,cAAc,KAAK,IAAI,MAAM,IAAI;AAEvC,MAAI,UAAuC;AAC3C,MAAI,eAAe,YAAY,WAAW;AACxC,cAAU;AAAA,EACZ,WAAW,eAAe,YAAY,CAAC,WAAW;AAChD,cAAU;AAAA,EACZ;AAEA,SAAO;AAAA,IACL,WAAW,YAAY;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAEA,SAAS,UAAU,QAAsC;AACvD,MAAI,OAAO,QAAQ,WAAW,GAAG;AAC/B,WAAO;AAAA,EACT;AACA,SAAO,mBAAmB,OAAO,MAAM,OAAO,YAAY,OAAO,QAAQ,OAAO,OAAO;AACzF;AAEA,SAAS,YAAY,SAAmB,UAA4B;AAClE,MAAI,QAAQ,SAAS,KAAK,SAAS,SAAS,GAAG;AAC7C,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,YAAY,OAAO;AACxC,QAAM,gBAAgB,YAAY,QAAQ;AAC1C,QAAM,YAAY,aAAa,OAAO,cAAc;AACpD,QAAM,cAAc,KAAK;AAAA,IACtB,aAAa,WAAW,QAAQ,SAC9B,cAAc,WAAW,SAAS;AAAA,EACvC;AAEA,MAAI,CAAC,OAAO,SAAS,WAAW,KAAK,gBAAgB,GAAG;AACtD,WAAO;AAAA,EACT;AACA,SAAO,YAAY;AACrB;AAEA,SAAS,YAAY,SAAuD;AAC1E,QAAM,OAAO,QAAQ,OAAO,CAAC,KAAK,WAAW,MAAM,QAAQ,CAAC,IAAI,QAAQ;AACxE,QAAM,WAAW,QAAQ,OAAO,CAAC,KAAK,WAAW;AAC/C,UAAM,QAAQ,SAAS;AACvB,WAAO,MAAO,QAAQ;AAAA,EACxB,GAAG,CAAC,KAAK,QAAQ,SAAS;AAC1B,SAAO,EAAE,MAAM,SAAS;AAC1B;;;ACrEA,sBAA2C;AAC3C,uBAAwB;AAGxB,eAAsB,aAAaA,OAA6C;AAC9E,MAAI;AACF,UAAM,OAAO,UAAM,0BAASA,OAAM,MAAM;AACxC,WAAO,KAAK,MAAM,IAAI;AAAA,EACxB,SAAS,OAAO;AACd,QAAI,cAAc,KAAK,GAAG;AACxB,aAAO;AAAA,IACT;AACA,UAAM;AAAA,EACR;AACF;AAEA,eAAsB,aAAaA,OAAc,QAAsC;AACrF,YAAM,2BAAM,0BAAQA,KAAI,GAAG,EAAE,WAAW,KAAK,CAAC;AAC9C,YAAM,2BAAUA,OAAM,GAAG,KAAK,UAAU,QAAQ,MAAM,CAAC,CAAC;AAAA,GAAM,MAAM;AACtE;AAEO,SAAS,oBAAoB,YAA4B;AAC9D,SAAO,GAAG,QAAQ,IAAI,CAAC,mBAAmB,UAAU;AACtD;AAEA,SAAS,cAAc,OAAgD;AACrE,SAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,SAAS,MAAM,SAAS;AAC1F;;;AC3BA,6BAKO;AAOA,SAAS,iBAAiB,OAAsC;AACrE,QAAM,aAAa,MAAM,cAAc,CAAC;AACxC,QAAM,cAAc,gBAAgB,UAAU;AAC9C,QAAM,SAAS,YAAY,OAAO,YAAY,gBAAgB,CAAC;AAC/D,QAAM,qBAAiB,4CAAoB,QAAQ;AAAA,IACjD,cAAc;AAAA,IACd,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,IAClB,eAAe;AAAA,IACf,WAAW,WAAW,SAAS,OAAO;AAAA,IACtC,kBAAkB;AAAA,IAClB,eAAe;AAAA,EACjB,CAAC;AAED,MAAI,gBAAgB,GAAG;AACrB,WAAO;AAAA,MACL;AAAA,MACA,SAAS,EAAE,QAAQ,MAAM,UAAU,CAAC,GAAG,eAAe,EAAE;AAAA,MACxD,UAAU,CAAC;AAAA,IACb;AAAA,EACF;AAEA,QAAM,WAAiC,CAAC;AACxC,MAAI,WAAW,UAAU,UAAa,eAAe,SAAS,KAAK,CAAC,YAAY,QAAQ,SAAS,YAAY,GAAG;AAC9G,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,WAAW,WAAW;AAAA,MACtB,QAAQ,MAAM,OAAO;AAAA,MACrB,IAAI;AAAA,IACN,CAAC;AAAA,EACH;AAEA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,MAAM,SAAS;AAAA,IACf,OAAO;AAAA,EACT;AACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,MAAM,SAAS;AAAA,IACf,OAAO;AAAA,EACT;AACA;AAAA,IACE;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW;AAAA,IACX,MAAM,SAAS;AAAA,IACf,OAAO;AAAA,EACT;AAEA,QAAM,UAA8B;AAAA,IAClC,QAAQ,SAAS,WAAW;AAAA,IAC5B,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AAEA,SAAO,EAAE,QAAQ,SAAS,SAAS;AACrC;AAEA,SAAS,YACP,OACA,YACA,OACe;AACf,QAAM,OAAO,SAAS,WAAW,UAAU,SACvC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,EAAE,IAC3C;AAAA,IACE,OAAO,MAAM,OAAO;AAAA,IACpB,OAAO,MAAM,OAAO;AAAA,IACpB,OAAO,MAAM,OAAO;AAAA,IACpB,SAAS,MAAM,OAAO,QAAQ;AAAA,EAChC;AACJ,QAAM,SAAwB;AAAA,IAC5B,UAAU,2BAA2B,MAAM,OAAO,IAAI;AAAA,IACtD,SAAS;AAAA,IACT,aAAY,oBAAI,KAAK,GAAE,YAAY;AAAA,IACnC,UAAU,EAAE,MAAM,GAAG,QAAQ,GAAG,UAAU,EAAE;AAAA,IAC5C,WAAW,EAAE,UAAU,GAAG,aAAa,GAAG,KAAK,GAAG,OAAO,EAAE;AAAA,IAC3D,UAAU,EAAE,oBAAoB,GAAG,kBAAkB,GAAG,OAAO,EAAE;AAAA,IACjE;AAAA,IACA,UAAU,EAAE,WAAW,GAAG,QAAQ,GAAG,UAAU,GAAG,UAAU,EAAE;AAAA,EAChE;AACA,MAAI,CAAC,SAAS,WAAW,YAAY,QAAW;AAC9C,UAAM,SAAS,MAAM,SAAS,WAAW;AACzC,WAAO,OAAO,EAAE,eAAe,QAAQ,UAAU,QAAQ,UAAU,EAAE;AAAA,EACvE;AACA,MAAI,CAAC,SAAS,WAAW,WAAW,QAAW;AAC7C,UAAM,SAAS,MAAM,SAAS,UAAU;AACxC,WAAO,QAAQ;AAAA,MACb,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,aAAa;AAAA,MACb,UAAU;AAAA,IACZ;AAAA,EACF;AACA,MAAI,CAAC,SAAS,WAAW,aAAa,QAAW;AAC/C,WAAO,WAAW;AAAA,MAChB,OAAO,MAAM,SAAS,YAAY;AAAA,MAClC,SAAS;AAAA,MACT,QAAQ;AAAA,IACV;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,qBACP,UACA,MACA,IACA,WACA,QACA,iBACM;AACN,MAAI,cAAc,QAAW;AAC3B;AAAA,EACF;AACA,QAAM,iBAAiB,UAAU;AACjC,QAAM,SAAS,OAAO,OAAO,kBAAkB,YAAY,kBAAkB;AAC7E,MAAI,CAAC,QAAQ;AACX,aAAS,KAAK;AAAA,MACZ;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAS,gBAAgB,YAAgC;AACvD,SAAO;AAAA,IACL,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,IACX,WAAW;AAAA,EACb,EAAE,OAAO,CAAC,UAAU,UAAU,MAAS,EAAE;AAC3C;;;ACxJO,SAAS,eACd,QACA,OAGI,CAAC,GACG;AACR,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,wBAAmB,OAAO,IAAI,EAAE;AAC3C,QAAM,KAAK,EAAE;AACb,QAAM,KAAK,oBAAoB;AAC/B,QAAM,KAAK,WAAW;AACtB,QAAM,KAAK,kBAAkB,OAAO,UAAU,IAAI;AAClD,QAAM,KAAK,cAAc,OAAO,MAAM,IAAI;AAC1C,QAAM,KAAK,WAAW,SAAS,OAAO,GAAG,CAAC,IAAI;AAC9C,QAAM,KAAK,WAAW,SAAS,OAAO,GAAG,CAAC,IAAI;AAC9C,QAAM,KAAK,WAAW,SAAS,OAAO,GAAG,CAAC,IAAI;AAC9C,QAAM,KAAK,YAAY,SAAS,OAAO,IAAI,CAAC,IAAI;AAChD,QAAM,KAAK,aAAa,SAAS,OAAO,KAAK,CAAC,IAAI;AAClD,QAAM,KAAK,WAAW,SAAS,OAAO,KAAK,CAAC,IAAI;AAChD,QAAM,KAAK,WAAW,SAAS,OAAO,KAAK,CAAC,IAAI;AAChD,QAAM,KAAK,aAAa,SAAS,OAAO,OAAO,CAAC,IAAI;AACpD,QAAM,KAAK,EAAE;AAEb,MAAI,KAAK,UAAU;AACjB,UAAM,UAAU;AAAA,MACd,EAAE,OAAO,OAAO,SAAS,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI;AAAA,MACjE,EAAE,OAAO,OAAO,SAAS,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI;AAAA,MACjE,EAAE,OAAO,OAAO,SAAS,OAAO,KAAK,UAAU,KAAK,SAAS,IAAI;AAAA,MACjE,EAAE,OAAO,QAAQ,SAAS,OAAO,MAAM,UAAU,KAAK,SAAS,KAAK;AAAA,MACpE,EAAE,OAAO,OAAO,SAAS,OAAO,OAAO,UAAU,KAAK,SAAS,MAAM;AAAA,MACrE,EAAE,OAAO,OAAO,SAAS,OAAO,OAAO,UAAU,KAAK,SAAS,MAAM;AAAA,MACrE,EAAE,OAAO,SAAS,SAAS,OAAO,SAAS,UAAU,KAAK,SAAS,QAAQ;AAAA,IAC7E;AACA,UAAM,KAAK,kBAAkB;AAC7B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,sDAAsD;AACjE,UAAM,KAAK,uBAAuB;AAClC,eAAW,UAAU,SAAS;AAC5B,YAAM,UAAU,OAAO,UAAU,OAAO;AACxC,YAAM,WAAW,OAAO,aAAa,IAAI,IAAK,UAAU,OAAO,WAAY;AAC3E,YAAM;AAAA,QACJ,KAAK,OAAO,KAAK,MAAM,SAAS,OAAO,OAAO,CAAC,MAAM,SAAS,OAAO,QAAQ,CAAC,MAAM,eAAe,OAAO,CAAC,MAAM,gBAAgB,QAAQ,CAAC;AAAA,MAC5I;AAAA,IACF;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,KAAK,gBAAgB;AACvB,UAAM,KAAK,sBAAsB;AACjC,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,kCAAkC;AAC7C,UAAM,KAAK,mBAAmB;AAC9B,eAAW,OAAO,cAAc,OAAO,SAAS,EAAE,GAAG;AACnD,YAAM,KAAK,KAAK,IAAI,KAAK,MAAM,IAAI,KAAK,MAAM,IAAI,KAAK,MAAM,IAAI,GAAG,IAAI;AAAA,IAC1E;AACA,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEA,SAAS,cAAc,SAAmB,MAKvC;AACD,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,MAAM,KAAK,IAAI,GAAG,OAAO;AAC/B,QAAM,MAAM,KAAK,IAAI,GAAG,OAAO;AAC/B,QAAM,QAAQ,QAAQ,MAAM,KAAK,MAAM,OAAO;AAC9C,QAAM,SAAS,IAAI,MAAc,IAAI,EAAE,KAAK,CAAC;AAE7C,aAAW,UAAU,SAAS;AAC5B,UAAM,WAAW,UAAU,IAAI,IAAI,KAAK,OAAO,SAAS,OAAO,KAAK;AACpE,UAAM,QAAQ,KAAK,IAAI,OAAO,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;AACtD,WAAO,KAAK,KAAK,OAAO,KAAK,KAAK,KAAK;AAAA,EACzC;AAEA,QAAM,OAAO,KAAK,IAAI,GAAG,QAAQ,CAAC;AAClC,SAAO,OAAO,IAAI,CAAC,OAAO,UAAU;AAClC,UAAM,QAAQ,MAAO,QAAQ;AAC7B,UAAM,MAAM,UAAU,OAAO,IAAI,MAAM,QAAQ;AAC/C,WAAO;AAAA,MACL,OAAO,QAAQ;AAAA,MACf,OAAO,GAAG,MAAM,QAAQ,CAAC,CAAC,IAAI,IAAI,QAAQ,CAAC,CAAC;AAAA,MAC5C;AAAA,MACA,KAAK,IAAI,OAAO,UAAU,IAAI,IAAI,KAAK,IAAI,GAAG,KAAK,MAAO,QAAQ,OAAQ,EAAE,CAAC,CAAC;AAAA,IAChF;AAAA,EACF,CAAC;AACH;AAEA,SAAS,SAAS,OAAuB;AACvC,SAAO,GAAG,MAAM,QAAQ,CAAC,CAAC;AAC5B;AAEA,SAAS,eAAe,OAAuB;AAC7C,QAAM,OAAO,QAAQ,IAAI,MAAM;AAC/B,SAAO,GAAG,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC;AACnC;AAEA,SAAS,gBAAgB,OAAuB;AAC9C,QAAM,OAAO,QAAQ,IAAI,MAAM;AAC/B,SAAO,GAAG,IAAI,GAAG,MAAM,QAAQ,CAAC,CAAC;AACnC;;;AC7FA,qBAAmE;AACnE,IAAAC,oBAAiB;AAmFjB,eAAsB,cAAc,OAAyD;AAC3F,QAAM,mBAAmB,MAAM,oBAAoB;AACnD,QAAM,eAAe,MAAM,gBAAgB;AAC3C,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,sBAAsB,MAAM,uBAAuB;AACzD,QAAM,mBAAmB,MAAM,oBAAoB;AACnD,QAAM,mBAAmB,MAAM;AAC/B,QAAM,eAAe,MAAM,gBAAgB,oBAAoB,MAAM,UAAU;AAC/E,QAAM,mBAAmB,MAAM,oBAAoB;AAEnD,QAAM,gBAAiB,MAAM,aAAa,YAAY;AACtD,QAAM,sBAAqD,CAAC;AAC5D,QAAM,WAAwB,CAAC;AAE/B,aAAW,MAAM,MAAM,KAAK;AAC1B,UAAM,SAAS,MAAM,QAAQ;AAAA,MAC3B,MAAM,GAAG,GAAG,IAAI;AAAA,MAChB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,IAAI,YAAY;AACd,cAAM,GAAG,GAAG;AAAA,MACd;AAAA,IACF,CAAC;AACD,UAAM,aAAa,MAAM,kBAAkB;AAAA,MACzC,MAAM,GAAG,GAAG,IAAI;AAAA,MAChB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,IAAI,YAAY;AACd,cAAM,GAAG,GAAG;AAAA,MACd;AAAA,IACF,CAAC;AACD,UAAM,SAAS,MAAM,cAAc;AAAA,MACjC,IAAI,YAAY;AACd,cAAM,GAAG,GAAG;AAAA,MACd;AAAA,MACA,YAAY;AAAA,IACd,CAAC;AAED,UAAM,gBAAgB,GAAG,sBAAsB,GAAG,iBAAiB;AACnE,UAAM,YAAY,GAAG,8BAA8B;AAEnD,UAAM,aAAa,iBAAiB;AAAA,MAClC,QAAQ;AAAA,MACR,YAAY,EAAE,OAAO,GAAG,eAAe;AAAA,IACzC,CAAC;AACD,UAAM,iBAAiB,iBAAiB;AAAA,MACtC,QAAQ;AAAA,MACR,YAAY,EAAE,OAAO,cAAc;AAAA,IACrC,CAAC;AACD,UAAM,mBAAmB,OAAO,yBAAyB;AAEzD,UAAM,cAAc,gBAAgB,GAAG,GAAG,IAAI,SAAS;AACvD,UAAM,aAAa,cACf,iBAAiB,EAAE,SAAS,QAAQ,UAAU,aAAa,WAAW,IAAI,CAAC,IAC3E;AAEJ,wBAAoB,GAAG,GAAG,IAAI,SAAS,IAAI;AAC3C,wBAAoB,GAAG,GAAG,IAAI,aAAa,IAAI;AAE/C,aAAS,KAAK;AAAA,MACZ,MAAM,GAAG;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA,kBAAkB,WAAW,QAAQ;AAAA,MACrC,sBAAsB,eAAe,QAAQ;AAAA,MAC7C;AAAA,MACA,mBAAmB,aAAa,WAAW,UAAU;AAAA,IACvD,CAAC;AAAA,EACH;AAEA,QAAM,iBAAiB,kBAAkB;AACzC,MAAI,gBAAgB;AAClB,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,IACF;AAAA,EACF;AAEA,QAAM,YAAY,SAAS;AAAA,IACzB,CAAC,MAAM,EAAE,oBAAoB,EAAE,wBAAwB,EAAE;AAAA,EAC3D;AAEA,cAAY;AAAA,IACV,YAAY,MAAM;AAAA,IAClB,YAAY,MAAM;AAAA,IAClB;AAAA,IACA,KAAK,MAAM;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO,EAAE,UAAU,WAAW,eAAe;AAC/C;AAeA,SAAS,YAAY,OAA+B;AAClD,QAAM,QAAkB;AAAA,IACtB,uBAAkB,MAAM,UAAU;AAAA,IAClC;AAAA,IACA,uDAAuD,MAAM,gBAAgB;AAAA,IAC7E;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC7B,UAAM,MAAM,MAAM,SAAS,GAAG;AAC9B,UAAM;AAAA,MACJ,KAAK,GAAG,IAAI,MAAM,IAAI,OAAO,IAAI,QAAQ,CAAC,CAAC,QAAQ,GAAG,cAAc,QAAQ,IAAI,mBAAmB,SAAS,MAAM,MAAM,IAAI,iBAAiB;AAAA,IAC/I;AAAA,EACF,CAAC;AAED,QAAM;AAAA,IACJ;AAAA,IACA,oCAAoC,MAAM,WAAW,KAAK,MAAM,mBAAmB;AAAA,IACnF;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC7B,UAAM,MAAM,MAAM,SAAS,GAAG;AAC9B,UAAM,MAAM,GAAG,sBAAsB,GAAG,iBAAiB;AACzD,UAAM;AAAA,MACJ,KAAK,GAAG,IAAI,MAAM,IAAI,WAAW,IAAI,QAAQ,CAAC,CAAC,QAAQ,GAAG,QAAQ,IAAI,uBAAuB,SAAS,MAAM;AAAA,IAC9G;AAAA,EACF,CAAC;AAED,QAAM;AAAA,IACJ;AAAA,IACA,wBAAwB,MAAM,gBAAgB;AAAA,IAC9C;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACA,QAAM,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC7B,UAAM,MAAM,MAAM,SAAS,GAAG;AAC9B,UAAM,MAAM,GAAG,8BAA8B,MAAM;AACnD,UAAM;AAAA,MACJ,KAAK,GAAG,IAAI,MAAM,IAAI,OAAO,kBAAkB,QAAQ,IAAI,OAAO,sBAAsB,QAAQ,GAAG,QAAQ,IAAI,mBAAmB,SAAS,MAAM;AAAA,IACnJ;AAAA,EACF,CAAC;AAED,QAAM,KAAK,IAAI,8BAA8B,EAAE;AAC/C,QAAM,IAAI,QAAQ,CAAC,IAAI,QAAQ;AAC7B,UAAM,MAAM,MAAM,SAAS,GAAG;AAC9B,UAAM,KAAK,OAAO,GAAG,IAAI,EAAE;AAC3B,UAAM,KAAK,EAAE;AACb,UAAM,cAAc,MAAM,gBAAgB,GAAG,GAAG,IAAI,SAAS;AAC7D,UAAM;AAAA,MACJ,eAAe,IAAI,QAAQ;AAAA,QACzB,gBAAgB;AAAA,QAChB,GAAI,gBAAgB,SAAY,EAAE,UAAU,YAAY,IAAI,CAAC;AAAA,MAC/D,CAAC;AAAA,IACH;AAAA,EACF,CAAC;AAED,gCAAU,kBAAAC,QAAK,QAAQ,MAAM,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7D,oCAAc,MAAM,YAAY,GAAG,MAAM,KAAK,IAAI,CAAC;AAAA,GAAM,MAAM;AACjE;AAOO,SAAS,oBAAoB,OAAuB;AACzD,MAAI,UAAU;AACd,SAAO,MAAM;AACX,UAAM,UAAU,kBAAAA,QAAK,KAAK,SAAS,cAAc;AACjD,YAAI,2BAAW,OAAO,GAAG;AACvB,YAAM,IAAI,KAAK,UAAM,6BAAa,SAAS,MAAM,CAAC;AAClD,UAAI,EAAE,SAAS,gBAAiB,QAAO;AAAA,IACzC;AACA,UAAM,SAAS,kBAAAA,QAAK,QAAQ,OAAO;AACnC,QAAI,WAAW,QAAS,OAAM,IAAI,MAAM,oCAAoC,KAAK,EAAE;AACnF,cAAU;AAAA,EACZ;AACF;;;ACtRA,IAAAC,kBAAmE;AACnE,IAAAC,oBAAiB;AA6CjB,eAAsB,kBACpB,OACkC;AAClC,QAAM,mBAAmB,MAAM,oBAAoB;AACnD,QAAM,eAAe,MAAM,gBAAgB;AAC3C,QAAM,cAAc,MAAM,eAAe;AACzC,QAAM,sBAAsB,MAAM,uBAAuB;AACzD,QAAM,mBAAmB,MAAM,oBAAoB;AACnD,QAAM,mBAAmB,MAAM;AAC/B,QAAM,eACJ,MAAM,gBAAgB,oBAAoB,GAAG,MAAM,UAAU,OAAO;AACtE,QAAM,mBAAmB,MAAM,oBAAoB;AAEnD,QAAM,gBAAiB,MAAM,aAAa,YAAY;AAItD,QAAM,sBAAqD,CAAC;AAC5D,QAAM,WAA4B,CAAC;AACnC,MAAI,iBAAiB;AACrB,MAAI,aAAa;AAEjB,aAAW,MAAM,MAAM,KAAK;AAC1B,UAAM,UAAU,GAAG,YAAY,OAAO,CAAC,QAAQ,CAAC,QAAQ,IAAI,GAAG,CAAC;AAChE,QAAI,QAAQ,SAAS,GAAG;AACtB,mBAAa;AACb,eAAS,KAAK;AAAA,QACZ,MAAM,GAAG;AAAA,QACT,SAAS;AAAA,QACT,YAAY,qBAAqB,QAAQ,KAAK,IAAI,CAAC;AAAA,MACrD,CAAC;AACD;AAAA,IACF;AAEA,UAAM,SAAS,MAAM,QAAQ;AAAA,MAC3B,MAAM,GAAG,GAAG,IAAI;AAAA,MAChB,YAAY;AAAA,MACZ,QAAQ;AAAA,MACR,IAAI,YAAY;AACd,cAAM,GAAG,GAAG;AAAA,MACd;AAAA,IACF,CAAC;AACD,UAAM,aAAa,MAAM,kBAAkB;AAAA,MACzC,MAAM,GAAG,GAAG,IAAI;AAAA,MAChB;AAAA,MACA;AAAA,MACA,QAAQ;AAAA,MACR,IAAI,YAAY;AACd,cAAM,GAAG,GAAG;AAAA,MACd;AAAA,IACF,CAAC;AACD,UAAM,SAAS,MAAM,cAAc;AAAA,MACjC,IAAI,YAAY;AACd,cAAM,GAAG,GAAG;AAAA,MACd;AAAA,MACA,YAAY;AAAA,IACd,CAAC;AAED,UAAM,gBAAgB,GAAG,sBAAsB,GAAG,iBAAiB;AACnE,UAAM,YAAY,GAAG,8BAA8B;AAEnD,UAAM,aAAa,iBAAiB;AAAA,MAClC,QAAQ;AAAA,MACR,YAAY,EAAE,OAAO,GAAG,eAAe;AAAA,IACzC,CAAC;AACD,UAAM,iBAAiB,iBAAiB;AAAA,MACtC,QAAQ;AAAA,MACR,YAAY,EAAE,OAAO,cAAc;AAAA,IACrC,CAAC;AACD,UAAM,mBAAmB,OAAO,yBAAyB;AAEzD,UAAM,cAAc,gBAAgB,GAAG,GAAG,IAAI,cAAc;AAC5D,UAAM,aAAa,cACf,iBAAiB,EAAE,SAAS,QAAQ,UAAU,aAAa,WAAW,IAAI,CAAC,IAC3E;AAEJ,wBAAoB,GAAG,GAAG,IAAI,cAAc,IAAI;AAChD,wBAAoB,GAAG,GAAG,IAAI,kBAAkB,IAAI;AAEpD,aAAS,KAAK;AAAA,MACZ,MAAM,GAAG;AAAA,MACT,SAAS;AAAA,MACT,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA,kBAAkB,WAAW,QAAQ;AAAA,MACrC,sBAAsB,eAAe,QAAQ;AAAA,MAC7C;AAAA,MACA,mBAAmB,aAAa,WAAW,UAAU;AAAA,IACvD,CAAC;AAAA,EACH;AAEA,QAAM,cAAc,SAAS,KAAK,CAAC,MAAM,CAAC,EAAE,OAAO;AACnD,MAAI,eAAe,kBAAkB,MAAM;AACzC,UAAM,aAAa,cAAc,mBAA+C;AAChF,qBAAiB;AAAA,EACnB;AAEA,QAAM,YAAY,SACf,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO,EACxB,MAAM,CAAC,MAAM,EAAE,oBAAoB,EAAE,wBAAwB,EAAE,gBAAgB;AAElF,kBAAgB;AAAA,IACd,YAAY,MAAM;AAAA,IAClB,YAAY,MAAM;AAAA,IAClB;AAAA,IACA,KAAK,MAAM;AAAA,IACX;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,CAAC;AAED,SAAO,EAAE,UAAU,WAAW,YAAY,eAAe;AAC3D;AAeA,SAAS,gBAAgB,OAAmC;AAC1D,QAAM,QAAkB;AAAA,IACtB,uBAAkB,MAAM,UAAU;AAAA,IAClC;AAAA,IACA,sFAAmF,MAAM,gBAAgB;AAAA,IACzG;AAAA,EACF;AAEA,QAAM,aAAa,MAAM,SAAS,OAAO,CAAC,MAAM,EAAE,OAAO;AACzD,QAAM,cAAc,MAAM,SAAS,OAAO,CAAC,MAAM,CAAC,EAAE,OAAO;AAE3D,MAAI,WAAW,SAAS,GAAG;AACzB,UAAM,KAAK,gCAAgC,IAAI,mBAAmB,WAAW;AAC7E,eAAW,KAAK,WAAY,OAAM,KAAK,KAAK,EAAE,IAAI,MAAM,EAAE,UAAU,IAAI;AACxE,UAAM,KAAK,EAAE;AAAA,EACf;AAEA,MAAI,YAAY,WAAW,GAAG;AAC5B,UAAM,KAAK,mEAAmE;AAAA,EAChF,OAAO;AACL,UAAM,KAAK,wBAAwB,EAAE;AACrC,UAAM,KAAK,wCAAwC;AACnD,UAAM,KAAK,uBAAuB;AAClC,UAAM,IAAI,QAAQ,CAAC,OAAO;AACxB,YAAM,MAAM,MAAM,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;AACzD,UAAI,CAAC,OAAO,IAAI,WAAW,CAAC,IAAI,OAAQ;AACxC,YAAM;AAAA,QACJ,KAAK,GAAG,IAAI,MAAM,IAAI,OAAO,IAAI,QAAQ,CAAC,CAAC,QAAQ,GAAG,cAAc,QAAQ,IAAI,mBAAmB,SAAS,MAAM,MAAM,IAAI,iBAAiB;AAAA,MAC/I;AAAA,IACF,CAAC;AAED,UAAM;AAAA,MACJ;AAAA,MACA,0CAA0C,MAAM,WAAW;AAAA,MAC3D;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,UAAM,IAAI,QAAQ,CAAC,OAAO;AACxB,YAAM,MAAM,MAAM,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;AACzD,UAAI,CAAC,OAAO,IAAI,WAAW,CAAC,IAAI,WAAY;AAC5C,YAAM,MAAM,GAAG,sBAAsB,GAAG,iBAAiB;AACzD,YAAM;AAAA,QACJ,KAAK,GAAG,IAAI,MAAM,IAAI,WAAW,IAAI,QAAQ,CAAC,CAAC,QAAQ,GAAG,QAAQ,IAAI,uBAAuB,SAAS,MAAM;AAAA,MAC9G;AAAA,IACF,CAAC;AAED,UAAM,KAAK,IAAI,8BAA8B,EAAE;AAC/C,UAAM,KAAK,gEAAsD;AACjE,UAAM,KAAK,uBAAuB;AAClC,UAAM,IAAI,QAAQ,CAAC,OAAO;AACxB,YAAM,MAAM,MAAM,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;AACzD,UAAI,CAAC,OAAO,IAAI,WAAW,CAAC,IAAI,OAAQ;AACxC,YAAM,MAAM,GAAG,8BAA8B,MAAM;AACnD,YAAM;AAAA,QACJ,KAAK,GAAG,IAAI,MAAM,IAAI,OAAO,kBAAkB,QAAQ,IAAI,OAAO,sBAAsB,QAAQ,GAAG,QAAQ,IAAI,mBAAmB,SAAS,MAAM;AAAA,MACnJ;AAAA,IACF,CAAC;AAED,UAAM,KAAK,IAAI,8BAA8B,EAAE;AAC/C,UAAM,IAAI,QAAQ,CAAC,OAAO;AACxB,YAAM,MAAM,MAAM,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI;AACzD,UAAI,CAAC,OAAO,IAAI,WAAW,CAAC,IAAI,OAAQ;AACxC,YAAM,KAAK,OAAO,GAAG,IAAI,EAAE;AAC3B,YAAM,KAAK,EAAE;AACb,YAAM,cAAc,MAAM,gBAAgB,GAAG,GAAG,IAAI,cAAc;AAClE,YAAM;AAAA,QACJ,eAAe,IAAI,QAAQ;AAAA,UACzB,gBAAgB;AAAA,UAChB,GAAI,gBAAgB,SAAY,EAAE,UAAU,YAAY,IAAI,CAAC;AAAA,QAC/D,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAAA,EACH;AAEA,iCAAU,kBAAAC,QAAK,QAAQ,MAAM,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;AAC7D,qCAAc,MAAM,YAAY,GAAG,MAAM,KAAK,IAAI,CAAC;AAAA,GAAM,MAAM;AACjE;","names":["path","import_node_path","path","import_node_fs","import_node_path","path"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -57,6 +57,65 @@ interface PerfGateResult {
|
|
|
57
57
|
declare function measure(input: MeasureInput): Promise<MeasureResult>;
|
|
58
58
|
declare function buildMeasureResult(name: string, iterations: number, warmup: number, samples: number[]): MeasureResult;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* measureConcurrent — drive `fn` under a fixed concurrency load and record
|
|
62
|
+
* per-call latency.
|
|
63
|
+
*
|
|
64
|
+
* Real production traffic is not serial. A p95 that looks fine at
|
|
65
|
+
* `iterations = 200, concurrency = 1` (the default `measure`) can collapse
|
|
66
|
+
* once N clients hit the same code path at once because contention on the
|
|
67
|
+
* shared engine / recorder / queue kicks in.
|
|
68
|
+
*
|
|
69
|
+
* This helper spawns `concurrency` parallel workers, each of which loops
|
|
70
|
+
* `iterationsPerWorker` times. Total sample count = concurrency ×
|
|
71
|
+
* iterationsPerWorker. Every sample is a wall-clock per-call latency (from
|
|
72
|
+
* `process.hrtime.bigint()` around each `fn()` invocation).
|
|
73
|
+
*
|
|
74
|
+
* Returned {@link MeasureResult} has the same shape as `measure` so
|
|
75
|
+
* downstream regression / gate / report code does not need to branch.
|
|
76
|
+
*
|
|
77
|
+
* @param input.name identifier for the report
|
|
78
|
+
* @param input.fn the async unit to exercise
|
|
79
|
+
* @param input.concurrency number of parallel workers (must be >= 1)
|
|
80
|
+
* @param input.iterationsPerWorker per-worker loop count (must be >= 1)
|
|
81
|
+
* @param input.warmup discarded warmup iterations per worker (default 0)
|
|
82
|
+
*/
|
|
83
|
+
interface ConcurrentInput {
|
|
84
|
+
name: string;
|
|
85
|
+
fn: () => Promise<unknown> | unknown;
|
|
86
|
+
concurrency: number;
|
|
87
|
+
iterationsPerWorker: number;
|
|
88
|
+
warmup?: number;
|
|
89
|
+
}
|
|
90
|
+
declare function measureConcurrent(input: ConcurrentInput): Promise<MeasureResult>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* measureMemory — capture heap deltas around a target function.
|
|
94
|
+
*
|
|
95
|
+
* Real production concerns include memory growth per call. A p95 of 5ms is
|
|
96
|
+
* useless if every call leaks 100KB of retained heap. This helper wraps a
|
|
97
|
+
* target function with a global.gc() + process.memoryUsage() bracket so
|
|
98
|
+
* tests can assert on `heapUsedDelta` / `rssUsedDelta` per call.
|
|
99
|
+
*
|
|
100
|
+
* Requires Node to be launched with `--expose-gc` for stable readings.
|
|
101
|
+
* When GC is not exposed we fall back to a delta without forced GC — the
|
|
102
|
+
* numbers are noisier but the trend still catches egregious leaks.
|
|
103
|
+
*/
|
|
104
|
+
interface MemorySample {
|
|
105
|
+
iterationCount: number;
|
|
106
|
+
heapUsedDeltaBytes: number;
|
|
107
|
+
heapUsedDeltaPerIterationBytes: number;
|
|
108
|
+
rssDeltaBytes: number;
|
|
109
|
+
externalDeltaBytes: number;
|
|
110
|
+
arrayBuffersDeltaBytes: number;
|
|
111
|
+
gcExposed: boolean;
|
|
112
|
+
}
|
|
113
|
+
interface MemoryInput {
|
|
114
|
+
fn: () => Promise<unknown> | unknown;
|
|
115
|
+
iterations: number;
|
|
116
|
+
}
|
|
117
|
+
declare function measureMemory(input: MemoryInput): Promise<MemorySample>;
|
|
118
|
+
|
|
60
119
|
declare function detectRegression(input: RegressionInput): RegressionResult;
|
|
61
120
|
|
|
62
121
|
declare function loadBaseline(path: string): Promise<MeasureResult | null>;
|
|
@@ -70,4 +129,113 @@ declare function emitPerfReport(result: MeasureResult, opts?: {
|
|
|
70
129
|
includeSamples?: boolean;
|
|
71
130
|
}): string;
|
|
72
131
|
|
|
73
|
-
|
|
132
|
+
interface PerfOpSpec {
|
|
133
|
+
name: string;
|
|
134
|
+
fn: () => Promise<unknown> | unknown;
|
|
135
|
+
/**
|
|
136
|
+
* Serial p95 hard cap (ms). Source: docs/quality/perf-thresholds.md.
|
|
137
|
+
*/
|
|
138
|
+
serialP95CapMs: number;
|
|
139
|
+
/**
|
|
140
|
+
* Optional override for concurrent cap. Default = 2 × serial cap per SSOT.
|
|
141
|
+
*/
|
|
142
|
+
concurrentP95CapMs?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Optional override for memory arrayBuffers cap.
|
|
145
|
+
* Default = 100 KB across 200 iterations.
|
|
146
|
+
*/
|
|
147
|
+
memoryArrayBuffersCapBytes?: number;
|
|
148
|
+
}
|
|
149
|
+
interface RunPerf3LayerInput {
|
|
150
|
+
moduleName: string;
|
|
151
|
+
ops: PerfOpSpec[];
|
|
152
|
+
/**
|
|
153
|
+
* Absolute path to the markdown report file. Overwritten each run.
|
|
154
|
+
*/
|
|
155
|
+
reportPath: string;
|
|
156
|
+
/**
|
|
157
|
+
* Optional override for baseline path. Default = defaultBaselinePath(moduleName).
|
|
158
|
+
*/
|
|
159
|
+
baselinePath?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Iterations for the serial phase. Default 200.
|
|
162
|
+
*/
|
|
163
|
+
serialIterations?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Warmup iterations for the serial phase (discarded). Default 5.
|
|
166
|
+
*/
|
|
167
|
+
serialWarmup?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Worker count for the concurrent phase. Default 10.
|
|
170
|
+
*/
|
|
171
|
+
concurrency?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Per-worker iterations for the concurrent phase. Default 50.
|
|
174
|
+
*/
|
|
175
|
+
iterationsPerWorker?: number;
|
|
176
|
+
/**
|
|
177
|
+
* Iterations for the memory phase. Default 200.
|
|
178
|
+
*/
|
|
179
|
+
memoryIterations?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Path (relative to reportPath's directory tree) that the report references
|
|
182
|
+
* as the threshold SSOT. Default: '../../quality/perf-thresholds'.
|
|
183
|
+
*/
|
|
184
|
+
thresholdDocLink?: string;
|
|
185
|
+
}
|
|
186
|
+
interface OpOutcome {
|
|
187
|
+
name: string;
|
|
188
|
+
serial: MeasureResult;
|
|
189
|
+
concurrent: MeasureResult;
|
|
190
|
+
memory: MemorySample;
|
|
191
|
+
serialGatePassed: boolean;
|
|
192
|
+
concurrentGatePassed: boolean;
|
|
193
|
+
memoryGatePassed: boolean;
|
|
194
|
+
regressionVerdict: 'stable' | 'improved' | 'regressed' | 'n/a (baseline seeded)';
|
|
195
|
+
}
|
|
196
|
+
interface RunPerf3LayerResult {
|
|
197
|
+
outcomes: OpOutcome[];
|
|
198
|
+
allPassed: boolean;
|
|
199
|
+
baselineSeeded: boolean;
|
|
200
|
+
}
|
|
201
|
+
declare function runPerf3Layer(input: RunPerf3LayerInput): Promise<RunPerf3LayerResult>;
|
|
202
|
+
/**
|
|
203
|
+
* resolveKiwaRepoRoot — walk upward from `start` until finding a package.json
|
|
204
|
+
* whose `name` matches `kiwa-monorepo`. Used by every kiwa perf test to
|
|
205
|
+
* resolve the report path regardless of vitest cwd.
|
|
206
|
+
*/
|
|
207
|
+
declare function resolveKiwaRepoRoot(start: string): string;
|
|
208
|
+
|
|
209
|
+
interface LivePerfOpSpec extends PerfOpSpec {
|
|
210
|
+
/**
|
|
211
|
+
* Env vars that must all be set for this op to reach the live API.
|
|
212
|
+
* When any is missing the op is skipped and reported as LIVE_ENV_MISSING.
|
|
213
|
+
*/
|
|
214
|
+
requiredEnv: string[];
|
|
215
|
+
}
|
|
216
|
+
interface RunPerf3LayerLiveInput {
|
|
217
|
+
moduleName: string;
|
|
218
|
+
ops: LivePerfOpSpec[];
|
|
219
|
+
reportPath: string;
|
|
220
|
+
baselinePath?: string;
|
|
221
|
+
serialIterations?: number;
|
|
222
|
+
serialWarmup?: number;
|
|
223
|
+
concurrency?: number;
|
|
224
|
+
iterationsPerWorker?: number;
|
|
225
|
+
memoryIterations?: number;
|
|
226
|
+
thresholdDocLink?: string;
|
|
227
|
+
}
|
|
228
|
+
interface LiveOpOutcome extends Partial<OpOutcome> {
|
|
229
|
+
name: string;
|
|
230
|
+
skipped: boolean;
|
|
231
|
+
skipReason: string | null;
|
|
232
|
+
}
|
|
233
|
+
interface RunPerf3LayerLiveResult {
|
|
234
|
+
outcomes: LiveOpOutcome[];
|
|
235
|
+
allPassed: boolean;
|
|
236
|
+
anySkipped: boolean;
|
|
237
|
+
baselineSeeded: boolean;
|
|
238
|
+
}
|
|
239
|
+
declare function runPerf3LayerLive(input: RunPerf3LayerLiveInput): Promise<RunPerf3LayerLiveResult>;
|
|
240
|
+
|
|
241
|
+
export { type ConcurrentInput, type LiveOpOutcome, type LivePerfOpSpec, type MeasureInput, type MeasureResult, type MemoryInput, type MemorySample, type OpOutcome, type PerfGateInput, type PerfGateResult, type PerfOpSpec, type RegressionInput, type RegressionResult, type RunPerf3LayerInput, type RunPerf3LayerLiveInput, type RunPerf3LayerLiveResult, type RunPerf3LayerResult, type Thresholds, buildMeasureResult, defaultBaselinePath, detectRegression, emitPerfReport, evaluatePerfGate, loadBaseline, measure, measureConcurrent, measureMemory, resolveKiwaRepoRoot, runPerf3Layer, runPerf3LayerLive, saveBaseline };
|
package/dist/index.d.ts
CHANGED
|
@@ -57,6 +57,65 @@ interface PerfGateResult {
|
|
|
57
57
|
declare function measure(input: MeasureInput): Promise<MeasureResult>;
|
|
58
58
|
declare function buildMeasureResult(name: string, iterations: number, warmup: number, samples: number[]): MeasureResult;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* measureConcurrent — drive `fn` under a fixed concurrency load and record
|
|
62
|
+
* per-call latency.
|
|
63
|
+
*
|
|
64
|
+
* Real production traffic is not serial. A p95 that looks fine at
|
|
65
|
+
* `iterations = 200, concurrency = 1` (the default `measure`) can collapse
|
|
66
|
+
* once N clients hit the same code path at once because contention on the
|
|
67
|
+
* shared engine / recorder / queue kicks in.
|
|
68
|
+
*
|
|
69
|
+
* This helper spawns `concurrency` parallel workers, each of which loops
|
|
70
|
+
* `iterationsPerWorker` times. Total sample count = concurrency ×
|
|
71
|
+
* iterationsPerWorker. Every sample is a wall-clock per-call latency (from
|
|
72
|
+
* `process.hrtime.bigint()` around each `fn()` invocation).
|
|
73
|
+
*
|
|
74
|
+
* Returned {@link MeasureResult} has the same shape as `measure` so
|
|
75
|
+
* downstream regression / gate / report code does not need to branch.
|
|
76
|
+
*
|
|
77
|
+
* @param input.name identifier for the report
|
|
78
|
+
* @param input.fn the async unit to exercise
|
|
79
|
+
* @param input.concurrency number of parallel workers (must be >= 1)
|
|
80
|
+
* @param input.iterationsPerWorker per-worker loop count (must be >= 1)
|
|
81
|
+
* @param input.warmup discarded warmup iterations per worker (default 0)
|
|
82
|
+
*/
|
|
83
|
+
interface ConcurrentInput {
|
|
84
|
+
name: string;
|
|
85
|
+
fn: () => Promise<unknown> | unknown;
|
|
86
|
+
concurrency: number;
|
|
87
|
+
iterationsPerWorker: number;
|
|
88
|
+
warmup?: number;
|
|
89
|
+
}
|
|
90
|
+
declare function measureConcurrent(input: ConcurrentInput): Promise<MeasureResult>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* measureMemory — capture heap deltas around a target function.
|
|
94
|
+
*
|
|
95
|
+
* Real production concerns include memory growth per call. A p95 of 5ms is
|
|
96
|
+
* useless if every call leaks 100KB of retained heap. This helper wraps a
|
|
97
|
+
* target function with a global.gc() + process.memoryUsage() bracket so
|
|
98
|
+
* tests can assert on `heapUsedDelta` / `rssUsedDelta` per call.
|
|
99
|
+
*
|
|
100
|
+
* Requires Node to be launched with `--expose-gc` for stable readings.
|
|
101
|
+
* When GC is not exposed we fall back to a delta without forced GC — the
|
|
102
|
+
* numbers are noisier but the trend still catches egregious leaks.
|
|
103
|
+
*/
|
|
104
|
+
interface MemorySample {
|
|
105
|
+
iterationCount: number;
|
|
106
|
+
heapUsedDeltaBytes: number;
|
|
107
|
+
heapUsedDeltaPerIterationBytes: number;
|
|
108
|
+
rssDeltaBytes: number;
|
|
109
|
+
externalDeltaBytes: number;
|
|
110
|
+
arrayBuffersDeltaBytes: number;
|
|
111
|
+
gcExposed: boolean;
|
|
112
|
+
}
|
|
113
|
+
interface MemoryInput {
|
|
114
|
+
fn: () => Promise<unknown> | unknown;
|
|
115
|
+
iterations: number;
|
|
116
|
+
}
|
|
117
|
+
declare function measureMemory(input: MemoryInput): Promise<MemorySample>;
|
|
118
|
+
|
|
60
119
|
declare function detectRegression(input: RegressionInput): RegressionResult;
|
|
61
120
|
|
|
62
121
|
declare function loadBaseline(path: string): Promise<MeasureResult | null>;
|
|
@@ -70,4 +129,113 @@ declare function emitPerfReport(result: MeasureResult, opts?: {
|
|
|
70
129
|
includeSamples?: boolean;
|
|
71
130
|
}): string;
|
|
72
131
|
|
|
73
|
-
|
|
132
|
+
interface PerfOpSpec {
|
|
133
|
+
name: string;
|
|
134
|
+
fn: () => Promise<unknown> | unknown;
|
|
135
|
+
/**
|
|
136
|
+
* Serial p95 hard cap (ms). Source: docs/quality/perf-thresholds.md.
|
|
137
|
+
*/
|
|
138
|
+
serialP95CapMs: number;
|
|
139
|
+
/**
|
|
140
|
+
* Optional override for concurrent cap. Default = 2 × serial cap per SSOT.
|
|
141
|
+
*/
|
|
142
|
+
concurrentP95CapMs?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Optional override for memory arrayBuffers cap.
|
|
145
|
+
* Default = 100 KB across 200 iterations.
|
|
146
|
+
*/
|
|
147
|
+
memoryArrayBuffersCapBytes?: number;
|
|
148
|
+
}
|
|
149
|
+
interface RunPerf3LayerInput {
|
|
150
|
+
moduleName: string;
|
|
151
|
+
ops: PerfOpSpec[];
|
|
152
|
+
/**
|
|
153
|
+
* Absolute path to the markdown report file. Overwritten each run.
|
|
154
|
+
*/
|
|
155
|
+
reportPath: string;
|
|
156
|
+
/**
|
|
157
|
+
* Optional override for baseline path. Default = defaultBaselinePath(moduleName).
|
|
158
|
+
*/
|
|
159
|
+
baselinePath?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Iterations for the serial phase. Default 200.
|
|
162
|
+
*/
|
|
163
|
+
serialIterations?: number;
|
|
164
|
+
/**
|
|
165
|
+
* Warmup iterations for the serial phase (discarded). Default 5.
|
|
166
|
+
*/
|
|
167
|
+
serialWarmup?: number;
|
|
168
|
+
/**
|
|
169
|
+
* Worker count for the concurrent phase. Default 10.
|
|
170
|
+
*/
|
|
171
|
+
concurrency?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Per-worker iterations for the concurrent phase. Default 50.
|
|
174
|
+
*/
|
|
175
|
+
iterationsPerWorker?: number;
|
|
176
|
+
/**
|
|
177
|
+
* Iterations for the memory phase. Default 200.
|
|
178
|
+
*/
|
|
179
|
+
memoryIterations?: number;
|
|
180
|
+
/**
|
|
181
|
+
* Path (relative to reportPath's directory tree) that the report references
|
|
182
|
+
* as the threshold SSOT. Default: '../../quality/perf-thresholds'.
|
|
183
|
+
*/
|
|
184
|
+
thresholdDocLink?: string;
|
|
185
|
+
}
|
|
186
|
+
interface OpOutcome {
|
|
187
|
+
name: string;
|
|
188
|
+
serial: MeasureResult;
|
|
189
|
+
concurrent: MeasureResult;
|
|
190
|
+
memory: MemorySample;
|
|
191
|
+
serialGatePassed: boolean;
|
|
192
|
+
concurrentGatePassed: boolean;
|
|
193
|
+
memoryGatePassed: boolean;
|
|
194
|
+
regressionVerdict: 'stable' | 'improved' | 'regressed' | 'n/a (baseline seeded)';
|
|
195
|
+
}
|
|
196
|
+
interface RunPerf3LayerResult {
|
|
197
|
+
outcomes: OpOutcome[];
|
|
198
|
+
allPassed: boolean;
|
|
199
|
+
baselineSeeded: boolean;
|
|
200
|
+
}
|
|
201
|
+
declare function runPerf3Layer(input: RunPerf3LayerInput): Promise<RunPerf3LayerResult>;
|
|
202
|
+
/**
|
|
203
|
+
* resolveKiwaRepoRoot — walk upward from `start` until finding a package.json
|
|
204
|
+
* whose `name` matches `kiwa-monorepo`. Used by every kiwa perf test to
|
|
205
|
+
* resolve the report path regardless of vitest cwd.
|
|
206
|
+
*/
|
|
207
|
+
declare function resolveKiwaRepoRoot(start: string): string;
|
|
208
|
+
|
|
209
|
+
interface LivePerfOpSpec extends PerfOpSpec {
|
|
210
|
+
/**
|
|
211
|
+
* Env vars that must all be set for this op to reach the live API.
|
|
212
|
+
* When any is missing the op is skipped and reported as LIVE_ENV_MISSING.
|
|
213
|
+
*/
|
|
214
|
+
requiredEnv: string[];
|
|
215
|
+
}
|
|
216
|
+
interface RunPerf3LayerLiveInput {
|
|
217
|
+
moduleName: string;
|
|
218
|
+
ops: LivePerfOpSpec[];
|
|
219
|
+
reportPath: string;
|
|
220
|
+
baselinePath?: string;
|
|
221
|
+
serialIterations?: number;
|
|
222
|
+
serialWarmup?: number;
|
|
223
|
+
concurrency?: number;
|
|
224
|
+
iterationsPerWorker?: number;
|
|
225
|
+
memoryIterations?: number;
|
|
226
|
+
thresholdDocLink?: string;
|
|
227
|
+
}
|
|
228
|
+
interface LiveOpOutcome extends Partial<OpOutcome> {
|
|
229
|
+
name: string;
|
|
230
|
+
skipped: boolean;
|
|
231
|
+
skipReason: string | null;
|
|
232
|
+
}
|
|
233
|
+
interface RunPerf3LayerLiveResult {
|
|
234
|
+
outcomes: LiveOpOutcome[];
|
|
235
|
+
allPassed: boolean;
|
|
236
|
+
anySkipped: boolean;
|
|
237
|
+
baselineSeeded: boolean;
|
|
238
|
+
}
|
|
239
|
+
declare function runPerf3LayerLive(input: RunPerf3LayerLiveInput): Promise<RunPerf3LayerLiveResult>;
|
|
240
|
+
|
|
241
|
+
export { type ConcurrentInput, type LiveOpOutcome, type LivePerfOpSpec, type MeasureInput, type MeasureResult, type MemoryInput, type MemorySample, type OpOutcome, type PerfGateInput, type PerfGateResult, type PerfOpSpec, type RegressionInput, type RegressionResult, type RunPerf3LayerInput, type RunPerf3LayerLiveInput, type RunPerf3LayerLiveResult, type RunPerf3LayerResult, type Thresholds, buildMeasureResult, defaultBaselinePath, detectRegression, emitPerfReport, evaluatePerfGate, loadBaseline, measure, measureConcurrent, measureMemory, resolveKiwaRepoRoot, runPerf3Layer, runPerf3LayerLive, saveBaseline };
|