Skip to content

Commit dc69f41

Browse files
Mossakaclaude
andcommitted
fix: measure memory while containers are running, not after teardown
Previously benchmarkMemory() ran `echo measuring_memory` which completed instantly, so containers were already stopped before docker stats ran, always reporting 0 MB. Now spawns awf with `sleep 30` in the background, polls until containers are healthy, then samples docker stats while containers are alive. Closes #1758 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b8f9f7a commit dc69f41

1 file changed

Lines changed: 93 additions & 23 deletions

File tree

scripts/ci/benchmark-performance.ts

Lines changed: 93 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* Outputs structured JSON with mean, median, p95, p99 per metric.
1212
*/
1313

14-
import { execSync, ExecSyncOptions } from "child_process";
14+
import { execSync, ExecSyncOptions, spawn, ChildProcess } from "child_process";
1515

1616
// ── Configuration ──────────────────────────────────────────────────
1717

@@ -159,45 +159,115 @@ function benchmarkHttpsLatency(): BenchmarkResult {
159159
return { metric: "squid_https_latency", unit: "ms", values, ...stats(values) };
160160
}
161161

162-
function benchmarkMemory(): BenchmarkResult {
162+
/**
163+
* Wait for Docker containers to be running, polling at 500ms intervals.
164+
* Throws if containers are not running within timeoutMs.
165+
*/
166+
function waitForContainers(containerNames: string[], timeoutMs: number): Promise<void> {
167+
const start = Date.now();
168+
return new Promise((resolve, reject) => {
169+
const poll = (): void => {
170+
if (Date.now() - start > timeoutMs) {
171+
reject(new Error(`Containers not running after ${timeoutMs}ms`));
172+
return;
173+
}
174+
try {
175+
const allRunning = containerNames.every((name) => {
176+
const result = execSync(
177+
`sudo docker ps --filter name=${name} --filter status=running --format '{{.Names}}' 2>/dev/null`,
178+
{ encoding: "utf-8", timeout: 5_000 }
179+
).trim();
180+
return result.includes(name);
181+
});
182+
if (allRunning) {
183+
resolve();
184+
return;
185+
}
186+
} catch {
187+
// container not ready yet
188+
}
189+
setTimeout(poll, 500);
190+
};
191+
poll();
192+
});
193+
}
194+
195+
/**
196+
* Parse a Docker memory usage string like "123.4MiB / 7.773GiB" into MB.
197+
*/
198+
function parseMb(s: string): number {
199+
const match = s.match(/([\d.]+)\s*(MiB|GiB|KiB)/i);
200+
if (!match) return 0;
201+
const val = parseFloat(match[1]);
202+
const unit = match[2].toLowerCase();
203+
if (unit === "gib") return val * 1024;
204+
if (unit === "kib") return val / 1024;
205+
return val;
206+
}
207+
208+
/**
209+
* Kill a spawned background process and its process group, best-effort.
210+
*/
211+
function killBackground(child: ChildProcess): void {
212+
try {
213+
if (child.pid) {
214+
// Kill the process group (negative PID) to catch child processes
215+
process.kill(-child.pid, "SIGTERM");
216+
}
217+
} catch {
218+
// Process may have already exited
219+
}
220+
try {
221+
child.kill("SIGKILL");
222+
} catch {
223+
// best-effort
224+
}
225+
}
226+
227+
async function benchmarkMemory(): Promise<BenchmarkResult> {
163228
console.error(" Benchmarking memory footprint...");
164229
const values: number[] = [];
165230

166231
for (let i = 0; i < ITERATIONS; i++) {
167232
cleanup();
168-
// Start containers, measure memory, then stop
233+
let child: ChildProcess | null = null;
169234
try {
170-
// Run a sleep command so containers stay up, then check memory
171-
const output = exec(
172-
`${AWF_CMD} --allow-domains ${ALLOWED_DOMAIN} --log-level error --keep-containers -- ` +
173-
`echo measuring_memory`
235+
// Start awf with a long-running command in the background so containers stay alive
236+
child = spawn(
237+
"sudo",
238+
["awf", "--allow-domains", ALLOWED_DOMAIN, "--log-level", "error", "--", "sleep", "30"],
239+
{
240+
detached: true,
241+
stdio: "ignore",
242+
}
174243
);
175-
// Get memory stats for both containers
244+
245+
// Wait for both containers to be running (up to 30s)
246+
await waitForContainers(["awf-squid", "awf-agent"], 30_000);
247+
248+
// Give containers a moment to stabilize memory usage
249+
await new Promise((resolve) => setTimeout(resolve, 2000));
250+
251+
// Get memory stats while containers are alive
176252
const squidMem = exec(
177253
"sudo docker stats awf-squid --no-stream --format '{{.MemUsage}}' 2>/dev/null || echo '0MiB'"
178254
);
179255
const agentMem = exec(
180256
"sudo docker stats awf-agent --no-stream --format '{{.MemUsage}}' 2>/dev/null || echo '0MiB'"
181257
);
182258

183-
// Parse memory values (format: "123.4MiB / 7.773GiB")
184-
const parseMb = (s: string): number => {
185-
const match = s.match(/([\d.]+)\s*(MiB|GiB|KiB)/i);
186-
if (!match) return 0;
187-
const val = parseFloat(match[1]);
188-
const unit = match[2].toLowerCase();
189-
if (unit === "gib") return val * 1024;
190-
if (unit === "kib") return val / 1024;
191-
return val;
192-
};
193-
194259
const totalMb = Math.round(parseMb(squidMem) + parseMb(agentMem));
195260
values.push(totalMb);
196261
console.error(` Iteration ${i + 1}/${ITERATIONS}: ${totalMb}MB (squid: ${squidMem}, agent: ${agentMem})`);
197-
} catch {
198-
console.error(` Iteration ${i + 1}/${ITERATIONS}: failed (skipped)`);
262+
} catch (err) {
263+
console.error(` Iteration ${i + 1}/${ITERATIONS}: failed (skipped) - ${err}`);
264+
} finally {
265+
// Always clean up the background process and containers
266+
if (child) {
267+
killBackground(child);
268+
}
269+
cleanup();
199270
}
200-
cleanup();
201271
}
202272

203273
if (values.length === 0) {
@@ -248,7 +318,7 @@ async function main(): Promise<void> {
248318
results.push(benchmarkWarmStart());
249319
results.push(benchmarkColdStart());
250320
results.push(benchmarkHttpsLatency());
251-
results.push(benchmarkMemory());
321+
results.push(await benchmarkMemory());
252322

253323
// Final cleanup
254324
cleanup();

0 commit comments

Comments
 (0)