agentsys 5.0.3 → 5.1.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/.claude-plugin/marketplace.json +21 -14
- package/.claude-plugin/plugin.json +1 -1
- package/AGENTS.md +2 -1
- package/CHANGELOG.md +18 -0
- package/README.md +7 -6
- package/adapters/codex/skills/agnix/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project-agents/SKILL.md +0 -1
- package/adapters/codex/skills/audit-project-github/SKILL.md +0 -1
- package/adapters/codex/skills/consult/SKILL.md +132 -57
- package/adapters/codex/skills/debate/SKILL.md +214 -0
- package/adapters/codex/skills/delivery-approval/SKILL.md +0 -1
- package/adapters/codex/skills/deslop/SKILL.md +0 -1
- package/adapters/codex/skills/drift-detect/SKILL.md +0 -1
- package/adapters/codex/skills/enhance/SKILL.md +0 -1
- package/adapters/codex/skills/learn/SKILL.md +0 -1
- package/adapters/codex/skills/next-task/SKILL.md +0 -1
- package/adapters/codex/skills/perf/SKILL.md +0 -1
- package/adapters/codex/skills/repo-map/SKILL.md +0 -1
- package/adapters/codex/skills/ship/SKILL.md +0 -1
- package/adapters/codex/skills/ship-ci-review-loop/SKILL.md +0 -1
- package/adapters/codex/skills/ship-deployment/SKILL.md +0 -1
- package/adapters/codex/skills/ship-error-handling/SKILL.md +0 -1
- package/adapters/codex/skills/sync-docs/SKILL.md +0 -1
- package/adapters/opencode/agents/agent-enhancer.md +0 -1
- package/adapters/opencode/agents/agnix-agent.md +0 -1
- package/adapters/opencode/agents/ci-fixer.md +0 -1
- package/adapters/opencode/agents/ci-monitor.md +0 -1
- package/adapters/opencode/agents/claudemd-enhancer.md +0 -1
- package/adapters/opencode/agents/consult-agent.md +122 -30
- package/adapters/opencode/agents/cross-file-enhancer.md +0 -1
- package/adapters/opencode/agents/debate-orchestrator.md +169 -0
- package/adapters/opencode/agents/delivery-validator.md +0 -1
- package/adapters/opencode/agents/deslop-agent.md +0 -1
- package/adapters/opencode/agents/docs-enhancer.md +0 -1
- package/adapters/opencode/agents/exploration-agent.md +0 -1
- package/adapters/opencode/agents/hooks-enhancer.md +0 -1
- package/adapters/opencode/agents/implementation-agent.md +0 -1
- package/adapters/opencode/agents/learn-agent.md +0 -1
- package/adapters/opencode/agents/map-validator.md +0 -1
- package/adapters/opencode/agents/perf-analyzer.md +0 -1
- package/adapters/opencode/agents/perf-code-paths.md +0 -1
- package/adapters/opencode/agents/perf-investigation-logger.md +0 -1
- package/adapters/opencode/agents/perf-orchestrator.md +0 -1
- package/adapters/opencode/agents/perf-theory-gatherer.md +0 -1
- package/adapters/opencode/agents/perf-theory-tester.md +0 -1
- package/adapters/opencode/agents/plan-synthesizer.md +0 -1
- package/adapters/opencode/agents/planning-agent.md +0 -1
- package/adapters/opencode/agents/plugin-enhancer.md +0 -1
- package/adapters/opencode/agents/prompt-enhancer.md +0 -1
- package/adapters/opencode/agents/simple-fixer.md +0 -1
- package/adapters/opencode/agents/skills-enhancer.md +0 -1
- package/adapters/opencode/agents/sync-docs-agent.md +0 -1
- package/adapters/opencode/agents/task-discoverer.md +0 -1
- package/adapters/opencode/agents/test-coverage-checker.md +0 -1
- package/adapters/opencode/agents/worktree-manager.md +0 -1
- package/adapters/opencode/commands/agnix.md +0 -1
- package/adapters/opencode/commands/audit-project-agents.md +0 -1
- package/adapters/opencode/commands/audit-project-github.md +0 -1
- package/adapters/opencode/commands/audit-project.md +0 -1
- package/adapters/opencode/commands/consult.md +133 -57
- package/adapters/opencode/commands/debate.md +224 -0
- package/adapters/opencode/commands/delivery-approval.md +0 -1
- package/adapters/opencode/commands/deslop.md +0 -1
- package/adapters/opencode/commands/drift-detect.md +0 -1
- package/adapters/opencode/commands/enhance.md +0 -1
- package/adapters/opencode/commands/learn.md +0 -1
- package/adapters/opencode/commands/next-task.md +0 -1
- package/adapters/opencode/commands/perf.md +0 -1
- package/adapters/opencode/commands/repo-map.md +0 -1
- package/adapters/opencode/commands/ship-ci-review-loop.md +0 -1
- package/adapters/opencode/commands/ship-deployment.md +0 -1
- package/adapters/opencode/commands/ship-error-handling.md +0 -1
- package/adapters/opencode/commands/ship.md +0 -1
- package/adapters/opencode/commands/sync-docs.md +0 -1
- package/adapters/opencode/skills/agnix/SKILL.md +1 -2
- package/adapters/opencode/skills/consult/SKILL.md +33 -23
- package/adapters/opencode/skills/debate/SKILL.md +245 -0
- package/adapters/opencode/skills/deslop/SKILL.md +1 -2
- package/adapters/opencode/skills/discover-tasks/SKILL.md +1 -2
- package/adapters/opencode/skills/drift-analysis/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-agent-prompts/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-claude-memory/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-cross-file/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-docs/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-hooks/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-orchestrator/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-plugins/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-prompts/SKILL.md +1 -2
- package/adapters/opencode/skills/enhance-skills/SKILL.md +1 -2
- package/adapters/opencode/skills/learn/SKILL.md +1 -2
- package/adapters/opencode/skills/orchestrate-review/SKILL.md +0 -1
- package/adapters/opencode/skills/perf-analyzer/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-baseline-manager/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-benchmarker/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-code-paths/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-investigation-logger/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-profiler/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-theory-gatherer/SKILL.md +1 -2
- package/adapters/opencode/skills/perf-theory-tester/SKILL.md +1 -2
- package/adapters/opencode/skills/repo-mapping/SKILL.md +1 -2
- package/adapters/opencode/skills/sync-docs/SKILL.md +1 -2
- package/adapters/opencode/skills/validate-delivery/SKILL.md +1 -2
- package/lib/adapter-transforms.js +24 -4
- package/package.json +1 -1
- package/plugins/agnix/.claude-plugin/plugin.json +1 -1
- package/plugins/agnix/skills/agnix/SKILL.md +1 -1
- package/plugins/audit-project/.claude-plugin/plugin.json +1 -1
- package/plugins/audit-project/lib/adapter-transforms.js +24 -4
- package/plugins/consult/.claude-plugin/plugin.json +1 -1
- package/plugins/consult/agents/consult-agent.md +122 -29
- package/plugins/consult/commands/consult.md +135 -58
- package/plugins/consult/skills/consult/SKILL.md +31 -20
- package/plugins/debate/.claude-plugin/plugin.json +21 -0
- package/plugins/debate/agents/debate-orchestrator.md +175 -0
- package/plugins/debate/commands/debate.md +221 -0
- package/plugins/debate/lib/adapter-transforms.js +298 -0
- package/plugins/debate/lib/collectors/codebase.js +392 -0
- package/plugins/debate/lib/collectors/docs-patterns.js +713 -0
- package/plugins/debate/lib/collectors/documentation.js +219 -0
- package/plugins/debate/lib/collectors/github.js +330 -0
- package/plugins/debate/lib/collectors/index.js +126 -0
- package/plugins/debate/lib/config/index.js +14 -0
- package/plugins/debate/lib/cross-platform/index.js +539 -0
- package/plugins/debate/lib/discovery/index.js +352 -0
- package/plugins/debate/lib/drift-detect/collectors.js +37 -0
- package/plugins/debate/lib/enhance/agent-analyzer.js +421 -0
- package/plugins/debate/lib/enhance/agent-patterns.js +571 -0
- package/plugins/debate/lib/enhance/auto-suppression.js +622 -0
- package/plugins/debate/lib/enhance/benchmark.js +417 -0
- package/plugins/debate/lib/enhance/cross-file-analyzer.js +930 -0
- package/plugins/debate/lib/enhance/cross-file-patterns.js +370 -0
- package/plugins/debate/lib/enhance/docs-analyzer.js +325 -0
- package/plugins/debate/lib/enhance/docs-patterns.js +671 -0
- package/plugins/debate/lib/enhance/fixer.js +721 -0
- package/plugins/debate/lib/enhance/hook-analyzer.js +135 -0
- package/plugins/debate/lib/enhance/hook-patterns.js +40 -0
- package/plugins/debate/lib/enhance/index.js +127 -0
- package/plugins/debate/lib/enhance/plugin-analyzer.js +402 -0
- package/plugins/debate/lib/enhance/plugin-patterns.js +326 -0
- package/plugins/debate/lib/enhance/projectmemory-analyzer.js +551 -0
- package/plugins/debate/lib/enhance/projectmemory-patterns.js +617 -0
- package/plugins/debate/lib/enhance/prompt-analyzer.js +457 -0
- package/plugins/debate/lib/enhance/prompt-patterns.js +1484 -0
- package/plugins/debate/lib/enhance/reporter.js +1348 -0
- package/plugins/debate/lib/enhance/security-patterns.js +284 -0
- package/plugins/debate/lib/enhance/skill-analyzer.js +182 -0
- package/plugins/debate/lib/enhance/skill-patterns.js +147 -0
- package/plugins/debate/lib/enhance/suppression.js +352 -0
- package/plugins/debate/lib/enhance/tool-patterns.js +373 -0
- package/plugins/debate/lib/index.js +270 -0
- package/plugins/debate/lib/patterns/cli-enhancers.js +611 -0
- package/plugins/debate/lib/patterns/pipeline.js +948 -0
- package/plugins/debate/lib/patterns/review-patterns.js +558 -0
- package/plugins/debate/lib/patterns/slop-analyzers.js +2305 -0
- package/plugins/debate/lib/patterns/slop-patterns.js +1187 -0
- package/plugins/debate/lib/perf/analyzer/index.js +22 -0
- package/plugins/debate/lib/perf/argument-parser.js +105 -0
- package/plugins/debate/lib/perf/baseline-comparator.js +50 -0
- package/plugins/debate/lib/perf/baseline-store.js +127 -0
- package/plugins/debate/lib/perf/benchmark-runner.js +404 -0
- package/plugins/debate/lib/perf/breaking-point-finder.js +52 -0
- package/plugins/debate/lib/perf/breaking-point-runner.js +60 -0
- package/plugins/debate/lib/perf/checkpoint.js +123 -0
- package/plugins/debate/lib/perf/code-paths.js +86 -0
- package/plugins/debate/lib/perf/consolidation.js +37 -0
- package/plugins/debate/lib/perf/constraint-runner.js +71 -0
- package/plugins/debate/lib/perf/experiment-runner.js +32 -0
- package/plugins/debate/lib/perf/index.js +41 -0
- package/plugins/debate/lib/perf/investigation-state.js +874 -0
- package/plugins/debate/lib/perf/optimization-runner.js +79 -0
- package/plugins/debate/lib/perf/profilers/go.js +22 -0
- package/plugins/debate/lib/perf/profilers/index.js +46 -0
- package/plugins/debate/lib/perf/profilers/java.js +23 -0
- package/plugins/debate/lib/perf/profilers/node.js +27 -0
- package/plugins/debate/lib/perf/profilers/python.js +23 -0
- package/plugins/debate/lib/perf/profilers/rust.js +23 -0
- package/plugins/debate/lib/perf/profiling-runner.js +75 -0
- package/plugins/debate/lib/perf/schemas.js +140 -0
- package/plugins/debate/lib/platform/detect-platform.js +413 -0
- package/plugins/debate/lib/platform/detection-configs.js +93 -0
- package/plugins/debate/lib/platform/state-dir.js +132 -0
- package/plugins/debate/lib/platform/verify-tools.js +182 -0
- package/plugins/debate/lib/repo-map/cache.js +152 -0
- package/plugins/debate/lib/repo-map/concurrency.js +29 -0
- package/plugins/debate/lib/repo-map/index.js +222 -0
- package/plugins/debate/lib/repo-map/installer.js +212 -0
- package/plugins/debate/lib/repo-map/queries/go.js +27 -0
- package/plugins/debate/lib/repo-map/queries/index.js +100 -0
- package/plugins/debate/lib/repo-map/queries/java.js +38 -0
- package/plugins/debate/lib/repo-map/queries/javascript.js +55 -0
- package/plugins/debate/lib/repo-map/queries/python.js +24 -0
- package/plugins/debate/lib/repo-map/queries/rust.js +73 -0
- package/plugins/debate/lib/repo-map/queries/typescript.js +38 -0
- package/plugins/debate/lib/repo-map/runner.js +1364 -0
- package/plugins/debate/lib/repo-map/updater.js +562 -0
- package/plugins/debate/lib/repo-map/usage-analyzer.js +407 -0
- package/plugins/debate/lib/schemas/plugin-manifest.schema.json +57 -0
- package/plugins/debate/lib/schemas/validator.js +247 -0
- package/plugins/debate/lib/sources/custom-handler.js +199 -0
- package/plugins/debate/lib/sources/policy-questions.js +246 -0
- package/plugins/debate/lib/sources/source-cache.js +165 -0
- package/plugins/debate/lib/state/workflow-state.js +576 -0
- package/plugins/debate/lib/types/agent-frontmatter.d.ts +134 -0
- package/plugins/debate/lib/types/command-frontmatter.d.ts +107 -0
- package/plugins/debate/lib/types/hook-frontmatter.d.ts +115 -0
- package/plugins/debate/lib/types/index.d.ts +84 -0
- package/plugins/debate/lib/types/plugin-manifest.d.ts +102 -0
- package/plugins/debate/lib/types/skill-frontmatter.d.ts +89 -0
- package/plugins/debate/lib/utils/atomic-write.js +94 -0
- package/plugins/debate/lib/utils/cache-manager.js +159 -0
- package/plugins/debate/lib/utils/command-parser.js +0 -0
- package/plugins/debate/lib/utils/context-optimizer.js +300 -0
- package/plugins/debate/lib/utils/deprecation.js +37 -0
- package/plugins/debate/lib/utils/shell-escape.js +88 -0
- package/plugins/debate/lib/utils/state-helpers.js +61 -0
- package/plugins/debate/skills/debate/SKILL.md +264 -0
- package/plugins/deslop/.claude-plugin/plugin.json +1 -1
- package/plugins/deslop/lib/adapter-transforms.js +24 -4
- package/plugins/deslop/skills/deslop/SKILL.md +1 -1
- package/plugins/drift-detect/.claude-plugin/plugin.json +1 -1
- package/plugins/drift-detect/lib/adapter-transforms.js +24 -4
- package/plugins/drift-detect/skills/drift-analysis/SKILL.md +1 -1
- package/plugins/enhance/.claude-plugin/plugin.json +1 -1
- package/plugins/enhance/lib/adapter-transforms.js +24 -4
- package/plugins/enhance/skills/enhance-agent-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-claude-memory/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-cross-file/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-docs/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-hooks/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-orchestrator/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-plugins/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-prompts/SKILL.md +1 -1
- package/plugins/enhance/skills/enhance-skills/SKILL.md +1 -1
- package/plugins/learn/.claude-plugin/plugin.json +1 -1
- package/plugins/learn/agents/learn-agent.md +1 -1
- package/plugins/learn/lib/adapter-transforms.js +24 -4
- package/plugins/learn/skills/learn/SKILL.md +1 -1
- package/plugins/next-task/.claude-plugin/plugin.json +1 -1
- package/plugins/next-task/agents/exploration-agent.md +1 -1
- package/plugins/next-task/lib/adapter-transforms.js +24 -4
- package/plugins/next-task/skills/discover-tasks/SKILL.md +1 -1
- package/plugins/next-task/skills/validate-delivery/SKILL.md +1 -1
- package/plugins/perf/.claude-plugin/plugin.json +1 -1
- package/plugins/perf/lib/adapter-transforms.js +24 -4
- package/plugins/perf/skills/perf-analyzer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-baseline-manager/SKILL.md +1 -1
- package/plugins/perf/skills/perf-benchmarker/SKILL.md +1 -1
- package/plugins/perf/skills/perf-code-paths/SKILL.md +1 -1
- package/plugins/perf/skills/perf-investigation-logger/SKILL.md +1 -1
- package/plugins/perf/skills/perf-profiler/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-gatherer/SKILL.md +1 -1
- package/plugins/perf/skills/perf-theory-tester/SKILL.md +1 -1
- package/plugins/repo-map/.claude-plugin/plugin.json +1 -1
- package/plugins/repo-map/lib/adapter-transforms.js +24 -4
- package/plugins/ship/.claude-plugin/plugin.json +1 -1
- package/plugins/ship/lib/adapter-transforms.js +24 -4
- package/plugins/sync-docs/.claude-plugin/plugin.json +1 -1
- package/plugins/sync-docs/lib/adapter-transforms.js +24 -4
- package/plugins/sync-docs/skills/sync-docs/SKILL.md +1 -1
- package/scripts/gen-adapters.js +6 -7
- package/scripts/generate-docs.js +4 -2
- package/scripts/plugins.txt +1 -0
- package/site/content.json +6 -6
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Perf analysis helpers.
|
|
3
|
+
*
|
|
4
|
+
* @module lib/perf/analyzer
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Build a compact summary of perf findings.
|
|
9
|
+
* @param {object} input
|
|
10
|
+
* @returns {object}
|
|
11
|
+
*/
|
|
12
|
+
function summarize(input = {}) {
|
|
13
|
+
return {
|
|
14
|
+
summary: input.summary || '',
|
|
15
|
+
recommendations: input.recommendations || [],
|
|
16
|
+
risks: input.risks || []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
summarize
|
|
22
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argument parsing helper for /perf.
|
|
3
|
+
*
|
|
4
|
+
* @module lib/perf/argument-parser
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const GREEDY_FLAGS = new Set(['--quote', '--change', '--scenario', '--command', '--rationale']);
|
|
8
|
+
|
|
9
|
+
function parseArgv(tokens) {
|
|
10
|
+
const args = [];
|
|
11
|
+
if (!Array.isArray(tokens)) return args;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
14
|
+
const token = tokens[i];
|
|
15
|
+
if (!token) continue;
|
|
16
|
+
|
|
17
|
+
if (token.startsWith('--')) {
|
|
18
|
+
args.push(token);
|
|
19
|
+
if (i + 1 >= tokens.length) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (GREEDY_FLAGS.has(token)) {
|
|
23
|
+
const valueTokens = [];
|
|
24
|
+
while (i + 1 < tokens.length && !String(tokens[i + 1]).startsWith('--')) {
|
|
25
|
+
valueTokens.push(tokens[++i]);
|
|
26
|
+
}
|
|
27
|
+
if (valueTokens.length > 0) {
|
|
28
|
+
args.push(valueTokens.join(' '));
|
|
29
|
+
}
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (!String(tokens[i + 1]).startsWith('--')) {
|
|
33
|
+
args.push(tokens[++i]);
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
args.push(token);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return args;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseArguments(raw) {
|
|
45
|
+
if (Array.isArray(raw)) {
|
|
46
|
+
return parseArgv(raw);
|
|
47
|
+
}
|
|
48
|
+
if (!raw || typeof raw !== 'string') return [];
|
|
49
|
+
|
|
50
|
+
const args = [];
|
|
51
|
+
let current = '';
|
|
52
|
+
let quote = null;
|
|
53
|
+
let escaped = false;
|
|
54
|
+
|
|
55
|
+
for (let i = 0; i < raw.length; i++) {
|
|
56
|
+
const ch = raw[i];
|
|
57
|
+
|
|
58
|
+
if (escaped) {
|
|
59
|
+
current += ch;
|
|
60
|
+
escaped = false;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (ch === '\\') {
|
|
65
|
+
if (quote) {
|
|
66
|
+
escaped = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (quote) {
|
|
72
|
+
if (ch === quote) {
|
|
73
|
+
quote = null;
|
|
74
|
+
} else {
|
|
75
|
+
current += ch;
|
|
76
|
+
}
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ch === '"' || ch === "'") {
|
|
81
|
+
quote = ch;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (/\s/.test(ch)) {
|
|
86
|
+
if (current) {
|
|
87
|
+
args.push(current);
|
|
88
|
+
current = '';
|
|
89
|
+
}
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
current += ch;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (current) {
|
|
97
|
+
args.push(current);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = {
|
|
104
|
+
parseArguments
|
|
105
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline comparison helpers
|
|
3
|
+
*
|
|
4
|
+
* @module lib/perf/baseline-comparator
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compute delta between baseline and current metrics.
|
|
9
|
+
* Supports flat numeric values under baseline.metrics/current.metrics.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} baseline
|
|
12
|
+
* @param {object} current
|
|
13
|
+
* @returns {object}
|
|
14
|
+
*/
|
|
15
|
+
function compareBaselines(baseline, current) {
|
|
16
|
+
const baselineMetrics = baseline?.metrics || {};
|
|
17
|
+
const currentMetrics = current?.metrics || {};
|
|
18
|
+
const keys = new Set([
|
|
19
|
+
...Object.keys(baselineMetrics),
|
|
20
|
+
...Object.keys(currentMetrics)
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const deltas = {};
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
const baseValue = baselineMetrics[key];
|
|
26
|
+
const currentValue = currentMetrics[key];
|
|
27
|
+
|
|
28
|
+
if (typeof baseValue === 'number' && typeof currentValue === 'number') {
|
|
29
|
+
const delta = currentValue - baseValue;
|
|
30
|
+
const percent = baseValue === 0 ? null : delta / baseValue;
|
|
31
|
+
deltas[key] = { baseline: baseValue, current: currentValue, delta, percent };
|
|
32
|
+
} else {
|
|
33
|
+
deltas[key] = {
|
|
34
|
+
baseline: baseValue ?? null,
|
|
35
|
+
current: currentValue ?? null,
|
|
36
|
+
delta: null,
|
|
37
|
+
percent: null
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
comparedAt: new Date().toISOString(),
|
|
44
|
+
metrics: deltas
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
compareBaselines
|
|
50
|
+
};
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baseline storage utilities for /perf
|
|
3
|
+
*
|
|
4
|
+
* Stores baselines under:
|
|
5
|
+
* - {state-dir}/perf/baselines/{version}.json
|
|
6
|
+
*
|
|
7
|
+
* @module lib/perf/baseline-store
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { getStateDir } = require('../platform/state-dir');
|
|
13
|
+
const { validateBaseline, assertValid } = require('./schemas');
|
|
14
|
+
|
|
15
|
+
const BASELINE_DIR = 'baselines';
|
|
16
|
+
|
|
17
|
+
function assertSafeBaselineVersion(version) {
|
|
18
|
+
if (!version || typeof version !== 'string') {
|
|
19
|
+
throw new Error('Baseline version is required');
|
|
20
|
+
}
|
|
21
|
+
if (version.includes('..') || version.includes('/') || version.includes('\\') || version.includes('\0')) {
|
|
22
|
+
throw new Error('Baseline version contains invalid characters');
|
|
23
|
+
}
|
|
24
|
+
if (!/^[a-zA-Z0-9._+-]+$/.test(version)) {
|
|
25
|
+
throw new Error('Baseline version contains invalid characters');
|
|
26
|
+
}
|
|
27
|
+
return version;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get baseline directory path
|
|
32
|
+
* @param {string} basePath
|
|
33
|
+
* @returns {string}
|
|
34
|
+
*/
|
|
35
|
+
function getBaselineDir(basePath = process.cwd()) {
|
|
36
|
+
return path.join(basePath, getStateDir(basePath), 'perf', BASELINE_DIR);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Ensure baseline directory exists
|
|
41
|
+
* @param {string} basePath
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
function ensureBaselineDir(basePath = process.cwd()) {
|
|
45
|
+
const dir = getBaselineDir(basePath);
|
|
46
|
+
if (!fs.existsSync(dir)) {
|
|
47
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
48
|
+
}
|
|
49
|
+
return dir;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Build baseline file path
|
|
54
|
+
* @param {string} version
|
|
55
|
+
* @param {string} basePath
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
function getBaselinePath(version, basePath = process.cwd()) {
|
|
59
|
+
const safeVersion = assertSafeBaselineVersion(version);
|
|
60
|
+
return path.join(ensureBaselineDir(basePath), `${safeVersion}.json`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List baseline versions
|
|
65
|
+
* @param {string} basePath
|
|
66
|
+
* @returns {string[]}
|
|
67
|
+
*/
|
|
68
|
+
function listBaselines(basePath = process.cwd()) {
|
|
69
|
+
const dir = ensureBaselineDir(basePath);
|
|
70
|
+
return fs.readdirSync(dir)
|
|
71
|
+
.filter(file => file.endsWith('.json'))
|
|
72
|
+
.map(file => path.basename(file, '.json'))
|
|
73
|
+
.sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read baseline file
|
|
78
|
+
* @param {string} version
|
|
79
|
+
* @param {string} basePath
|
|
80
|
+
* @returns {object|null}
|
|
81
|
+
*/
|
|
82
|
+
function readBaseline(version, basePath = process.cwd()) {
|
|
83
|
+
const baselinePath = getBaselinePath(version, basePath);
|
|
84
|
+
if (!fs.existsSync(baselinePath)) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(fs.readFileSync(baselinePath, 'utf8'));
|
|
89
|
+
const validation = validateBaseline(parsed);
|
|
90
|
+
if (!validation.ok) {
|
|
91
|
+
console.error(`[CRITICAL] Invalid baseline file at ${baselinePath}: ${validation.errors.join(', ')}`);
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return parsed;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`[CRITICAL] Corrupted baseline file at ${baselinePath}: ${error.message}`);
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Write baseline file (overwrites existing)
|
|
103
|
+
* @param {string} version
|
|
104
|
+
* @param {object} baseline
|
|
105
|
+
* @param {string} basePath
|
|
106
|
+
* @returns {boolean}
|
|
107
|
+
*/
|
|
108
|
+
function writeBaseline(version, baseline, basePath = process.cwd()) {
|
|
109
|
+
const baselinePath = getBaselinePath(version, basePath);
|
|
110
|
+
const payload = {
|
|
111
|
+
version,
|
|
112
|
+
recordedAt: new Date().toISOString(),
|
|
113
|
+
...baseline
|
|
114
|
+
};
|
|
115
|
+
assertValid(validateBaseline(payload), 'Invalid baseline payload');
|
|
116
|
+
fs.writeFileSync(baselinePath, JSON.stringify(payload, null, 2), 'utf8');
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = {
|
|
121
|
+
getBaselineDir,
|
|
122
|
+
ensureBaselineDir,
|
|
123
|
+
getBaselinePath,
|
|
124
|
+
listBaselines,
|
|
125
|
+
readBaseline,
|
|
126
|
+
writeBaseline
|
|
127
|
+
};
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sequential benchmark runner utilities.
|
|
3
|
+
*
|
|
4
|
+
* @module lib/perf/benchmark-runner
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execFileSync } = require('child_process');
|
|
8
|
+
const { validateBaseline } = require('./schemas');
|
|
9
|
+
const { parseCommand, resolveExecutableForPlatform } = require('../utils/command-parser');
|
|
10
|
+
|
|
11
|
+
const DEFAULT_MIN_DURATION = 60;
|
|
12
|
+
const BINARY_SEARCH_MIN_DURATION = 30;
|
|
13
|
+
const DEFAULT_DURATION_SLACK_SECONDS = 1;
|
|
14
|
+
|
|
15
|
+
function parseDuration(value) {
|
|
16
|
+
const parsed = Number(value);
|
|
17
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Normalize benchmark options and enforce minimum durations.
|
|
22
|
+
* @param {object} options
|
|
23
|
+
* @returns {object}
|
|
24
|
+
*/
|
|
25
|
+
function normalizeBenchmarkOptions(options = {}) {
|
|
26
|
+
const mode = options.mode || 'full';
|
|
27
|
+
const defaultMin = mode === 'binary-search'
|
|
28
|
+
? BINARY_SEARCH_MIN_DURATION
|
|
29
|
+
: DEFAULT_MIN_DURATION;
|
|
30
|
+
const requestedDuration = parseDuration(options.duration);
|
|
31
|
+
const requestedMin = parseDuration(options.minDuration);
|
|
32
|
+
const minDuration = requestedMin ?? requestedDuration ?? defaultMin;
|
|
33
|
+
const duration = Math.max(requestedDuration ?? minDuration, minDuration);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
...options,
|
|
37
|
+
mode,
|
|
38
|
+
duration,
|
|
39
|
+
warmup: options.warmup || 10,
|
|
40
|
+
allowShort: options.allowShort === true
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Run a benchmark command synchronously (sequential only).
|
|
46
|
+
* @param {string} command
|
|
47
|
+
* @param {object} options
|
|
48
|
+
* @param {number} [options.duration]
|
|
49
|
+
* @param {number} [options.minDuration]
|
|
50
|
+
* @param {boolean} [options.setDurationEnv]
|
|
51
|
+
* @param {string} [options.runMode]
|
|
52
|
+
* @returns {{ success: boolean, output: string }}
|
|
53
|
+
*/
|
|
54
|
+
function runBenchmark(command, options = {}) {
|
|
55
|
+
if (!command || typeof command !== 'string') {
|
|
56
|
+
throw new Error('Benchmark command must be a non-empty string');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parsedCommand = parseCommand(command, 'Benchmark command');
|
|
60
|
+
const executable = resolveExecutableForPlatform(parsedCommand.executable);
|
|
61
|
+
const normalized = normalizeBenchmarkOptions(options);
|
|
62
|
+
const setDurationEnv = options.setDurationEnv !== false;
|
|
63
|
+
const env = {
|
|
64
|
+
...process.env,
|
|
65
|
+
...normalized.env
|
|
66
|
+
};
|
|
67
|
+
if (setDurationEnv) {
|
|
68
|
+
env.PERF_RUN_DURATION = String(normalized.duration);
|
|
69
|
+
}
|
|
70
|
+
if (options.runMode) {
|
|
71
|
+
env.PERF_RUN_MODE = options.runMode;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const start = Date.now();
|
|
75
|
+
let output;
|
|
76
|
+
try {
|
|
77
|
+
output = execFileSync(executable, parsedCommand.args, {
|
|
78
|
+
stdio: 'pipe',
|
|
79
|
+
encoding: 'utf8',
|
|
80
|
+
env,
|
|
81
|
+
windowsHide: true,
|
|
82
|
+
cwd: options.cwd || process.cwd()
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const stderr = error.stderr ? String(error.stderr).trim() : '';
|
|
86
|
+
const stdout = error.stdout ? String(error.stdout).trim() : '';
|
|
87
|
+
const exitCode = error.status ?? 'unknown';
|
|
88
|
+
const details = stderr || stdout || error.message || 'No error details available';
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Benchmark command failed (exit code ${exitCode}): ${parsedCommand.display}\n` +
|
|
91
|
+
`Details: ${details}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const elapsedSeconds = (Date.now() - start) / 1000;
|
|
95
|
+
|
|
96
|
+
const allowShort = normalized.allowShort || process.env.PERF_ALLOW_SHORT === '1';
|
|
97
|
+
if (!allowShort && setDurationEnv && elapsedSeconds + DEFAULT_DURATION_SLACK_SECONDS < normalized.duration) {
|
|
98
|
+
throw new Error(`Benchmark finished too quickly (${elapsedSeconds.toFixed(2)}s < ${normalized.duration}s)`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
output,
|
|
104
|
+
duration: normalized.duration,
|
|
105
|
+
warmup: normalized.warmup,
|
|
106
|
+
mode: normalized.mode,
|
|
107
|
+
elapsedSeconds
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseLineMetrics(output) {
|
|
112
|
+
const lines = output.split(/\r?\n/);
|
|
113
|
+
const metrics = {};
|
|
114
|
+
let sawMarker = false;
|
|
115
|
+
|
|
116
|
+
for (const line of lines) {
|
|
117
|
+
const markerIndex = line.indexOf('PERF_METRICS');
|
|
118
|
+
if (markerIndex === -1) continue;
|
|
119
|
+
|
|
120
|
+
sawMarker = true;
|
|
121
|
+
const rest = line.slice(markerIndex + 'PERF_METRICS'.length).trim();
|
|
122
|
+
if (!rest) continue;
|
|
123
|
+
|
|
124
|
+
const tokens = rest.split(/\s+/).filter(Boolean);
|
|
125
|
+
let scenario = null;
|
|
126
|
+
const lineMetrics = {};
|
|
127
|
+
|
|
128
|
+
for (const token of tokens) {
|
|
129
|
+
const eqIndex = token.indexOf('=');
|
|
130
|
+
if (eqIndex === -1) continue;
|
|
131
|
+
|
|
132
|
+
const key = token.slice(0, eqIndex).trim();
|
|
133
|
+
const rawValue = token.slice(eqIndex + 1).trim();
|
|
134
|
+
if (!key) continue;
|
|
135
|
+
|
|
136
|
+
if (key === 'scenario') {
|
|
137
|
+
scenario = rawValue;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const value = Number(rawValue);
|
|
142
|
+
if (!Number.isFinite(value)) {
|
|
143
|
+
return { ok: false, error: `Metric ${key} must be a number` };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
lineMetrics[key] = value;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Object.keys(lineMetrics).length === 0) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (scenario) {
|
|
154
|
+
if (!metrics.scenarios) {
|
|
155
|
+
metrics.scenarios = {};
|
|
156
|
+
}
|
|
157
|
+
metrics.scenarios[scenario] = {
|
|
158
|
+
...(metrics.scenarios[scenario] || {}),
|
|
159
|
+
...lineMetrics
|
|
160
|
+
};
|
|
161
|
+
} else {
|
|
162
|
+
Object.assign(metrics, lineMetrics);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (!sawMarker) {
|
|
167
|
+
return { ok: false, error: 'Metrics markers not found' };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { ok: true, metrics };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Parse metrics from benchmark output using PERF_METRICS markers.
|
|
175
|
+
* @param {string} output
|
|
176
|
+
* @returns {{ ok: boolean, metrics?: object, error?: string }}
|
|
177
|
+
*/
|
|
178
|
+
function parseMetrics(output) {
|
|
179
|
+
if (typeof output !== 'string') {
|
|
180
|
+
return { ok: false, error: 'Output must be a string' };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const startMarker = 'PERF_METRICS_START';
|
|
184
|
+
const endMarker = 'PERF_METRICS_END';
|
|
185
|
+
const startIndex = output.indexOf(startMarker);
|
|
186
|
+
const endIndex = output.indexOf(endMarker);
|
|
187
|
+
|
|
188
|
+
if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
|
|
189
|
+
const jsonStart = startIndex + startMarker.length;
|
|
190
|
+
const raw = output.slice(jsonStart, endIndex).trim();
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const parsed = JSON.parse(raw);
|
|
194
|
+
const validation = validateBaseline({
|
|
195
|
+
version: 'temp',
|
|
196
|
+
recordedAt: new Date().toISOString(),
|
|
197
|
+
command: 'temp',
|
|
198
|
+
metrics: parsed
|
|
199
|
+
});
|
|
200
|
+
if (!validation.ok) {
|
|
201
|
+
return { ok: false, error: `Invalid metrics: ${validation.errors.join(', ')}` };
|
|
202
|
+
}
|
|
203
|
+
return { ok: true, metrics: parsed };
|
|
204
|
+
} catch (error) {
|
|
205
|
+
return { ok: false, error: `Failed to parse metrics JSON: ${error.message}` };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const lineParsed = parseLineMetrics(output);
|
|
210
|
+
if (!lineParsed.ok) {
|
|
211
|
+
return lineParsed;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const validation = validateBaseline({
|
|
215
|
+
version: 'temp',
|
|
216
|
+
recordedAt: new Date().toISOString(),
|
|
217
|
+
command: 'temp',
|
|
218
|
+
metrics: lineParsed.metrics
|
|
219
|
+
});
|
|
220
|
+
if (!validation.ok) {
|
|
221
|
+
return { ok: false, error: `Invalid metrics: ${validation.errors.join(', ')}` };
|
|
222
|
+
}
|
|
223
|
+
return { ok: true, metrics: lineParsed.metrics };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function flattenMetrics(metrics) {
|
|
227
|
+
if (!metrics || typeof metrics !== 'object' || Array.isArray(metrics)) {
|
|
228
|
+
throw new Error('metrics must be an object');
|
|
229
|
+
}
|
|
230
|
+
const flat = {};
|
|
231
|
+
|
|
232
|
+
for (const [key, value] of Object.entries(metrics)) {
|
|
233
|
+
if (key === 'scenarios') {
|
|
234
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
235
|
+
throw new Error('metrics.scenarios must be an object');
|
|
236
|
+
}
|
|
237
|
+
for (const [scenarioName, scenarioMetrics] of Object.entries(value)) {
|
|
238
|
+
if (!scenarioMetrics || typeof scenarioMetrics !== 'object' || Array.isArray(scenarioMetrics)) {
|
|
239
|
+
throw new Error(`metrics.scenarios.${scenarioName} must be an object`);
|
|
240
|
+
}
|
|
241
|
+
for (const [metricName, metricValue] of Object.entries(scenarioMetrics)) {
|
|
242
|
+
if (typeof metricValue !== 'number' || Number.isNaN(metricValue)) {
|
|
243
|
+
throw new Error(`metric ${scenarioName}.${metricName} must be a number`);
|
|
244
|
+
}
|
|
245
|
+
flat[`scenarios.${scenarioName}.${metricName}`] = metricValue;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (typeof value !== 'number' || Number.isNaN(value)) {
|
|
252
|
+
throw new Error(`metric ${key} must be a number`);
|
|
253
|
+
}
|
|
254
|
+
flat[key] = value;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return flat;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function unflattenMetrics(flat) {
|
|
261
|
+
const metrics = {};
|
|
262
|
+
for (const [key, value] of Object.entries(flat)) {
|
|
263
|
+
if (!key.startsWith('scenarios.')) {
|
|
264
|
+
metrics[key] = value;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const parts = key.split('.');
|
|
268
|
+
if (parts.length < 3) {
|
|
269
|
+
throw new Error(`invalid scenario metric key: ${key}`);
|
|
270
|
+
}
|
|
271
|
+
const scenarioName = parts[1];
|
|
272
|
+
const metricName = parts.slice(2).join('.');
|
|
273
|
+
if (!metrics.scenarios) {
|
|
274
|
+
metrics.scenarios = {};
|
|
275
|
+
}
|
|
276
|
+
if (!metrics.scenarios[scenarioName]) {
|
|
277
|
+
metrics.scenarios[scenarioName] = {};
|
|
278
|
+
}
|
|
279
|
+
metrics.scenarios[scenarioName][metricName] = value;
|
|
280
|
+
}
|
|
281
|
+
return metrics;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function aggregateValues(values, aggregate) {
|
|
285
|
+
const normalized = (aggregate || 'median').toLowerCase();
|
|
286
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
287
|
+
|
|
288
|
+
switch (normalized) {
|
|
289
|
+
case 'median': {
|
|
290
|
+
const mid = Math.floor(sorted.length / 2);
|
|
291
|
+
if (sorted.length % 2 === 0) {
|
|
292
|
+
return (sorted[mid - 1] + sorted[mid]) / 2;
|
|
293
|
+
}
|
|
294
|
+
return sorted[mid];
|
|
295
|
+
}
|
|
296
|
+
case 'mean': {
|
|
297
|
+
const sum = sorted.reduce((acc, value) => acc + value, 0);
|
|
298
|
+
return sum / sorted.length;
|
|
299
|
+
}
|
|
300
|
+
case 'min':
|
|
301
|
+
return sorted[0];
|
|
302
|
+
case 'max':
|
|
303
|
+
return sorted[sorted.length - 1];
|
|
304
|
+
default:
|
|
305
|
+
throw new Error(`Unsupported aggregate: ${aggregate}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function aggregateMetrics(samples, aggregate = 'median') {
|
|
310
|
+
if (!Array.isArray(samples) || samples.length === 0) {
|
|
311
|
+
throw new Error('samples must be a non-empty array');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const flattened = samples.map(flattenMetrics);
|
|
315
|
+
const keys = Object.keys(flattened[0]).sort();
|
|
316
|
+
|
|
317
|
+
for (const sample of flattened) {
|
|
318
|
+
const sampleKeys = Object.keys(sample).sort();
|
|
319
|
+
if (sampleKeys.length !== keys.length) {
|
|
320
|
+
throw new Error('Metric sets differ across runs');
|
|
321
|
+
}
|
|
322
|
+
for (let i = 0; i < keys.length; i++) {
|
|
323
|
+
if (keys[i] !== sampleKeys[i]) {
|
|
324
|
+
throw new Error('Metric sets differ across runs');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const aggregated = {};
|
|
330
|
+
for (const key of keys) {
|
|
331
|
+
const values = flattened.map(sample => sample[key]);
|
|
332
|
+
aggregated[key] = aggregateValues(values, aggregate);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return unflattenMetrics(aggregated);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function resolveRuns(options) {
|
|
339
|
+
if (!options || options.runs == null) return 1;
|
|
340
|
+
const runs = Number(options.runs);
|
|
341
|
+
if (!Number.isFinite(runs) || runs < 1) {
|
|
342
|
+
throw new Error('runs must be a positive number');
|
|
343
|
+
}
|
|
344
|
+
return Math.floor(runs);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Run benchmark multiple times and aggregate metrics.
|
|
349
|
+
* @param {string} command
|
|
350
|
+
* @param {object} options
|
|
351
|
+
* @returns {{ metrics: object, samples: object[], runs: number, aggregate: string }}
|
|
352
|
+
*/
|
|
353
|
+
function runBenchmarkSeries(command, options = {}) {
|
|
354
|
+
const runs = resolveRuns(options);
|
|
355
|
+
const aggregate = options.aggregate || (runs > 1 ? 'median' : 'median');
|
|
356
|
+
const runMode = options.runMode || (runs > 1 ? 'oneshot' : 'duration');
|
|
357
|
+
const env = {
|
|
358
|
+
...options.env,
|
|
359
|
+
PERF_RUN_MODE: runMode
|
|
360
|
+
};
|
|
361
|
+
const allowShort = options.allowShort === true || runMode === 'oneshot';
|
|
362
|
+
const setDurationEnv = runMode !== 'oneshot' && options.setDurationEnv !== false;
|
|
363
|
+
|
|
364
|
+
const samples = [];
|
|
365
|
+
for (let i = 0; i < runs; i++) {
|
|
366
|
+
let result;
|
|
367
|
+
try {
|
|
368
|
+
result = runBenchmark(command, {
|
|
369
|
+
...options,
|
|
370
|
+
env,
|
|
371
|
+
allowShort,
|
|
372
|
+
setDurationEnv,
|
|
373
|
+
runMode
|
|
374
|
+
});
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new Error(
|
|
377
|
+
`Benchmark run ${i + 1}/${runs} failed: ${error.message}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const parsed = parseMetrics(result.output);
|
|
381
|
+
if (!parsed.ok) {
|
|
382
|
+
throw new Error(`Metrics parse failed on run ${i + 1}/${runs}: ${parsed.error}`);
|
|
383
|
+
}
|
|
384
|
+
samples.push(parsed.metrics);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const metrics = samples.length === 1 ? samples[0] : aggregateMetrics(samples, aggregate);
|
|
388
|
+
return {
|
|
389
|
+
metrics,
|
|
390
|
+
samples,
|
|
391
|
+
runs,
|
|
392
|
+
aggregate
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
module.exports = {
|
|
397
|
+
DEFAULT_MIN_DURATION,
|
|
398
|
+
BINARY_SEARCH_MIN_DURATION,
|
|
399
|
+
normalizeBenchmarkOptions,
|
|
400
|
+
runBenchmark,
|
|
401
|
+
runBenchmarkSeries,
|
|
402
|
+
aggregateMetrics,
|
|
403
|
+
parseMetrics
|
|
404
|
+
};
|