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,172 @@
|
|
|
1
|
+
# Memory Diagnostics Scripts
|
|
2
|
+
|
|
3
|
+
This folder contains repeatable memory diagnostics for daemon + MCP search/indexing workloads.
|
|
4
|
+
|
|
5
|
+
Reference ADR: `adr/018-adr-memory-lifecycle-and-regression-guardrails.md`
|
|
6
|
+
|
|
7
|
+
## Scripts
|
|
8
|
+
|
|
9
|
+
### `profile.mjs`
|
|
10
|
+
|
|
11
|
+
Purpose:
|
|
12
|
+
|
|
13
|
+
- Repeated in-process memory profiling for chunking and indexing loops.
|
|
14
|
+
- Useful for post-GC drift checks while iterating on lifecycle fixes.
|
|
15
|
+
|
|
16
|
+
Run:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm run memory:profile
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Optional env:
|
|
23
|
+
|
|
24
|
+
- `VIBERAG_MEM_RUNS` (default `3`)
|
|
25
|
+
- `VIBERAG_MEM_ITERATIONS` (default `200`)
|
|
26
|
+
|
|
27
|
+
Notes:
|
|
28
|
+
|
|
29
|
+
- Uses `--expose-gc` via npm script.
|
|
30
|
+
- Prints JSON samples (`rssMB`, `heapUsedMB`, `externalMB`, `arrayBuffersMB`).
|
|
31
|
+
|
|
32
|
+
### `isolate-intent-growth.mjs`
|
|
33
|
+
|
|
34
|
+
Purpose:
|
|
35
|
+
|
|
36
|
+
- Reproduces memory behavior per search intent on a fresh daemon process.
|
|
37
|
+
- Kills daemon between scenarios to avoid cross-run contamination.
|
|
38
|
+
- Captures before/after RSS from both `ps` and daemon status.
|
|
39
|
+
|
|
40
|
+
Run:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm run memory:isolate
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Useful env:
|
|
47
|
+
|
|
48
|
+
- `VIBERAG_ISOLATE_QUERY`
|
|
49
|
+
- `VIBERAG_ISOLATE_INTENTS` (comma-separated, e.g. `exact_text,concept`)
|
|
50
|
+
- `VIBERAG_ISOLATE_K` (clamped to `1..100`)
|
|
51
|
+
- `VIBERAG_ISOLATE_EXPLAIN` (`1` or `0`)
|
|
52
|
+
- `VIBERAG_ISOLATE_REQUEST_TIMEOUT_MS`
|
|
53
|
+
- `VIBERAG_ISOLATE_POST_SAMPLE_DELAY_MS`
|
|
54
|
+
- `VIBERAG_ISOLATE_VMMAP_TRIGGER_MB`
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
VIBERAG_ISOLATE_QUERY='indexing memory lifecycle ownership contract cleanup' \
|
|
60
|
+
VIBERAG_ISOLATE_INTENTS='exact_text' \
|
|
61
|
+
VIBERAG_ISOLATE_K=100 \
|
|
62
|
+
npm run memory:isolate
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `stress-search-growth.mjs`
|
|
66
|
+
|
|
67
|
+
Purpose:
|
|
68
|
+
|
|
69
|
+
- High-concurrency search stress test with periodic memory sampling.
|
|
70
|
+
- Supports optional indexing churn and safety guards.
|
|
71
|
+
|
|
72
|
+
Run:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npm run memory:stress
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Safety defaults:
|
|
79
|
+
|
|
80
|
+
- Hard guard: `VIBERAG_STRESS_MAX_RSS_MB=10240` (10 GB)
|
|
81
|
+
- Soft guard: `VIBERAG_STRESS_SOFT_RSS_MB=8192` (80% of hard cap by default)
|
|
82
|
+
- On guard trigger, script cancels work and requests daemon shutdown.
|
|
83
|
+
|
|
84
|
+
Key env:
|
|
85
|
+
|
|
86
|
+
- `VIBERAG_STRESS_SECONDS`
|
|
87
|
+
- `VIBERAG_STRESS_PARALLELISM`
|
|
88
|
+
- `VIBERAG_STRESS_MAX_IN_FLIGHT`
|
|
89
|
+
- `VIBERAG_STRESS_K` (clamped to `1..100`)
|
|
90
|
+
- `VIBERAG_STRESS_MAX_RSS_MB`
|
|
91
|
+
- `VIBERAG_STRESS_SOFT_RSS_MB`
|
|
92
|
+
- `VIBERAG_STRESS_GUARD_KILL` (`1` default, `0` disables SIGTERM fallback)
|
|
93
|
+
- `VIBERAG_STRESS_INDEX_CHURN_SECONDS`
|
|
94
|
+
- `VIBERAG_STRESS_INDEX_FORCE`
|
|
95
|
+
|
|
96
|
+
### `watch-reindex-stress.mjs`
|
|
97
|
+
|
|
98
|
+
Purpose:
|
|
99
|
+
|
|
100
|
+
- Sustained watcher-driven reindex stress via parallel continuous file edits.
|
|
101
|
+
- Simulates real file-change churn with high write rates over many probe files.
|
|
102
|
+
- Tracks daemon RSS/external memory, watcher pending queue pressure, and index update cadence.
|
|
103
|
+
- Includes hard/soft RSS guards to avoid host OOM during investigation.
|
|
104
|
+
|
|
105
|
+
Run:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm run memory:watch-stress
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Key env:
|
|
112
|
+
|
|
113
|
+
- `VIBERAG_WATCH_STRESS_SECONDS` (default `60`)
|
|
114
|
+
- `VIBERAG_WATCH_STRESS_WRITERS` (default `48`)
|
|
115
|
+
- `VIBERAG_WATCH_STRESS_FILES` (default `96`)
|
|
116
|
+
- `VIBERAG_WATCH_STRESS_WRITE_INTERVAL_MS` (default `0`)
|
|
117
|
+
- `VIBERAG_WATCH_STRESS_CONTENT_KB` (default `2`)
|
|
118
|
+
- `VIBERAG_WATCH_STRESS_SAMPLE_MS` (default `500`)
|
|
119
|
+
- `VIBERAG_WATCH_STRESS_MAX_RSS_MB` (default `10240`)
|
|
120
|
+
- `VIBERAG_WATCH_STRESS_SOFT_RSS_MB` (default `80%` of max)
|
|
121
|
+
- `VIBERAG_WATCH_STRESS_CLEAN_START` (`1` by default, kills existing daemon first)
|
|
122
|
+
- `VIBERAG_WATCH_STRESS_TARGET_DIR` (default `source/daemon/__watch_stress_probe`)
|
|
123
|
+
|
|
124
|
+
Example:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
VIBERAG_WATCH_STRESS_SECONDS=60 \
|
|
128
|
+
VIBERAG_WATCH_STRESS_WRITERS=64 \
|
|
129
|
+
VIBERAG_WATCH_STRESS_FILES=128 \
|
|
130
|
+
VIBERAG_WATCH_STRESS_MAX_RSS_MB=10240 \
|
|
131
|
+
npm run memory:watch-stress
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Testing Daemon Memory Alerts
|
|
135
|
+
|
|
136
|
+
You can force local alerting by lowering daemon monitor thresholds via env vars.
|
|
137
|
+
|
|
138
|
+
Supported daemon monitor env vars:
|
|
139
|
+
|
|
140
|
+
- `VIBERAG_MEMORY_MONITOR_POLL_INTERVAL_MS` (default `3000`)
|
|
141
|
+
- `VIBERAG_MEMORY_MONITOR_THRESHOLD_MB` (default `4096`)
|
|
142
|
+
- `VIBERAG_MEMORY_MONITOR_RECOVERY_MB` (default `3584`)
|
|
143
|
+
- `VIBERAG_MEMORY_MONITOR_GROWTH_THRESHOLD_MB` (default `1024`)
|
|
144
|
+
- `VIBERAG_MEMORY_MONITOR_GROWTH_WINDOW_MS` (default `10000`)
|
|
145
|
+
- `VIBERAG_MEMORY_MONITOR_MIN_REPORT_INTERVAL_MS` (default `10000`)
|
|
146
|
+
- `VIBERAG_MEMORY_MONITOR_MAX_REPORTS_PER_DAY` (default `20`)
|
|
147
|
+
|
|
148
|
+
Example (force threshold alerts at normal RSS):
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
VIBERAG_MEMORY_MONITOR_THRESHOLD_MB=200 \
|
|
152
|
+
VIBERAG_MEMORY_MONITOR_RECOVERY_MB=150 \
|
|
153
|
+
VIBERAG_STRESS_SECONDS=10 \
|
|
154
|
+
VIBERAG_STRESS_PARALLELISM=1 \
|
|
155
|
+
VIBERAG_STRESS_MAX_IN_FLIGHT=1 \
|
|
156
|
+
npm run memory:stress
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Expected signal:
|
|
160
|
+
|
|
161
|
+
- Daemon log line like:
|
|
162
|
+
`Memory monitor triggered Sentry report (threshold) at rss=...MB`
|
|
163
|
+
|
|
164
|
+
## Output Files
|
|
165
|
+
|
|
166
|
+
Generated run logs are written to `scripts/memory/out/` as JSONL.
|
|
167
|
+
|
|
168
|
+
- `isolate-intent-growth-*.jsonl`
|
|
169
|
+
- `stress-search-growth-*.jsonl`
|
|
170
|
+
- `watch-reindex-stress-*.jsonl`
|
|
171
|
+
|
|
172
|
+
These generated files are intentionally gitignored. Keep `scripts/memory/out/.gitkeep` tracked so the folder exists by default.
|
|
@@ -0,0 +1,348 @@
|
|
|
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
|
+
const OUTPUT_DIR = path.join(REPO_ROOT, 'scripts/memory/out');
|
|
15
|
+
|
|
16
|
+
const QUERY =
|
|
17
|
+
process.env['VIBERAG_ISOLATE_QUERY'] ??
|
|
18
|
+
'indexing memory lifecycle ownership contract cleanup';
|
|
19
|
+
const K = Math.max(
|
|
20
|
+
1,
|
|
21
|
+
Math.min(100, Number(process.env['VIBERAG_ISOLATE_K'] ?? '100')),
|
|
22
|
+
);
|
|
23
|
+
const EXPLAIN = process.env['VIBERAG_ISOLATE_EXPLAIN'] !== '0';
|
|
24
|
+
const REQUEST_TIMEOUT_MS = Number(
|
|
25
|
+
process.env['VIBERAG_ISOLATE_REQUEST_TIMEOUT_MS'] ?? '30000',
|
|
26
|
+
);
|
|
27
|
+
const POST_SAMPLE_DELAY_MS = Number(
|
|
28
|
+
process.env['VIBERAG_ISOLATE_POST_SAMPLE_DELAY_MS'] ?? '1500',
|
|
29
|
+
);
|
|
30
|
+
const VMAP_TRIGGER_MB = Number(
|
|
31
|
+
process.env['VIBERAG_ISOLATE_VMMAP_TRIGGER_MB'] ?? '1500',
|
|
32
|
+
);
|
|
33
|
+
const INTENTS = (
|
|
34
|
+
process.env['VIBERAG_ISOLATE_INTENTS'] ??
|
|
35
|
+
'definition,concept,usage,exact_text,similar_code,auto'
|
|
36
|
+
)
|
|
37
|
+
.split(',')
|
|
38
|
+
.map(intent => intent.trim())
|
|
39
|
+
.filter(Boolean);
|
|
40
|
+
|
|
41
|
+
if (INTENTS.length === 0) {
|
|
42
|
+
throw new Error('VIBERAG_ISOLATE_INTENTS produced an empty intent list.');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function sleep(ms) {
|
|
46
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function nowIso() {
|
|
50
|
+
return new Date().toISOString();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function compactError(error) {
|
|
54
|
+
if (error instanceof Error) return `${error.name}: ${error.message}`;
|
|
55
|
+
return String(error);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function mbFromKb(kb) {
|
|
59
|
+
return Number((kb / 1024).toFixed(1));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
63
|
+
const timeout = new Promise((_, reject) => {
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
});
|
|
68
|
+
return Promise.race([promise, timeout]);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function listDaemonRows() {
|
|
72
|
+
const {stdout} = await execFile('ps', [
|
|
73
|
+
'-axo',
|
|
74
|
+
'pid=,rss=,vsz=,%cpu=,%mem=,state=,etime=,command=',
|
|
75
|
+
]);
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (const line of stdout.split('\n')) {
|
|
78
|
+
if (!line.includes(DAEMON_ENTRY)) continue;
|
|
79
|
+
const match = line.match(
|
|
80
|
+
/^\s*(\d+)\s+(\d+)\s+(\d+)\s+([0-9.]+)\s+([0-9.]+)\s+(\S+)\s+(\S+)\s+(.+)$/,
|
|
81
|
+
);
|
|
82
|
+
if (!match) continue;
|
|
83
|
+
rows.push({
|
|
84
|
+
pid: Number(match[1]),
|
|
85
|
+
rssMB: mbFromKb(Number(match[2])),
|
|
86
|
+
vszMB: mbFromKb(Number(match[3])),
|
|
87
|
+
cpuPercent: Number(match[4]),
|
|
88
|
+
memPercent: Number(match[5]),
|
|
89
|
+
state: match[6],
|
|
90
|
+
etime: match[7],
|
|
91
|
+
command: match[8],
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return rows;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function killDaemonProcesses() {
|
|
98
|
+
const initial = await listDaemonRows();
|
|
99
|
+
if (initial.length === 0) {
|
|
100
|
+
await cleanupStaleRunArtifacts();
|
|
101
|
+
return {killed: [], zombieLike: []};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const killed = [];
|
|
105
|
+
for (const row of initial) {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(row.pid, 'SIGTERM');
|
|
108
|
+
killed.push({pid: row.pid, signal: 'SIGTERM'});
|
|
109
|
+
} catch {}
|
|
110
|
+
}
|
|
111
|
+
await sleep(500);
|
|
112
|
+
|
|
113
|
+
const afterTerm = await listDaemonRows();
|
|
114
|
+
for (const row of afterTerm) {
|
|
115
|
+
try {
|
|
116
|
+
process.kill(row.pid, 'SIGKILL');
|
|
117
|
+
killed.push({pid: row.pid, signal: 'SIGKILL'});
|
|
118
|
+
} catch {}
|
|
119
|
+
}
|
|
120
|
+
await sleep(500);
|
|
121
|
+
|
|
122
|
+
const stillThere = await listDaemonRows();
|
|
123
|
+
if (stillThere.length === 0) {
|
|
124
|
+
await cleanupStaleRunArtifacts();
|
|
125
|
+
}
|
|
126
|
+
return {killed, zombieLike: stillThere.map(row => row.pid)};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function cleanupStaleRunArtifacts() {
|
|
130
|
+
const {getDaemonLockPath, getDaemonPidPath, getDaemonSocketPath} =
|
|
131
|
+
await import(path.join(REPO_ROOT, 'dist/daemon/lib/constants.js'));
|
|
132
|
+
const stalePaths = [
|
|
133
|
+
getDaemonLockPath(REPO_ROOT),
|
|
134
|
+
getDaemonPidPath(REPO_ROOT),
|
|
135
|
+
getDaemonSocketPath(REPO_ROOT),
|
|
136
|
+
];
|
|
137
|
+
for (const stalePath of stalePaths) {
|
|
138
|
+
try {
|
|
139
|
+
await fs.rm(stalePath, {force: true, recursive: true});
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function summarizeResult(result) {
|
|
145
|
+
if (!result || typeof result !== 'object') {
|
|
146
|
+
return {bytes: 0, groups: null};
|
|
147
|
+
}
|
|
148
|
+
let bytes = 0;
|
|
149
|
+
try {
|
|
150
|
+
bytes = JSON.stringify(result).length;
|
|
151
|
+
} catch {}
|
|
152
|
+
|
|
153
|
+
const groups = result.groups ?? {};
|
|
154
|
+
return {
|
|
155
|
+
bytes,
|
|
156
|
+
groups: {
|
|
157
|
+
definitions: Array.isArray(groups.definitions)
|
|
158
|
+
? groups.definitions.length
|
|
159
|
+
: 0,
|
|
160
|
+
files: Array.isArray(groups.files) ? groups.files.length : 0,
|
|
161
|
+
blocks: Array.isArray(groups.blocks) ? groups.blocks.length : 0,
|
|
162
|
+
usages: Array.isArray(groups.usages) ? groups.usages.length : 0,
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function captureVmmapSummary(pid) {
|
|
168
|
+
try {
|
|
169
|
+
const {stdout} = await withTimeout(
|
|
170
|
+
execFile('vmmap', ['-summary', String(pid)]),
|
|
171
|
+
5000,
|
|
172
|
+
'vmmap',
|
|
173
|
+
);
|
|
174
|
+
const lines = stdout.split('\n');
|
|
175
|
+
const picked = lines.filter(
|
|
176
|
+
line =>
|
|
177
|
+
line.includes('Physical footprint') ||
|
|
178
|
+
line.includes('MALLOC') ||
|
|
179
|
+
line.includes('VM_ALLOCATE') ||
|
|
180
|
+
line.includes('MALLOC_NANO') ||
|
|
181
|
+
line.includes('TOTAL'),
|
|
182
|
+
);
|
|
183
|
+
return picked.slice(0, 120);
|
|
184
|
+
} catch (error) {
|
|
185
|
+
return [`vmmap_error: ${compactError(error)}`];
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const {DaemonClient} = await import(
|
|
190
|
+
path.join(REPO_ROOT, 'dist/client/index.js')
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
await fs.mkdir(OUTPUT_DIR, {recursive: true});
|
|
194
|
+
const runId = nowIso().replace(/[:.]/g, '-');
|
|
195
|
+
const outputFile = path.join(
|
|
196
|
+
OUTPUT_DIR,
|
|
197
|
+
`isolate-intent-growth-${runId}.jsonl`,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
async function writeRow(row) {
|
|
201
|
+
await fs.appendFile(outputFile, `${JSON.stringify(row)}\n`, 'utf8');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const header = {
|
|
205
|
+
ts: nowIso(),
|
|
206
|
+
label: 'run.start',
|
|
207
|
+
query: QUERY,
|
|
208
|
+
intents: INTENTS,
|
|
209
|
+
k: K,
|
|
210
|
+
explain: EXPLAIN,
|
|
211
|
+
requestTimeoutMs: REQUEST_TIMEOUT_MS,
|
|
212
|
+
postSampleDelayMs: POST_SAMPLE_DELAY_MS,
|
|
213
|
+
vmmapTriggerMB: VMAP_TRIGGER_MB,
|
|
214
|
+
outputFile,
|
|
215
|
+
};
|
|
216
|
+
console.log(JSON.stringify(header));
|
|
217
|
+
await writeRow(header);
|
|
218
|
+
|
|
219
|
+
const summary = [];
|
|
220
|
+
|
|
221
|
+
for (const intent of INTENTS) {
|
|
222
|
+
const scenarioStart = Date.now();
|
|
223
|
+
const cleanupBefore = await killDaemonProcesses();
|
|
224
|
+
const client = new DaemonClient({
|
|
225
|
+
projectRoot: REPO_ROOT,
|
|
226
|
+
autoStart: true,
|
|
227
|
+
connectTimeout: 30_000,
|
|
228
|
+
clientSource: 'mcp',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
let baselineStatus = null;
|
|
232
|
+
let baselinePs = null;
|
|
233
|
+
let searchOutcome = null;
|
|
234
|
+
let postStatus = null;
|
|
235
|
+
let postPs = null;
|
|
236
|
+
let postDelayStatus = null;
|
|
237
|
+
let vmmapSummary = null;
|
|
238
|
+
let error = null;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await withTimeout(client.connect(), 30_000, 'connect');
|
|
242
|
+
baselineStatus = await client.status();
|
|
243
|
+
baselinePs = (await listDaemonRows())[0] ?? null;
|
|
244
|
+
|
|
245
|
+
const searchStarted = Date.now();
|
|
246
|
+
try {
|
|
247
|
+
const result = await withTimeout(
|
|
248
|
+
client.search(QUERY, {intent, k: K, explain: EXPLAIN}),
|
|
249
|
+
REQUEST_TIMEOUT_MS,
|
|
250
|
+
`search:${intent}`,
|
|
251
|
+
);
|
|
252
|
+
searchOutcome = {
|
|
253
|
+
ok: true,
|
|
254
|
+
durationMs: Date.now() - searchStarted,
|
|
255
|
+
...summarizeResult(result),
|
|
256
|
+
};
|
|
257
|
+
} catch (searchError) {
|
|
258
|
+
searchOutcome = {
|
|
259
|
+
ok: false,
|
|
260
|
+
durationMs: Date.now() - searchStarted,
|
|
261
|
+
error: compactError(searchError),
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
postStatus = await client.status();
|
|
267
|
+
} catch (statusError) {
|
|
268
|
+
error = `post_status: ${compactError(statusError)}`;
|
|
269
|
+
}
|
|
270
|
+
postPs = (await listDaemonRows())[0] ?? null;
|
|
271
|
+
|
|
272
|
+
await sleep(POST_SAMPLE_DELAY_MS);
|
|
273
|
+
try {
|
|
274
|
+
postDelayStatus = await client.status();
|
|
275
|
+
} catch {}
|
|
276
|
+
|
|
277
|
+
const observedPostRss = Math.max(
|
|
278
|
+
postStatus?.memory?.rssMB ?? 0,
|
|
279
|
+
postPs?.rssMB ?? 0,
|
|
280
|
+
);
|
|
281
|
+
if (observedPostRss >= VMAP_TRIGGER_MB && postPs?.pid) {
|
|
282
|
+
vmmapSummary = await captureVmmapSummary(postPs.pid);
|
|
283
|
+
}
|
|
284
|
+
} catch (scenarioError) {
|
|
285
|
+
error = compactError(scenarioError);
|
|
286
|
+
} finally {
|
|
287
|
+
await client.disconnect().catch(() => {});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const cleanupAfter = await killDaemonProcesses();
|
|
291
|
+
const row = {
|
|
292
|
+
ts: nowIso(),
|
|
293
|
+
label: 'scenario.result',
|
|
294
|
+
intent,
|
|
295
|
+
durationMs: Date.now() - scenarioStart,
|
|
296
|
+
baseline: {
|
|
297
|
+
pid: baselinePs?.pid ?? null,
|
|
298
|
+
rssMBPs: baselinePs?.rssMB ?? null,
|
|
299
|
+
rssMBStatus: baselineStatus?.memory?.rssMB ?? null,
|
|
300
|
+
heapUsedMBStatus: baselineStatus?.memory?.heapUsedMB ?? null,
|
|
301
|
+
externalMBStatus: baselineStatus?.memory?.externalMB ?? null,
|
|
302
|
+
},
|
|
303
|
+
search: searchOutcome,
|
|
304
|
+
post: {
|
|
305
|
+
pid: postPs?.pid ?? null,
|
|
306
|
+
rssMBPs: postPs?.rssMB ?? null,
|
|
307
|
+
rssMBStatus: postStatus?.memory?.rssMB ?? null,
|
|
308
|
+
heapUsedMBStatus: postStatus?.memory?.heapUsedMB ?? null,
|
|
309
|
+
externalMBStatus: postStatus?.memory?.externalMB ?? null,
|
|
310
|
+
indexingStatus: postStatus?.indexing?.status ?? null,
|
|
311
|
+
},
|
|
312
|
+
postDelayed: {
|
|
313
|
+
rssMBStatus: postDelayStatus?.memory?.rssMB ?? null,
|
|
314
|
+
heapUsedMBStatus: postDelayStatus?.memory?.heapUsedMB ?? null,
|
|
315
|
+
externalMBStatus: postDelayStatus?.memory?.externalMB ?? null,
|
|
316
|
+
},
|
|
317
|
+
cleanupBefore,
|
|
318
|
+
cleanupAfter,
|
|
319
|
+
vmmapSummary,
|
|
320
|
+
error,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
summary.push(row);
|
|
324
|
+
console.log(JSON.stringify(row));
|
|
325
|
+
await writeRow(row);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const final = {
|
|
329
|
+
ts: nowIso(),
|
|
330
|
+
label: 'run.summary',
|
|
331
|
+
outputFile,
|
|
332
|
+
results: summary.map(row => {
|
|
333
|
+
const start =
|
|
334
|
+
Math.max(row.baseline.rssMBPs ?? 0, row.baseline.rssMBStatus ?? 0) || 0;
|
|
335
|
+
const end = Math.max(row.post.rssMBPs ?? 0, row.post.rssMBStatus ?? 0) || 0;
|
|
336
|
+
return {
|
|
337
|
+
intent: row.intent,
|
|
338
|
+
ok: row.search?.ok ?? false,
|
|
339
|
+
searchDurationMs: row.search?.durationMs ?? null,
|
|
340
|
+
searchError: row.search?.error ?? null,
|
|
341
|
+
startMB: start,
|
|
342
|
+
endMB: end,
|
|
343
|
+
growthMB: Number((end - start).toFixed(1)),
|
|
344
|
+
};
|
|
345
|
+
}),
|
|
346
|
+
};
|
|
347
|
+
console.log(JSON.stringify(final));
|
|
348
|
+
await writeRow(final);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {fileURLToPath} from 'node:url';
|
|
7
|
+
|
|
8
|
+
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const REPO_ROOT = path.resolve(SCRIPT_DIR, '..', '..');
|
|
10
|
+
const RUNS = Number(process.env['VIBERAG_MEM_RUNS'] ?? '3');
|
|
11
|
+
const ITERATIONS = Number(process.env['VIBERAG_MEM_ITERATIONS'] ?? '200');
|
|
12
|
+
|
|
13
|
+
const {Chunker} = await import(
|
|
14
|
+
path.join(REPO_ROOT, 'dist/daemon/lib/chunker/index.js')
|
|
15
|
+
);
|
|
16
|
+
const {IndexingServiceV2} = await import(
|
|
17
|
+
path.join(REPO_ROOT, 'dist/daemon/services/v2/indexing.js')
|
|
18
|
+
);
|
|
19
|
+
const {createConfigForProvider, saveConfig} = await import(
|
|
20
|
+
path.join(REPO_ROOT, 'dist/daemon/lib/config.js')
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
class DummyEmbeddingProvider {
|
|
24
|
+
constructor(dimensions) {
|
|
25
|
+
this.dimensions = dimensions;
|
|
26
|
+
this.initialized = false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async initialize() {
|
|
30
|
+
this.initialized = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async embed(texts) {
|
|
34
|
+
if (!this.initialized) {
|
|
35
|
+
await this.initialize();
|
|
36
|
+
}
|
|
37
|
+
return texts.map((text, index) => {
|
|
38
|
+
const seed = ((text.length + index * 7) % 97) / 100;
|
|
39
|
+
const out = new Array(this.dimensions);
|
|
40
|
+
for (let i = 0; i < out.length; i += 1) out[i] = seed;
|
|
41
|
+
return out;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async embedSingle(text) {
|
|
46
|
+
const [vector] = await this.embed([text]);
|
|
47
|
+
return vector;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
close() {
|
|
51
|
+
this.initialized = false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mb(value) {
|
|
56
|
+
return Number((value / (1024 * 1024)).toFixed(1));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function memory(label) {
|
|
60
|
+
const usage = process.memoryUsage();
|
|
61
|
+
const sample = {
|
|
62
|
+
label,
|
|
63
|
+
time: new Date().toISOString(),
|
|
64
|
+
rssMB: mb(usage.rss),
|
|
65
|
+
heapUsedMB: mb(usage.heapUsed),
|
|
66
|
+
externalMB: mb(usage.external),
|
|
67
|
+
arrayBuffersMB: mb(usage.arrayBuffers),
|
|
68
|
+
};
|
|
69
|
+
console.log(JSON.stringify(sample));
|
|
70
|
+
return sample;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function forceGc() {
|
|
74
|
+
if (typeof global.gc !== 'function') {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
global.gc();
|
|
78
|
+
global.gc();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function createSyntheticProject() {
|
|
82
|
+
const root = await fs.mkdtemp(
|
|
83
|
+
path.join(os.tmpdir(), 'viberag-memory-profile-'),
|
|
84
|
+
);
|
|
85
|
+
const projectRoot = path.join(root, 'project');
|
|
86
|
+
const homeRoot = path.join(root, 'home');
|
|
87
|
+
process.env['VIBERAG_HOME'] = homeRoot;
|
|
88
|
+
|
|
89
|
+
await fs.mkdir(projectRoot, {recursive: true});
|
|
90
|
+
await fs.mkdir(homeRoot, {recursive: true});
|
|
91
|
+
await fs.cp(
|
|
92
|
+
path.join(REPO_ROOT, 'source'),
|
|
93
|
+
path.join(projectRoot, 'source'),
|
|
94
|
+
{
|
|
95
|
+
recursive: true,
|
|
96
|
+
},
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const config = createConfigForProvider('gemini');
|
|
100
|
+
config.watch = {...config.watch, enabled: false};
|
|
101
|
+
config.extensions = ['.ts', '.tsx', '.js', '.mjs', '.cjs'];
|
|
102
|
+
await saveConfig(projectRoot, config);
|
|
103
|
+
|
|
104
|
+
return {projectRoot, homeRoot, dimensions: config.embeddingDimensions};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async function runChunkerScenario(projectRoot) {
|
|
108
|
+
console.log(JSON.stringify({section: 'chunker'}));
|
|
109
|
+
const filePath = path.join(projectRoot, 'source/daemon/lib/chunker/index.ts');
|
|
110
|
+
const source = await fs.readFile(filePath, 'utf8');
|
|
111
|
+
|
|
112
|
+
const chunker = new Chunker();
|
|
113
|
+
await chunker.initialize();
|
|
114
|
+
|
|
115
|
+
forceGc();
|
|
116
|
+
memory('chunker.before');
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < RUNS; i += 1) {
|
|
119
|
+
for (let j = 0; j < ITERATIONS; j += 1) {
|
|
120
|
+
chunker.analyzeFile(filePath, source, {
|
|
121
|
+
chunkMaxSize: 2000,
|
|
122
|
+
definitionMaxChunkSize: Number.MAX_SAFE_INTEGER,
|
|
123
|
+
refs: {
|
|
124
|
+
identifier_mode: 'symbolish',
|
|
125
|
+
max_occurrences_per_token: 0,
|
|
126
|
+
include_string_literals: false,
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
forceGc();
|
|
131
|
+
memory(`chunker.iter${i}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
chunker.close();
|
|
135
|
+
forceGc();
|
|
136
|
+
memory('chunker.afterClose');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runIndexScenario(projectRoot, dimensions) {
|
|
140
|
+
console.log(JSON.stringify({section: 'indexing'}));
|
|
141
|
+
for (let i = 0; i < RUNS; i += 1) {
|
|
142
|
+
forceGc();
|
|
143
|
+
memory(`index.before.${i}`);
|
|
144
|
+
|
|
145
|
+
const indexer = new IndexingServiceV2(projectRoot, {
|
|
146
|
+
embeddings: new DummyEmbeddingProvider(dimensions),
|
|
147
|
+
});
|
|
148
|
+
const started = Date.now();
|
|
149
|
+
const stats = await indexer.index({force: true});
|
|
150
|
+
memory(`index.after.${i}`);
|
|
151
|
+
indexer.close();
|
|
152
|
+
|
|
153
|
+
forceGc();
|
|
154
|
+
memory(`index.afterClose.${i}`);
|
|
155
|
+
console.log(
|
|
156
|
+
JSON.stringify({
|
|
157
|
+
label: `index.stats.${i}`,
|
|
158
|
+
durationMs: Date.now() - started,
|
|
159
|
+
filesIndexed: stats.filesIndexed,
|
|
160
|
+
symbolRowsUpserted: stats.symbolRowsUpserted,
|
|
161
|
+
chunkRowsUpserted: stats.chunkRowsUpserted,
|
|
162
|
+
embeddingsComputed: stats.embeddingsComputed,
|
|
163
|
+
}),
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (typeof global.gc !== 'function') {
|
|
169
|
+
console.error(
|
|
170
|
+
'[memory-profile] Run with --expose-gc for reliable post-GC comparisons.',
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const setup = await createSyntheticProject();
|
|
175
|
+
console.log(
|
|
176
|
+
JSON.stringify({
|
|
177
|
+
label: 'setup',
|
|
178
|
+
...setup,
|
|
179
|
+
runs: RUNS,
|
|
180
|
+
iterations: ITERATIONS,
|
|
181
|
+
}),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
await runChunkerScenario(setup.projectRoot);
|
|
185
|
+
await runIndexScenario(setup.projectRoot, setup.dimensions);
|
|
186
|
+
|
|
187
|
+
forceGc();
|
|
188
|
+
memory('done');
|