knodin 0.5.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/README.md +590 -0
- package/dist/bin/cli.js +1704 -0
- package/dist/src/agent-integration.js +250 -0
- package/dist/src/artifact-refresh.js +81 -0
- package/dist/src/cli-args.js +267 -0
- package/dist/src/cli-model.js +324 -0
- package/dist/src/compact-structural.js +96 -0
- package/dist/src/competitive-constraints.js +20 -0
- package/dist/src/competitive-manifest.js +330 -0
- package/dist/src/competitive-measurement.js +183 -0
- package/dist/src/competitive-runner.js +453 -0
- package/dist/src/competitive-sandbox.js +108 -0
- package/dist/src/context-export.js +422 -0
- package/dist/src/context.js +102 -0
- package/dist/src/docs-sections.js +141 -0
- package/dist/src/doctor.js +380 -0
- package/dist/src/engine/ann-hnsw.js +271 -0
- package/dist/src/engine/embeddings.js +193 -0
- package/dist/src/engine/file-walker.js +43 -0
- package/dist/src/engine/index.js +13030 -0
- package/dist/src/engine/perf.js +115 -0
- package/dist/src/engine/prune.js +112 -0
- package/dist/src/engine/source-policy.js +69 -0
- package/dist/src/engine/sqlite.js +71 -0
- package/dist/src/engine/symbol-delete.js +58 -0
- package/dist/src/failure-diagnosis.js +590 -0
- package/dist/src/fleet.js +7 -0
- package/dist/src/git-executable.js +31 -0
- package/dist/src/graph-query-health.js +115 -0
- package/dist/src/index-activity.js +125 -0
- package/dist/src/init-progress-worker.js +107 -0
- package/dist/src/init-progress.js +155 -0
- package/dist/src/init.js +985 -0
- package/dist/src/lifecycle-health.js +213 -0
- package/dist/src/lsp-readonly.js +217 -0
- package/dist/src/output-compression.js +629 -0
- package/dist/src/output-telemetry.js +359 -0
- package/dist/src/pr-triage.js +638 -0
- package/dist/src/relationship-adapters.js +370 -0
- package/dist/src/release-attestation.js +533 -0
- package/dist/src/repair-progress-worker.js +121 -0
- package/dist/src/repair-progress.js +262 -0
- package/dist/src/repository-init-process.js +173 -0
- package/dist/src/repository-management.js +1089 -0
- package/dist/src/response-budget.js +184 -0
- package/dist/src/server.js +53 -0
- package/dist/src/system-config.js +615 -0
- package/dist/src/terminal-help.js +83 -0
- package/dist/src/tools/knodin-tools.js +1438 -0
- package/dist/src/tools/reckon-tools.js +5 -0
- package/dist/src/update-policy.js +944 -0
- package/dist/src/update-trust.js +503 -0
- package/dist/src/version.js +13 -0
- package/dist/src/visualization.js +162 -0
- package/dist/src/wait-for-fresh.js +98 -0
- package/dist/src/worktree-lifecycle.js +231 -0
- package/docs/CLI.md +39 -0
- package/docs/COMMAND-OUTPUT-COMPRESSION.md +194 -0
- package/docs/DEAD-CODE-AND-IMPACT.md +27 -0
- package/docs/DOCTOR-AND-UPDATES.md +84 -0
- package/docs/INDEXING-POLICY-AND-PROVENANCE.md +37 -0
- package/docs/INSTALLATION.md +208 -0
- package/docs/MCP.md +100 -0
- package/docs/PT-ACCESS-RECOMMENDATION.md +91 -0
- package/docs/RELEASE-0.3-EVIDENCE.md +73 -0
- package/docs/REPOSITORIES-AND-WORKTREES.md +81 -0
- package/docs/SIGNED-UPDATES.md +146 -0
- package/docs/SYSTEMS-AND-RELATIONSHIPS.md +45 -0
- package/docs/TELEMETRY.md +42 -0
- package/docs/releases/0.3.0.md +46 -0
- package/docs/releases/0.4.0.md +68 -0
- package/docs/releases/0.4.1.md +28 -0
- package/docs/releases/0.4.2.md +27 -0
- package/docs/releases/0.4.3.md +23 -0
- package/docs/releases/0.5.0.md +29 -0
- package/package.json +110 -0
- package/schemas/release-attestation-v1.schema.json +210 -0
- package/tree-sitter-prisma.wasm +0 -0
- package/tree-sitter-sql.wasm +0 -0
- package/tree-sitter-xml.wasm +0 -0
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
const bunHarness = (script) => ({ output }) => ["bun", `scripts/${script}`, output];
|
|
2
|
+
export const COMPETITIVE_MANIFEST = [
|
|
3
|
+
{
|
|
4
|
+
id: "gitnexus",
|
|
5
|
+
name: "GitNexus",
|
|
6
|
+
version: "1.6.9",
|
|
7
|
+
roadmap: ["C2", "C4", "C5", "C6", "C7", "C8", "C9", "C10", "C24", "C25", "C26"],
|
|
8
|
+
baseline: "gitnexus-vs-knodin.raw.json",
|
|
9
|
+
requirements: [
|
|
10
|
+
{ type: "command", value: "gitnexus" },
|
|
11
|
+
{ type: "command", value: "bun" },
|
|
12
|
+
],
|
|
13
|
+
versionCommand: ["gitnexus", "--version"],
|
|
14
|
+
prepare: [["gitnexus", "analyze"]],
|
|
15
|
+
command: bunHarness("gitnexus-bakeoff.ts"),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: "codegraph",
|
|
19
|
+
name: "CodeGraph",
|
|
20
|
+
version: "1.5.0",
|
|
21
|
+
roadmap: ["C2", "C3", "C11", "C24", "C25"],
|
|
22
|
+
baseline: "codegraph-vs-knodin.raw.json",
|
|
23
|
+
requirements: [
|
|
24
|
+
{ type: "command", value: "codegraph" },
|
|
25
|
+
{ type: "command", value: "bun" },
|
|
26
|
+
],
|
|
27
|
+
versionCommand: ["codegraph", "--version"],
|
|
28
|
+
command: bunHarness("codegraph-bakeoff.ts"),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: "code-review-graph",
|
|
32
|
+
name: "code-review-graph",
|
|
33
|
+
version: "2.3.7",
|
|
34
|
+
roadmap: ["C3", "C5", "C11", "C12", "C15", "C17", "C24", "C26"],
|
|
35
|
+
baseline: "code-review-graph-vs-knodin.raw.json",
|
|
36
|
+
requirements: [
|
|
37
|
+
{ type: "command", value: "code-review-graph" },
|
|
38
|
+
{ type: "command", value: "bun" },
|
|
39
|
+
],
|
|
40
|
+
versionCommand: ["code-review-graph", "--version"],
|
|
41
|
+
prepare: [["code-review-graph", "build"]],
|
|
42
|
+
command: bunHarness("code-review-graph-bakeoff.ts"),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: "graphify",
|
|
46
|
+
name: "Graphify",
|
|
47
|
+
version: "0.9.23",
|
|
48
|
+
roadmap: ["C2", "C3", "C11", "C12", "C13", "C17", "C24", "C26"],
|
|
49
|
+
baseline: "graphify-vs-knodin.raw.json",
|
|
50
|
+
requirements: [
|
|
51
|
+
{ type: "command", value: "graphify" },
|
|
52
|
+
{ type: "command", value: "graphify-mcp" },
|
|
53
|
+
{ type: "command", value: "bun" },
|
|
54
|
+
],
|
|
55
|
+
versionCommand: ["graphify", "--version"],
|
|
56
|
+
prepare: [
|
|
57
|
+
["graphify", "extract", ".", "--code-only", "--no-cluster"],
|
|
58
|
+
["graphify", "cluster-only", ".", "--no-label", "--no-viz"],
|
|
59
|
+
],
|
|
60
|
+
command: bunHarness("graphify-bakeoff.ts"),
|
|
61
|
+
caseMappings: [
|
|
62
|
+
{
|
|
63
|
+
idPattern: "^neighbors-calls-(?:path|default)$",
|
|
64
|
+
useCase: "direct call neighbors",
|
|
65
|
+
mode: "warm",
|
|
66
|
+
scope: {
|
|
67
|
+
competitorOperation: "get_neighbors",
|
|
68
|
+
knodinOperation: "query:traverse",
|
|
69
|
+
dimensions: ["target", "direction=both", "relation=call", "depth=1", "items=200"],
|
|
70
|
+
},
|
|
71
|
+
budgets: { bytes: null, tokens: null, items: 200 },
|
|
72
|
+
reason: "Both sides return direct inbound and outbound call neighbors for createEngine.",
|
|
73
|
+
oracleId: "contains-create-engine",
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "serena",
|
|
79
|
+
name: "Serena",
|
|
80
|
+
version: "1.6.2.dev0",
|
|
81
|
+
roadmap: ["C11", "C12", "C15", "C24", "C25"],
|
|
82
|
+
baseline: "serena-vs-knodin.raw.json",
|
|
83
|
+
requirements: [
|
|
84
|
+
{ type: "command", value: "serena" },
|
|
85
|
+
{ type: "command", value: "bun" },
|
|
86
|
+
],
|
|
87
|
+
versionCommand: ["serena", "--version"],
|
|
88
|
+
command: ({ output, fixture }) => ["bun", "scripts/serena-bakeoff.ts", fixture, output],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "codebase-memory",
|
|
92
|
+
name: "codebase-memory-mcp",
|
|
93
|
+
version: "0.9.0",
|
|
94
|
+
roadmap: ["C4", "C10", "C11", "C12", "C13", "C16", "C24", "C25", "C26"],
|
|
95
|
+
baseline: "codebase-memory-vs-knodin.raw.json",
|
|
96
|
+
requirements: [
|
|
97
|
+
{ type: "command", value: "codebase-memory-mcp" },
|
|
98
|
+
{ type: "command", value: "bun" },
|
|
99
|
+
],
|
|
100
|
+
versionCommand: ["codebase-memory-mcp", "--version"],
|
|
101
|
+
command: bunHarness("codebase-memory-bakeoff.ts"),
|
|
102
|
+
caseMappings: [
|
|
103
|
+
{
|
|
104
|
+
idPattern: "^search-graph-e0-c0$",
|
|
105
|
+
useCase: "bounded graph search",
|
|
106
|
+
mode: "warm",
|
|
107
|
+
scope: {
|
|
108
|
+
competitorOperation: "search_graph",
|
|
109
|
+
knodinOperation: "search",
|
|
110
|
+
dimensions: ["query=create engine", "offset=0", "items=10", "connected=false"],
|
|
111
|
+
},
|
|
112
|
+
budgets: { bytes: null, tokens: null, items: 10 },
|
|
113
|
+
reason: "Both sides run the same unexpanded first-page query with the same item limit.",
|
|
114
|
+
oracleId: "contains-create-engine",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
idPattern: "^trace-(?:inbound|outbound|both)-calls-r0-t1$",
|
|
118
|
+
useCase: "typed call traversal",
|
|
119
|
+
mode: "warm",
|
|
120
|
+
scope: {
|
|
121
|
+
competitorOperation: "trace_path",
|
|
122
|
+
knodinOperation: "query:traverse",
|
|
123
|
+
dimensions: [
|
|
124
|
+
"target=createEngine",
|
|
125
|
+
"direction",
|
|
126
|
+
"relation=call",
|
|
127
|
+
"depth=3",
|
|
128
|
+
"include_tests=true",
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
budgets: { bytes: null, tokens: null, items: 100 },
|
|
132
|
+
reason: "Both sides use the same target, direction, call relation, depth, and test scope.",
|
|
133
|
+
oracleId: "contains-create-engine",
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
idPattern: "^architecture-(?:languages|packages|entry_points|hotspots|boundaries|layers)$",
|
|
137
|
+
useCase: "single architecture facet",
|
|
138
|
+
mode: "warm",
|
|
139
|
+
scope: {
|
|
140
|
+
competitorOperation: "get_architecture",
|
|
141
|
+
knodinOperation: "query:architecture_overview",
|
|
142
|
+
dimensions: ["one named facet", "repository scope", "source inclusion=metadata"],
|
|
143
|
+
},
|
|
144
|
+
budgets: { bytes: null, tokens: null, items: null },
|
|
145
|
+
reason: "Each row requests one architecture facet that knodin supports independently.",
|
|
146
|
+
oracleId: "architecture-facet",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: "grepai",
|
|
152
|
+
name: "grepai",
|
|
153
|
+
version: "0.35.0",
|
|
154
|
+
roadmap: ["C3", "C11", "C12", "C16", "C17", "C24"],
|
|
155
|
+
baseline: "grepai-vs-knodin.raw.json",
|
|
156
|
+
requirements: [
|
|
157
|
+
{ type: "command", value: "grepai" },
|
|
158
|
+
{ type: "command", value: "bun" },
|
|
159
|
+
],
|
|
160
|
+
versionCommand: ["grepai", "version"],
|
|
161
|
+
command: bunHarness("grepai-bakeoff.ts"),
|
|
162
|
+
caseMappings: [
|
|
163
|
+
{
|
|
164
|
+
idPattern: "^status-json-v0$",
|
|
165
|
+
useCase: "cached index status",
|
|
166
|
+
mode: "warm",
|
|
167
|
+
scope: {
|
|
168
|
+
competitorOperation: "grepai_index_status",
|
|
169
|
+
knodinOperation: "status:cached",
|
|
170
|
+
dimensions: ["cached summary", "repository scope", "non-verbose"],
|
|
171
|
+
},
|
|
172
|
+
budgets: { bytes: null, tokens: null, items: null },
|
|
173
|
+
reason: "Both sides return a cached, non-deep index status summary.",
|
|
174
|
+
oracleId: "contains-index",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
idPattern: "^search-json-c0-l(?:1|5|20)-all$",
|
|
178
|
+
useCase: "bounded semantic search",
|
|
179
|
+
mode: "warm",
|
|
180
|
+
scope: {
|
|
181
|
+
competitorOperation: "grepai_search",
|
|
182
|
+
knodinOperation: "search",
|
|
183
|
+
dimensions: ["query=index freshness reconciliation", "repository scope", "items"],
|
|
184
|
+
},
|
|
185
|
+
budgets: { bytes: null, tokens: null, items: null },
|
|
186
|
+
reason: "Both sides use the same semantic query, repository scope, and row limit.",
|
|
187
|
+
oracleId: "contains-index",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
idPattern: "^grepai_trace_(?:callers|callees)-json-c0$",
|
|
191
|
+
useCase: "direct call neighbors",
|
|
192
|
+
mode: "warm",
|
|
193
|
+
scope: {
|
|
194
|
+
competitorOperation: "grepai_trace_callers|grepai_trace_callees",
|
|
195
|
+
knodinOperation: "query:callers_of|callees_of",
|
|
196
|
+
dimensions: ["target=createEngine", "direction", "depth=1", "relation=call"],
|
|
197
|
+
},
|
|
198
|
+
budgets: { bytes: null, tokens: null, items: null },
|
|
199
|
+
reason: "Both sides return the same direct call-neighbor direction for createEngine.",
|
|
200
|
+
oracleId: "contains-create-engine",
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "repomix",
|
|
206
|
+
name: "Repomix",
|
|
207
|
+
version: "1.17.0",
|
|
208
|
+
roadmap: ["C3", "C14", "C24"],
|
|
209
|
+
baseline: "repomix-vs-knodin.raw.json",
|
|
210
|
+
requirements: [
|
|
211
|
+
{ type: "command", value: "repomix" },
|
|
212
|
+
{ type: "command", value: "bun" },
|
|
213
|
+
],
|
|
214
|
+
versionCommand: ["repomix", "--version"],
|
|
215
|
+
command: ({ output, fixture }) => ["bun", "scripts/repomix-bakeoff.ts", fixture, output],
|
|
216
|
+
caseMappings: [
|
|
217
|
+
{
|
|
218
|
+
idPattern: "^pack-(?:xml|markdown|json)-c0$",
|
|
219
|
+
useCase: "two-file context pack",
|
|
220
|
+
mode: "warm",
|
|
221
|
+
scope: {
|
|
222
|
+
competitorOperation: "pack_codebase",
|
|
223
|
+
knodinOperation: "pack:export",
|
|
224
|
+
dimensions: ["format", "two includes", "test exclusion", "compression=false"],
|
|
225
|
+
},
|
|
226
|
+
budgets: { bytes: null, tokens: null, items: 2 },
|
|
227
|
+
reason: "Both sides pack the same two full files in the same supported format without compression.",
|
|
228
|
+
oracleId: "pack-two-files",
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
idPattern: "^read-(?:xml|markdown|json)-c0-(?:all|head|middle)$",
|
|
232
|
+
useCase: "bounded packed artifact read",
|
|
233
|
+
mode: "warm",
|
|
234
|
+
scope: {
|
|
235
|
+
competitorOperation: "read_repomix_output",
|
|
236
|
+
knodinOperation: "pack:read",
|
|
237
|
+
dimensions: ["same saved artifact", "same inclusive line range"],
|
|
238
|
+
},
|
|
239
|
+
budgets: { bytes: null, tokens: null, items: null },
|
|
240
|
+
reason: "Both sides read the same inclusive range from their immediately preceding pack.",
|
|
241
|
+
oracleId: "packed-read",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
idPattern: "^grep-(?:xml|markdown|json)-c0-i(?:0|1)-x0$",
|
|
245
|
+
useCase: "exact packed artifact grep",
|
|
246
|
+
mode: "warm",
|
|
247
|
+
scope: {
|
|
248
|
+
competitorOperation: "grep_repomix_output",
|
|
249
|
+
knodinOperation: "pack:grep",
|
|
250
|
+
dimensions: ["same saved artifact", "same regex", "same case mode", "context=0"],
|
|
251
|
+
},
|
|
252
|
+
budgets: { bytes: null, tokens: null, items: null },
|
|
253
|
+
reason: "Both sides grep their equivalent packs with the same expression and no context lines.",
|
|
254
|
+
oracleId: "grep-create-engine",
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "aider",
|
|
260
|
+
name: "Aider repo-map",
|
|
261
|
+
version: "0.86.2",
|
|
262
|
+
roadmap: ["C3", "C14", "C24"],
|
|
263
|
+
baseline: "aider-vs-knodin.raw.json",
|
|
264
|
+
requirements: [
|
|
265
|
+
{ type: "command", value: "aider" },
|
|
266
|
+
{ type: "command", value: "bun" },
|
|
267
|
+
],
|
|
268
|
+
versionCommand: ["aider", "--version"],
|
|
269
|
+
command: bunHarness("aider-repomap-bakeoff.ts"),
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
id: "code2prompt",
|
|
273
|
+
name: "code2prompt",
|
|
274
|
+
version: "4.2.0",
|
|
275
|
+
roadmap: ["C3", "C5", "C14", "C24"],
|
|
276
|
+
baseline: "code2prompt-vs-knodin.raw.json",
|
|
277
|
+
requirements: [
|
|
278
|
+
{ type: "command", value: "code2prompt" },
|
|
279
|
+
{ type: "command", value: "bun" },
|
|
280
|
+
],
|
|
281
|
+
versionCommand: ["code2prompt", "--version"],
|
|
282
|
+
command: bunHarness("code2prompt-bakeoff.ts"),
|
|
283
|
+
caseMappings: [
|
|
284
|
+
{
|
|
285
|
+
idPattern: "^format-(?:markdown|json|xml)-l(?:0|1)-t0-a0-c1$",
|
|
286
|
+
useCase: "two-file context pack",
|
|
287
|
+
mode: "warm",
|
|
288
|
+
scope: {
|
|
289
|
+
competitorOperation: "code2prompt",
|
|
290
|
+
knodinOperation: "pack:export",
|
|
291
|
+
dimensions: [
|
|
292
|
+
"format",
|
|
293
|
+
"line numbers",
|
|
294
|
+
"directory tree=false",
|
|
295
|
+
"relative paths",
|
|
296
|
+
"two includes",
|
|
297
|
+
],
|
|
298
|
+
},
|
|
299
|
+
budgets: { bytes: null, tokens: null, items: 2 },
|
|
300
|
+
reason: "Both sides pack the same two files with matching format, line, tree, and path settings.",
|
|
301
|
+
oracleId: "pack-two-files",
|
|
302
|
+
},
|
|
303
|
+
],
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
id: "claude-context",
|
|
307
|
+
name: "claude-context-mcp",
|
|
308
|
+
version: "0.1.15",
|
|
309
|
+
roadmap: ["C11", "C12", "C16", "C24"],
|
|
310
|
+
baseline: "claude-context-vs-knodin.raw.json",
|
|
311
|
+
requirements: [
|
|
312
|
+
{ type: "command", value: "claude-context-mcp" },
|
|
313
|
+
{ type: "command", value: "bun" },
|
|
314
|
+
{ type: "tcp", host: "127.0.0.1", port: 11434, label: "Ollama" },
|
|
315
|
+
{ type: "tcp", host: "127.0.0.1", port: 19530, label: "Milvus" },
|
|
316
|
+
],
|
|
317
|
+
versionCommand: ["bun", "pm", "ls", "-g"],
|
|
318
|
+
command: ({ output, fixture }) => ["bun", "scripts/claude-context-bakeoff.ts", fixture, output],
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: "mcp-codebase-index",
|
|
322
|
+
name: "mcp-codebase-index",
|
|
323
|
+
version: "1.6.0",
|
|
324
|
+
roadmap: ["C16", "C24"],
|
|
325
|
+
baseline: "mcp-codebase-index-blocker.raw.json",
|
|
326
|
+
requirements: [{ type: "command", value: "mcp-codebase-index" }],
|
|
327
|
+
versionCommand: ["bun", "pm", "ls", "-g"],
|
|
328
|
+
blocker: "Mandatory hosted Gemini embeddings violate the local-only protocol.",
|
|
329
|
+
},
|
|
330
|
+
];
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import childProcess from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
/**
|
|
4
|
+
* Twenty warm observations are the minimum that gives nearest-rank p95 a
|
|
5
|
+
* distinct tail observation instead of aliasing a three-sample median.
|
|
6
|
+
*/
|
|
7
|
+
export const MIN_WARM_SAMPLES = 20;
|
|
8
|
+
export const DEFAULT_WARMUP_COUNT = 3;
|
|
9
|
+
export const DEFAULT_COLD_SAMPLES = 5;
|
|
10
|
+
export const DEFAULT_CALL_TIMEOUT_MS = 30_000;
|
|
11
|
+
function percentile(sorted, fraction) {
|
|
12
|
+
const index = Math.max(0, Math.ceil(fraction * sorted.length) - 1);
|
|
13
|
+
return sorted[index];
|
|
14
|
+
}
|
|
15
|
+
export function summarizeLatencySamples(mode, samplesMs, warmupCount) {
|
|
16
|
+
if (!Number.isInteger(warmupCount) || warmupCount < 0)
|
|
17
|
+
throw new Error("Warm-up count must be a non-negative integer");
|
|
18
|
+
if (samplesMs.length === 0)
|
|
19
|
+
throw new Error("At least one latency sample is required");
|
|
20
|
+
if (mode === "warm" && samplesMs.length < MIN_WARM_SAMPLES) {
|
|
21
|
+
throw new Error(`Warm measurements require at least ${MIN_WARM_SAMPLES} samples for a meaningful p95`);
|
|
22
|
+
}
|
|
23
|
+
if (samplesMs.some((sample) => !Number.isFinite(sample) || sample < 0))
|
|
24
|
+
throw new Error("Latency samples must be finite non-negative numbers");
|
|
25
|
+
const raw = [...samplesMs];
|
|
26
|
+
const sorted = [...raw].sort((left, right) => left - right);
|
|
27
|
+
const p50Ms = percentile(sorted, 0.5);
|
|
28
|
+
return {
|
|
29
|
+
mode,
|
|
30
|
+
warmupCount,
|
|
31
|
+
sampleCount: raw.length,
|
|
32
|
+
samplesMs: raw,
|
|
33
|
+
p50Ms,
|
|
34
|
+
p95Ms: percentile(sorted, 0.95),
|
|
35
|
+
minMs: sorted[0],
|
|
36
|
+
maxMs: sorted.at(-1),
|
|
37
|
+
medianMs: p50Ms,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export async function sampleOperation(operation, options) {
|
|
41
|
+
const mode = options.mode ?? "warm";
|
|
42
|
+
const warmupCount = mode === "warm" ? (options.warmupCount ?? DEFAULT_WARMUP_COUNT) : 0;
|
|
43
|
+
const sampleCount = options.sampleCount ?? (mode === "warm" ? MIN_WARM_SAMPLES : DEFAULT_COLD_SAMPLES);
|
|
44
|
+
let value;
|
|
45
|
+
const call = async (kind, index) => {
|
|
46
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
47
|
+
let timer;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
operation(),
|
|
51
|
+
new Promise((_, reject) => {
|
|
52
|
+
timer = setTimeout(async () => {
|
|
53
|
+
try {
|
|
54
|
+
if (options.onTimeout)
|
|
55
|
+
await options.onTimeout(kind, index);
|
|
56
|
+
else
|
|
57
|
+
reapDescendants(process.pid);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Cleanup failure must not replace or strand the timeout result.
|
|
61
|
+
}
|
|
62
|
+
finally {
|
|
63
|
+
reject(new Error(`Competitive ${kind} call ${index + 1} timed out after ${timeoutMs}ms`));
|
|
64
|
+
}
|
|
65
|
+
}, timeoutMs);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (timer)
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
for (let index = 0; index < warmupCount; index++) {
|
|
76
|
+
await options.beforeSample?.("warmup", index);
|
|
77
|
+
value = await call("warmup", index);
|
|
78
|
+
}
|
|
79
|
+
const samplesMs = [];
|
|
80
|
+
for (let index = 0; index < sampleCount; index++) {
|
|
81
|
+
await options.beforeSample?.("sample", index);
|
|
82
|
+
const startedAt = performance.now();
|
|
83
|
+
value = await call("sample", index);
|
|
84
|
+
samplesMs.push(Number((performance.now() - startedAt).toFixed(3)));
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
value: value,
|
|
88
|
+
measurement: summarizeLatencySamples(mode, samplesMs, warmupCount),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
export function reapDescendants(rootPid) {
|
|
92
|
+
const output = childProcess.spawnSync("ps", ["-axo", "pid=,ppid="]).stdout.toString();
|
|
93
|
+
const processes = output
|
|
94
|
+
.split("\n")
|
|
95
|
+
.map((line) => line.trim().split(/\s+/).map(Number))
|
|
96
|
+
.filter(([pid, parentPid]) => Number.isInteger(pid) && Number.isInteger(parentPid));
|
|
97
|
+
const descendants = new Set([rootPid]);
|
|
98
|
+
for (let changed = true; changed;) {
|
|
99
|
+
changed = false;
|
|
100
|
+
for (const [pid, parentPid] of processes) {
|
|
101
|
+
if (!descendants.has(pid) && descendants.has(parentPid)) {
|
|
102
|
+
descendants.add(pid);
|
|
103
|
+
changed = true;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
descendants.delete(rootPid);
|
|
108
|
+
for (const signal of ["-TERM", "-KILL"]) {
|
|
109
|
+
for (const pid of [...descendants].reverse()) {
|
|
110
|
+
childProcess.spawnSync("/bin/kill", [signal, String(pid)]);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function createCheckpointingResults(outputPath) {
|
|
115
|
+
const results = [];
|
|
116
|
+
results.push = (...items) => {
|
|
117
|
+
const length = Array.prototype.push.apply(results, items);
|
|
118
|
+
const temporary = `${outputPath}.partial-${process.pid}`;
|
|
119
|
+
fs.writeFileSync(temporary, `${JSON.stringify({
|
|
120
|
+
schemaVersion: 2,
|
|
121
|
+
status: "partial",
|
|
122
|
+
terminal: false,
|
|
123
|
+
partial: true,
|
|
124
|
+
generatedAt: new Date().toISOString(),
|
|
125
|
+
results,
|
|
126
|
+
}, null, 2)}\n`);
|
|
127
|
+
fs.renameSync(temporary, outputPath);
|
|
128
|
+
return length;
|
|
129
|
+
};
|
|
130
|
+
return results;
|
|
131
|
+
}
|
|
132
|
+
function emptyOwner() {
|
|
133
|
+
return { rssBytes: 0, pids: [] };
|
|
134
|
+
}
|
|
135
|
+
export function attributeProcessTree(processes, rootPid, patterns, phase, rootOwner = "harness") {
|
|
136
|
+
const descendants = new Set([rootPid]);
|
|
137
|
+
for (let changed = true; changed;) {
|
|
138
|
+
changed = false;
|
|
139
|
+
for (const process of processes) {
|
|
140
|
+
if (!descendants.has(process.pid) && descendants.has(process.parentPid)) {
|
|
141
|
+
descendants.add(process.pid);
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const result = {
|
|
147
|
+
phase,
|
|
148
|
+
harness: emptyOwner(),
|
|
149
|
+
knodin: emptyOwner(),
|
|
150
|
+
competitor: emptyOwner(),
|
|
151
|
+
unknown: emptyOwner(),
|
|
152
|
+
};
|
|
153
|
+
for (const process of processes) {
|
|
154
|
+
if (!descendants.has(process.pid))
|
|
155
|
+
continue;
|
|
156
|
+
const owner = process.pid === rootPid
|
|
157
|
+
? rootOwner
|
|
158
|
+
: patterns.knodin.some((pattern) => pattern.test(process.command))
|
|
159
|
+
? "knodin"
|
|
160
|
+
: patterns.competitor.some((pattern) => pattern.test(process.command))
|
|
161
|
+
? "competitor"
|
|
162
|
+
: "unknown";
|
|
163
|
+
result[owner].rssBytes += process.rssBytes;
|
|
164
|
+
result[owner].pids.push(process.pid);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
export function mergeResourcePeaks(left, right) {
|
|
169
|
+
const merged = {
|
|
170
|
+
phase: right.phase,
|
|
171
|
+
harness: emptyOwner(),
|
|
172
|
+
knodin: emptyOwner(),
|
|
173
|
+
competitor: emptyOwner(),
|
|
174
|
+
unknown: emptyOwner(),
|
|
175
|
+
};
|
|
176
|
+
for (const owner of ["harness", "knodin", "competitor", "unknown"]) {
|
|
177
|
+
merged[owner] =
|
|
178
|
+
right[owner].rssBytes >= left[owner].rssBytes
|
|
179
|
+
? { ...right[owner], pids: [...right[owner].pids] }
|
|
180
|
+
: { ...left[owner], pids: [...left[owner].pids] };
|
|
181
|
+
}
|
|
182
|
+
return merged;
|
|
183
|
+
}
|