viberag 0.7.0 → 0.8.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/dist/cli/commands/useCommands.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/types.d.ts +6 -0
- package/dist/daemon/lib/chunker/index.js +32 -20
- package/dist/daemon/lib/telemetry/client.d.ts +1 -1
- package/dist/daemon/lib/telemetry/client.js +1 -1
- package/dist/daemon/lib/telemetry/privacy-policy.d.ts +1 -1
- package/dist/daemon/lib/telemetry/privacy-policy.js +1 -1
- package/dist/daemon/lib/telemetry/sentry.d.ts +10 -4
- package/dist/daemon/lib/telemetry/sentry.js +53 -3
- package/dist/daemon/lib/user-settings.js +1 -1
- package/dist/daemon/owner.d.ts +23 -0
- package/dist/daemon/owner.js +106 -2
- package/dist/daemon/services/memory-monitor-sentry.d.ts +32 -0
- package/dist/daemon/services/memory-monitor-sentry.js +200 -0
- package/dist/daemon/services/memory-monitor.d.ts +136 -0
- package/dist/daemon/services/memory-monitor.js +509 -0
- package/dist/daemon/services/v2/indexing.js +113 -46
- package/dist/daemon/services/v2/search/engine.js +138 -26
- package/dist/daemon/services/v2/storage/index.d.ts +1 -0
- package/dist/daemon/services/v2/storage/index.js +18 -0
- package/dist/mcp/server.js +3 -2
- package/package.json +9 -2
- package/scripts/memory/README.md +172 -0
- package/scripts/memory/isolate-intent-growth.mjs +348 -0
- package/scripts/memory/out/.gitkeep +1 -0
- package/scripts/memory/profile.mjs +188 -0
- package/scripts/memory/stress-search-growth.mjs +560 -0
- package/scripts/memory/watch-reindex-stress.mjs +683 -0
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import {execFile as execFileCb} from 'node:child_process';
|
|
6
|
+
import {promisify} from 'node:util';
|
|
7
|
+
import {fileURLToPath} from 'node:url';
|
|
8
|
+
|
|
9
|
+
const execFile = promisify(execFileCb);
|
|
10
|
+
|
|
11
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const REPO_ROOT = path.resolve(SCRIPT_DIR, '..', '..');
|
|
13
|
+
const DAEMON_ENTRY = path.join(REPO_ROOT, 'dist/daemon/index.js');
|
|
14
|
+
|
|
15
|
+
const DURATION_SECONDS = Number(process.env['VIBERAG_STRESS_SECONDS'] ?? '60');
|
|
16
|
+
const PARALLELISM = Number(process.env['VIBERAG_STRESS_PARALLELISM'] ?? '24');
|
|
17
|
+
const SAMPLE_MS = Number(process.env['VIBERAG_STRESS_SAMPLE_MS'] ?? '500');
|
|
18
|
+
const REQUESTED_RESULT_K = Number(process.env['VIBERAG_STRESS_K'] ?? '96');
|
|
19
|
+
const RESULT_K = Math.max(1, Math.min(100, REQUESTED_RESULT_K));
|
|
20
|
+
const COOLDOWN_SECONDS = Number(
|
|
21
|
+
process.env['VIBERAG_STRESS_COOLDOWN_SECONDS'] ?? '10',
|
|
22
|
+
);
|
|
23
|
+
const INDEX_CHURN_SECONDS = Number(
|
|
24
|
+
process.env['VIBERAG_STRESS_INDEX_CHURN_SECONDS'] ?? '0',
|
|
25
|
+
);
|
|
26
|
+
const INDEX_FORCE = process.env['VIBERAG_STRESS_INDEX_FORCE'] === '1';
|
|
27
|
+
const MAX_RSS_MB = Number(process.env['VIBERAG_STRESS_MAX_RSS_MB'] ?? '10240');
|
|
28
|
+
const SOFT_RSS_MB = Number(
|
|
29
|
+
process.env['VIBERAG_STRESS_SOFT_RSS_MB'] ?? Math.floor(MAX_RSS_MB * 0.8),
|
|
30
|
+
);
|
|
31
|
+
const MAX_IN_FLIGHT = Math.max(
|
|
32
|
+
1,
|
|
33
|
+
Math.min(
|
|
34
|
+
PARALLELISM,
|
|
35
|
+
Number(process.env['VIBERAG_STRESS_MAX_IN_FLIGHT'] ?? '8'),
|
|
36
|
+
),
|
|
37
|
+
);
|
|
38
|
+
const RAMP_STEP_MS = Number(
|
|
39
|
+
process.env['VIBERAG_STRESS_RAMP_STEP_MS'] ?? '500',
|
|
40
|
+
);
|
|
41
|
+
const GUARD_KILL = process.env['VIBERAG_STRESS_GUARD_KILL'] !== '0';
|
|
42
|
+
const OUTPUT_DIR = process.env['VIBERAG_STRESS_OUTPUT_DIR']
|
|
43
|
+
? path.resolve(process.env['VIBERAG_STRESS_OUTPUT_DIR'])
|
|
44
|
+
: path.join(REPO_ROOT, 'scripts/memory/out');
|
|
45
|
+
|
|
46
|
+
if (!Number.isFinite(DURATION_SECONDS) || DURATION_SECONDS <= 0) {
|
|
47
|
+
throw new Error('VIBERAG_STRESS_SECONDS must be > 0');
|
|
48
|
+
}
|
|
49
|
+
if (!Number.isFinite(PARALLELISM) || PARALLELISM <= 0) {
|
|
50
|
+
throw new Error('VIBERAG_STRESS_PARALLELISM must be > 0');
|
|
51
|
+
}
|
|
52
|
+
if (!Number.isFinite(SAMPLE_MS) || SAMPLE_MS < 100) {
|
|
53
|
+
throw new Error('VIBERAG_STRESS_SAMPLE_MS must be >= 100');
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isFinite(MAX_RSS_MB) || MAX_RSS_MB <= 0) {
|
|
56
|
+
throw new Error('VIBERAG_STRESS_MAX_RSS_MB must be > 0');
|
|
57
|
+
}
|
|
58
|
+
if (
|
|
59
|
+
!Number.isFinite(SOFT_RSS_MB) ||
|
|
60
|
+
SOFT_RSS_MB <= 0 ||
|
|
61
|
+
SOFT_RSS_MB > MAX_RSS_MB
|
|
62
|
+
) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
'VIBERAG_STRESS_SOFT_RSS_MB must be > 0 and <= VIBERAG_STRESS_MAX_RSS_MB',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
if (!Number.isFinite(MAX_IN_FLIGHT) || MAX_IN_FLIGHT <= 0) {
|
|
68
|
+
throw new Error('VIBERAG_STRESS_MAX_IN_FLIGHT must be > 0');
|
|
69
|
+
}
|
|
70
|
+
if (!Number.isFinite(RAMP_STEP_MS) || RAMP_STEP_MS < 100) {
|
|
71
|
+
throw new Error('VIBERAG_STRESS_RAMP_STEP_MS must be >= 100');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const QUERY_POOL = [
|
|
75
|
+
'indexing memory lifecycle ownership contract cleanup',
|
|
76
|
+
'lancedb upsert pipeline flush batch rollback idempotent',
|
|
77
|
+
'chunker parse tree wasm parser close release resources',
|
|
78
|
+
'search engine warmup embeddings cache memory pressure',
|
|
79
|
+
'watcher pending changes indexing throttling backpressure',
|
|
80
|
+
'daemon status rss heap external telemetry sentry threshold',
|
|
81
|
+
'merkle tree hashing file change detection incremental',
|
|
82
|
+
'find_references symbol usage expansion surrounding code',
|
|
83
|
+
'exact_text throwIfAborted error path cleanup finally',
|
|
84
|
+
'semantic search hybrid vector fts ranking explain',
|
|
85
|
+
'storage close table handles connection lifecycle',
|
|
86
|
+
'indexAsync status cancel operation indexing phase',
|
|
87
|
+
'grammar smoke parser initialization teardown',
|
|
88
|
+
'embedding provider batch retry limits slot state',
|
|
89
|
+
'project manifest persistence resume startup checks',
|
|
90
|
+
'memory regression test chunker repeated analyzeFile',
|
|
91
|
+
'indexing memory regression repeated force runs',
|
|
92
|
+
'request_id telemetry operation boundaries mcp source',
|
|
93
|
+
'reindex required schema compatibility manifest version',
|
|
94
|
+
'daemon owner state machine transitions indexing warmup',
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
const INTENTS = ['concept', 'usage', 'definition', 'exact_text', 'auto'];
|
|
98
|
+
|
|
99
|
+
function sleep(ms) {
|
|
100
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function nowIso() {
|
|
104
|
+
return new Date().toISOString();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function compactError(error) {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
return `${error.name}: ${error.message}`;
|
|
110
|
+
}
|
|
111
|
+
return String(error);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function mbFromKb(value) {
|
|
115
|
+
return Number((value / 1024).toFixed(1));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
119
|
+
const timeout = new Promise((_, reject) => {
|
|
120
|
+
setTimeout(
|
|
121
|
+
() => reject(new Error(`timeout:${label}:${timeoutMs}ms`)),
|
|
122
|
+
timeoutMs,
|
|
123
|
+
);
|
|
124
|
+
});
|
|
125
|
+
return Promise.race([promise, timeout]);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function findDaemonProcess() {
|
|
129
|
+
const {stdout} = await execFile('ps', [
|
|
130
|
+
'-axo',
|
|
131
|
+
'pid=,rss=,vsz=,%cpu=,%mem=,state=,etime=,command=',
|
|
132
|
+
]);
|
|
133
|
+
const lines = stdout.split('\n');
|
|
134
|
+
for (const line of lines) {
|
|
135
|
+
if (!line.includes(DAEMON_ENTRY)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const match = line.match(
|
|
139
|
+
/^\s*(\d+)\s+(\d+)\s+(\d+)\s+([0-9.]+)\s+([0-9.]+)\s+(\S+)\s+(\S+)\s+(.+)$/,
|
|
140
|
+
);
|
|
141
|
+
if (!match) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
pid: Number(match[1]),
|
|
146
|
+
rssMB: mbFromKb(Number(match[2])),
|
|
147
|
+
vszMB: mbFromKb(Number(match[3])),
|
|
148
|
+
cpuPercent: Number(match[4]),
|
|
149
|
+
memPercent: Number(match[5]),
|
|
150
|
+
state: match[6],
|
|
151
|
+
etime: match[7],
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function addErrorBucket(state, error) {
|
|
158
|
+
const key = compactError(error).slice(0, 220);
|
|
159
|
+
const current = state.errorBuckets.get(key) ?? 0;
|
|
160
|
+
state.errorBuckets.set(key, current + 1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function topErrorBuckets(state, limit = 10) {
|
|
164
|
+
return [...state.errorBuckets.entries()]
|
|
165
|
+
.sort((a, b) => b[1] - a[1])
|
|
166
|
+
.slice(0, limit)
|
|
167
|
+
.map(([error, count]) => ({error, count}));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function summarizeSamples(samples) {
|
|
171
|
+
const observed = samples
|
|
172
|
+
.map(sample => sample.observedRssMB)
|
|
173
|
+
.filter(Number.isFinite);
|
|
174
|
+
const statusRss = samples
|
|
175
|
+
.map(sample => sample.rssMBStatus)
|
|
176
|
+
.filter(Number.isFinite);
|
|
177
|
+
const psRss = samples.map(sample => sample.rssMBPs).filter(Number.isFinite);
|
|
178
|
+
if (observed.length === 0) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
observedStartMB: observed[0],
|
|
183
|
+
observedEndMB: observed.at(-1),
|
|
184
|
+
observedPeakMB: Math.max(...observed),
|
|
185
|
+
observedGrowthMB: Number((observed.at(-1) - observed[0]).toFixed(1)),
|
|
186
|
+
statusPeakMB: statusRss.length > 0 ? Math.max(...statusRss) : null,
|
|
187
|
+
psPeakMB: psRss.length > 0 ? Math.max(...psRss) : null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const {DaemonClient} = await import(
|
|
192
|
+
path.join(REPO_ROOT, 'dist/client/index.js')
|
|
193
|
+
);
|
|
194
|
+
const client = new DaemonClient({
|
|
195
|
+
projectRoot: REPO_ROOT,
|
|
196
|
+
autoStart: true,
|
|
197
|
+
connectTimeout: 30_000,
|
|
198
|
+
clientSource: 'mcp',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
const runId = new Date().toISOString().replace(/[:.]/g, '-');
|
|
202
|
+
const outputFile = path.join(OUTPUT_DIR, `stress-search-growth-${runId}.jsonl`);
|
|
203
|
+
|
|
204
|
+
const state = {
|
|
205
|
+
stop: false,
|
|
206
|
+
stopReason: null,
|
|
207
|
+
completed: 0,
|
|
208
|
+
failed: 0,
|
|
209
|
+
inFlight: 0,
|
|
210
|
+
currentInFlightLimit: 1,
|
|
211
|
+
maxObservedInFlight: 0,
|
|
212
|
+
indexRequests: 0,
|
|
213
|
+
totalLatencyMs: 0,
|
|
214
|
+
maxLatencyMs: 0,
|
|
215
|
+
lastError: null,
|
|
216
|
+
lastObservedRssMB: null,
|
|
217
|
+
lastSeenPid: null,
|
|
218
|
+
errorBuckets: new Map(),
|
|
219
|
+
guardTriggered: false,
|
|
220
|
+
guardTriggeredAt: null,
|
|
221
|
+
guardObservedRssMB: null,
|
|
222
|
+
guardKind: null,
|
|
223
|
+
guardCancelError: null,
|
|
224
|
+
guardShutdownError: null,
|
|
225
|
+
guardKillError: null,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const samples = [];
|
|
229
|
+
|
|
230
|
+
async function writeRow(row) {
|
|
231
|
+
await fs.appendFile(outputFile, `${JSON.stringify(row)}\n`, 'utf8');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function triggerGuard(kind, row) {
|
|
235
|
+
if (state.guardTriggered) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
state.guardTriggered = true;
|
|
239
|
+
state.guardTriggeredAt = row.ts;
|
|
240
|
+
state.guardObservedRssMB = row.observedRssMB;
|
|
241
|
+
state.guardKind = kind;
|
|
242
|
+
state.stop = true;
|
|
243
|
+
state.stopReason = `${kind}_guard`;
|
|
244
|
+
row.guardTriggered = true;
|
|
245
|
+
row.guardKind = kind;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await withTimeout(
|
|
249
|
+
client.cancel({
|
|
250
|
+
target: 'all',
|
|
251
|
+
reason: `${kind} guard triggered at ${row.observedRssMB}MB`,
|
|
252
|
+
}),
|
|
253
|
+
1200,
|
|
254
|
+
'cancel',
|
|
255
|
+
);
|
|
256
|
+
} catch (error) {
|
|
257
|
+
state.guardCancelError = compactError(error);
|
|
258
|
+
addErrorBucket(state, error);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await withTimeout(
|
|
263
|
+
client.shutdown(`${kind} memory guard requested shutdown`),
|
|
264
|
+
1200,
|
|
265
|
+
'shutdown',
|
|
266
|
+
);
|
|
267
|
+
} catch (error) {
|
|
268
|
+
state.guardShutdownError = compactError(error);
|
|
269
|
+
addErrorBucket(state, error);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (GUARD_KILL && Number.isInteger(state.lastSeenPid)) {
|
|
273
|
+
try {
|
|
274
|
+
process.kill(state.lastSeenPid, 'SIGTERM');
|
|
275
|
+
} catch (error) {
|
|
276
|
+
state.guardKillError = compactError(error);
|
|
277
|
+
addErrorBucket(state, error);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function sample(label, elapsedSec) {
|
|
283
|
+
let status = null;
|
|
284
|
+
let statusError = null;
|
|
285
|
+
try {
|
|
286
|
+
status = await client.status();
|
|
287
|
+
} catch (error) {
|
|
288
|
+
statusError = compactError(error);
|
|
289
|
+
addErrorBucket(state, error);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let psSample = null;
|
|
293
|
+
let psError = null;
|
|
294
|
+
try {
|
|
295
|
+
psSample = await findDaemonProcess();
|
|
296
|
+
} catch (error) {
|
|
297
|
+
psError = compactError(error);
|
|
298
|
+
addErrorBucket(state, error);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const slots = Array.isArray(status?.slots) ? status.slots : [];
|
|
302
|
+
const failures = Array.isArray(status?.failures) ? status.failures : [];
|
|
303
|
+
const row = {
|
|
304
|
+
ts: nowIso(),
|
|
305
|
+
label,
|
|
306
|
+
elapsedSec,
|
|
307
|
+
pid: psSample?.pid ?? null,
|
|
308
|
+
rssMBPs: psSample?.rssMB ?? null,
|
|
309
|
+
vszMBPs: psSample?.vszMB ?? null,
|
|
310
|
+
cpuPercentPs: psSample?.cpuPercent ?? null,
|
|
311
|
+
memPercentPs: psSample?.memPercent ?? null,
|
|
312
|
+
processStatePs: psSample?.state ?? null,
|
|
313
|
+
processEtimePs: psSample?.etime ?? null,
|
|
314
|
+
rssMBStatus: status?.memory?.rssMB ?? null,
|
|
315
|
+
heapUsedMB: status?.memory?.heapUsedMB ?? null,
|
|
316
|
+
externalMB: status?.memory?.externalMB ?? null,
|
|
317
|
+
arrayBuffersMB: status?.memory?.arrayBuffersMB ?? null,
|
|
318
|
+
indexingStatus: status?.indexing?.status ?? null,
|
|
319
|
+
indexingPhase: status?.indexing?.phase ?? null,
|
|
320
|
+
indexingStage: status?.indexing?.stage ?? null,
|
|
321
|
+
indexingCurrent: status?.indexing?.current ?? null,
|
|
322
|
+
indexingTotal: status?.indexing?.total ?? null,
|
|
323
|
+
warmupStatus: status?.warmupStatus ?? null,
|
|
324
|
+
slotsTotal: slots.length,
|
|
325
|
+
slotsBusy: slots.filter(slot => slot.state !== 'idle').length,
|
|
326
|
+
slotsRateLimited: slots.filter(slot => slot.state === 'rate-limited')
|
|
327
|
+
.length,
|
|
328
|
+
failuresCount: failures.length,
|
|
329
|
+
completedSearches: state.completed,
|
|
330
|
+
failedSearches: state.failed,
|
|
331
|
+
inFlightSearches: state.inFlight,
|
|
332
|
+
inFlightLimit: state.currentInFlightLimit,
|
|
333
|
+
indexRequests: state.indexRequests,
|
|
334
|
+
lastError: state.lastError,
|
|
335
|
+
statusError,
|
|
336
|
+
psError,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
if (Number.isInteger(psSample?.pid)) {
|
|
340
|
+
state.lastSeenPid = psSample.pid;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const observedCandidates = [row.rssMBPs, row.rssMBStatus].filter(
|
|
344
|
+
Number.isFinite,
|
|
345
|
+
);
|
|
346
|
+
row.observedRssMB =
|
|
347
|
+
observedCandidates.length > 0
|
|
348
|
+
? Number(Math.max(...observedCandidates).toFixed(1))
|
|
349
|
+
: null;
|
|
350
|
+
if (Number.isFinite(row.observedRssMB)) {
|
|
351
|
+
state.lastObservedRssMB = row.observedRssMB;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (!state.guardTriggered && Number.isFinite(row.observedRssMB)) {
|
|
355
|
+
if (row.observedRssMB >= MAX_RSS_MB) {
|
|
356
|
+
await triggerGuard('hard', row);
|
|
357
|
+
} else if (row.observedRssMB >= SOFT_RSS_MB) {
|
|
358
|
+
await triggerGuard('soft', row);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
samples.push(row);
|
|
363
|
+
await writeRow(row);
|
|
364
|
+
console.log(JSON.stringify(row));
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
async function runRamp(deadlineMs) {
|
|
368
|
+
while (
|
|
369
|
+
!state.stop &&
|
|
370
|
+
Date.now() < deadlineMs &&
|
|
371
|
+
state.currentInFlightLimit < MAX_IN_FLIGHT
|
|
372
|
+
) {
|
|
373
|
+
await sleep(RAMP_STEP_MS);
|
|
374
|
+
if (state.stop) {
|
|
375
|
+
break;
|
|
376
|
+
}
|
|
377
|
+
state.currentInFlightLimit = Math.min(
|
|
378
|
+
MAX_IN_FLIGHT,
|
|
379
|
+
state.currentInFlightLimit + 1,
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
async function runWorker(workerId, deadlineMs) {
|
|
385
|
+
let iteration = 0;
|
|
386
|
+
while (!state.stop && Date.now() < deadlineMs) {
|
|
387
|
+
while (
|
|
388
|
+
!state.stop &&
|
|
389
|
+
Date.now() < deadlineMs &&
|
|
390
|
+
state.inFlight >= state.currentInFlightLimit
|
|
391
|
+
) {
|
|
392
|
+
await sleep(10);
|
|
393
|
+
}
|
|
394
|
+
if (state.stop || Date.now() >= deadlineMs) {
|
|
395
|
+
break;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const intent = INTENTS[(workerId + iteration) % INTENTS.length];
|
|
399
|
+
const base = QUERY_POOL[(workerId + iteration) % QUERY_POOL.length];
|
|
400
|
+
const query = `${base} worker:${workerId} iter:${iteration} nonce:${Date.now().toString(36)}`;
|
|
401
|
+
const started = Date.now();
|
|
402
|
+
|
|
403
|
+
state.inFlight += 1;
|
|
404
|
+
state.maxObservedInFlight = Math.max(
|
|
405
|
+
state.maxObservedInFlight,
|
|
406
|
+
state.inFlight,
|
|
407
|
+
);
|
|
408
|
+
try {
|
|
409
|
+
await client.search(query, {
|
|
410
|
+
intent,
|
|
411
|
+
k: RESULT_K,
|
|
412
|
+
explain: true,
|
|
413
|
+
});
|
|
414
|
+
const latency = Date.now() - started;
|
|
415
|
+
state.completed += 1;
|
|
416
|
+
state.totalLatencyMs += latency;
|
|
417
|
+
state.maxLatencyMs = Math.max(state.maxLatencyMs, latency);
|
|
418
|
+
} catch (error) {
|
|
419
|
+
state.failed += 1;
|
|
420
|
+
state.lastError = compactError(error);
|
|
421
|
+
addErrorBucket(state, error);
|
|
422
|
+
await sleep(25);
|
|
423
|
+
} finally {
|
|
424
|
+
state.inFlight = Math.max(0, state.inFlight - 1);
|
|
425
|
+
}
|
|
426
|
+
iteration += 1;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function runIndexChurn(deadlineMs) {
|
|
431
|
+
if (!Number.isFinite(INDEX_CHURN_SECONDS) || INDEX_CHURN_SECONDS <= 0) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const intervalMs = INDEX_CHURN_SECONDS * 1000;
|
|
435
|
+
while (!state.stop && Date.now() < deadlineMs) {
|
|
436
|
+
await sleep(intervalMs);
|
|
437
|
+
if (state.stop || Date.now() >= deadlineMs) {
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
await client.indexAsync({force: INDEX_FORCE});
|
|
442
|
+
state.indexRequests += 1;
|
|
443
|
+
} catch (error) {
|
|
444
|
+
state.lastError = compactError(error);
|
|
445
|
+
addErrorBucket(state, error);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
await fs.mkdir(OUTPUT_DIR, {recursive: true});
|
|
451
|
+
await fs.writeFile(outputFile, '', 'utf8');
|
|
452
|
+
await client.connect();
|
|
453
|
+
|
|
454
|
+
const startedAt = Date.now();
|
|
455
|
+
const deadlineMs = startedAt + DURATION_SECONDS * 1000;
|
|
456
|
+
|
|
457
|
+
const header = {
|
|
458
|
+
ts: nowIso(),
|
|
459
|
+
label: 'run.start',
|
|
460
|
+
durationSeconds: DURATION_SECONDS,
|
|
461
|
+
parallelism: PARALLELISM,
|
|
462
|
+
sampleMs: SAMPLE_MS,
|
|
463
|
+
requestedResultK: REQUESTED_RESULT_K,
|
|
464
|
+
resultK: RESULT_K,
|
|
465
|
+
maxRssMB: MAX_RSS_MB,
|
|
466
|
+
softRssMB: SOFT_RSS_MB,
|
|
467
|
+
maxInFlight: MAX_IN_FLIGHT,
|
|
468
|
+
rampStepMs: RAMP_STEP_MS,
|
|
469
|
+
guardKill: GUARD_KILL,
|
|
470
|
+
cooldownSeconds: COOLDOWN_SECONDS,
|
|
471
|
+
indexChurnSeconds: INDEX_CHURN_SECONDS,
|
|
472
|
+
indexForce: INDEX_FORCE,
|
|
473
|
+
outputFile,
|
|
474
|
+
};
|
|
475
|
+
await writeRow(header);
|
|
476
|
+
console.log(JSON.stringify(header));
|
|
477
|
+
|
|
478
|
+
if (REQUESTED_RESULT_K !== RESULT_K) {
|
|
479
|
+
console.log(
|
|
480
|
+
JSON.stringify({
|
|
481
|
+
ts: nowIso(),
|
|
482
|
+
label: 'run.note',
|
|
483
|
+
note: `clamped resultK from ${REQUESTED_RESULT_K} to ${RESULT_K} (daemon max is 100)`,
|
|
484
|
+
}),
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
await sample('baseline', 0);
|
|
489
|
+
|
|
490
|
+
const ramp = runRamp(deadlineMs);
|
|
491
|
+
const workers = Array.from({length: PARALLELISM}, (_, workerId) =>
|
|
492
|
+
runWorker(workerId, deadlineMs),
|
|
493
|
+
);
|
|
494
|
+
const churn = runIndexChurn(deadlineMs);
|
|
495
|
+
|
|
496
|
+
while (Date.now() < deadlineMs && !state.stop) {
|
|
497
|
+
await sleep(SAMPLE_MS);
|
|
498
|
+
const elapsedSec = Number(((Date.now() - startedAt) / 1000).toFixed(1));
|
|
499
|
+
await sample('active', elapsedSec);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
if (!state.stopReason) {
|
|
503
|
+
state.stopReason = 'duration_complete';
|
|
504
|
+
}
|
|
505
|
+
state.stop = true;
|
|
506
|
+
|
|
507
|
+
const workerSettled = await Promise.race([
|
|
508
|
+
Promise.allSettled([...workers, churn, ramp]).then(() => true),
|
|
509
|
+
sleep(20_000).then(() => false),
|
|
510
|
+
]);
|
|
511
|
+
|
|
512
|
+
await sample(
|
|
513
|
+
'post.active',
|
|
514
|
+
Number(((Date.now() - startedAt) / 1000).toFixed(1)),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (COOLDOWN_SECONDS > 0 && !state.guardTriggered) {
|
|
518
|
+
const cooldownEnd = Date.now() + COOLDOWN_SECONDS * 1000;
|
|
519
|
+
while (Date.now() < cooldownEnd) {
|
|
520
|
+
await sleep(SAMPLE_MS);
|
|
521
|
+
const elapsedSec = Number(((Date.now() - startedAt) / 1000).toFixed(1));
|
|
522
|
+
await sample('cooldown', elapsedSec);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
const avgLatencyMs =
|
|
527
|
+
state.completed > 0
|
|
528
|
+
? Number((state.totalLatencyMs / state.completed).toFixed(1))
|
|
529
|
+
: 0;
|
|
530
|
+
|
|
531
|
+
const summary = {
|
|
532
|
+
ts: nowIso(),
|
|
533
|
+
label: 'run.summary',
|
|
534
|
+
durationSeconds: DURATION_SECONDS,
|
|
535
|
+
parallelism: PARALLELISM,
|
|
536
|
+
completedSearches: state.completed,
|
|
537
|
+
failedSearches: state.failed,
|
|
538
|
+
indexRequests: state.indexRequests,
|
|
539
|
+
avgLatencyMs,
|
|
540
|
+
maxLatencyMs: state.maxLatencyMs,
|
|
541
|
+
lastError: state.lastError,
|
|
542
|
+
stopReason: state.stopReason,
|
|
543
|
+
workersSettled: workerSettled,
|
|
544
|
+
inFlightAtEnd: state.inFlight,
|
|
545
|
+
maxObservedInFlight: state.maxObservedInFlight,
|
|
546
|
+
guardTriggered: state.guardTriggered,
|
|
547
|
+
guardKind: state.guardKind,
|
|
548
|
+
guardTriggeredAt: state.guardTriggeredAt,
|
|
549
|
+
guardObservedRssMB: state.guardObservedRssMB,
|
|
550
|
+
guardCancelError: state.guardCancelError,
|
|
551
|
+
guardShutdownError: state.guardShutdownError,
|
|
552
|
+
guardKillError: state.guardKillError,
|
|
553
|
+
errorBuckets: topErrorBuckets(state, 10),
|
|
554
|
+
memory: summarizeSamples(samples),
|
|
555
|
+
outputFile,
|
|
556
|
+
};
|
|
557
|
+
await writeRow(summary);
|
|
558
|
+
console.log(JSON.stringify(summary));
|
|
559
|
+
|
|
560
|
+
await client.disconnect().catch(() => {});
|