-
Notifications
You must be signed in to change notification settings - Fork 19
fix: measure memory while containers are running, not after teardown #1765
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -11,7 +11,7 @@ | |||||||
| * Outputs structured JSON with mean, median, p95, p99 per metric. | ||||||||
| */ | ||||||||
|
|
||||||||
| import { execSync, ExecSyncOptions } from "child_process"; | ||||||||
| import { execSync, ExecSyncOptions, spawn, ChildProcess } from "child_process"; | ||||||||
|
|
||||||||
| // ── Configuration ────────────────────────────────────────────────── | ||||||||
|
|
||||||||
|
|
@@ -159,45 +159,130 @@ function benchmarkHttpsLatency(): BenchmarkResult { | |||||||
| return { metric: "squid_https_latency", unit: "ms", values, ...stats(values) }; | ||||||||
| } | ||||||||
|
|
||||||||
| function benchmarkMemory(): BenchmarkResult { | ||||||||
| /** | ||||||||
| * Wait for Docker containers to be running, polling at 500ms intervals. | ||||||||
| * Uses exact name matching (anchored regex) to avoid false positives from | ||||||||
| * containers with similar names (e.g., "awf-squid-old"). | ||||||||
| * Throws if containers are not running within timeoutMs. | ||||||||
| */ | ||||||||
| function waitForContainers(containerNames: string[], timeoutMs: number): Promise<void> { | ||||||||
| const start = Date.now(); | ||||||||
| return new Promise((resolve, reject) => { | ||||||||
| const poll = (): void => { | ||||||||
| if (Date.now() - start > timeoutMs) { | ||||||||
| reject(new Error(`Containers not running after ${timeoutMs}ms`)); | ||||||||
| return; | ||||||||
| } | ||||||||
| try { | ||||||||
| const allRunning = containerNames.every((name) => { | ||||||||
| const result = execSync( | ||||||||
| `sudo docker ps --filter name=^${name}$ --filter status=running --format '{{.Names}}' 2>/dev/null`, | ||||||||
| { encoding: "utf-8", timeout: 5_000 } | ||||||||
| ) | ||||||||
| .trim() | ||||||||
| .split("\n") | ||||||||
| .map((n) => n.trim()) | ||||||||
| .filter(Boolean); | ||||||||
| return result.some((n) => n === name); | ||||||||
| }); | ||||||||
| if (allRunning) { | ||||||||
| resolve(); | ||||||||
| return; | ||||||||
| } | ||||||||
| } catch { | ||||||||
| // container not ready yet | ||||||||
| } | ||||||||
| setTimeout(poll, 500); | ||||||||
| }; | ||||||||
| poll(); | ||||||||
| }); | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Parse a Docker memory usage string like "123.4MiB / 7.773GiB" into MB. | ||||||||
| */ | ||||||||
| function parseMb(s: string): number { | ||||||||
| const match = s.match(/([\d.]+)\s*(MiB|GiB|KiB)/i); | ||||||||
| if (!match) return 0; | ||||||||
| const val = parseFloat(match[1]); | ||||||||
| const unit = match[2].toLowerCase(); | ||||||||
| if (unit === "gib") return val * 1024; | ||||||||
| if (unit === "kib") return val / 1024; | ||||||||
| return val; | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Kill a spawned background process and its entire process group, best-effort. | ||||||||
| * Sends SIGTERM then SIGKILL to the process group so descendant processes | ||||||||
| * (e.g., sudo, awf, docker) don't survive. | ||||||||
| */ | ||||||||
| function killBackground(child: ChildProcess): void { | ||||||||
| const pid = child.pid; | ||||||||
| if (!pid) return; | ||||||||
|
|
||||||||
| try { | ||||||||
| // SIGTERM the process group to allow graceful shutdown | ||||||||
| process.kill(-pid, "SIGTERM"); | ||||||||
| } catch { | ||||||||
| // Process group may have already exited | ||||||||
| } | ||||||||
|
|
||||||||
| try { | ||||||||
| // SIGKILL the entire process group to ensure nothing survives | ||||||||
| process.kill(-pid, "SIGKILL"); | ||||||||
| } catch { | ||||||||
| // Process group may have already exited | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
| async function benchmarkMemory(): Promise<BenchmarkResult> { | ||||||||
| console.error(" Benchmarking memory footprint..."); | ||||||||
| const values: number[] = []; | ||||||||
|
|
||||||||
| for (let i = 0; i < ITERATIONS; i++) { | ||||||||
| cleanup(); | ||||||||
| // Start containers, measure memory, then stop | ||||||||
| let child: ChildProcess | null = null; | ||||||||
| try { | ||||||||
| // Run a sleep command so containers stay up, then check memory | ||||||||
| const output = exec( | ||||||||
| `${AWF_CMD} --allow-domains ${ALLOWED_DOMAIN} --log-level error --keep-containers -- ` + | ||||||||
| `echo measuring_memory` | ||||||||
| // Start awf with a long-running command in the background so containers stay alive. | ||||||||
| // Derive spawn args from AWF_CMD to stay consistent with the rest of the script. | ||||||||
| const awfParts = AWF_CMD.split(/\s+/); | ||||||||
| child = spawn( | ||||||||
| awfParts[0], | ||||||||
| [...awfParts.slice(1), "--allow-domains", ALLOWED_DOMAIN, "--log-level", "error", "--", "sleep", "30"], | ||||||||
| { | ||||||||
|
Comment on lines
+249
to
+252
|
||||||||
| detached: true, | ||||||||
| stdio: "ignore", | ||||||||
| } | ||||||||
| ); | ||||||||
|
||||||||
| ); | |
| ); | |
| child.unref(); |
Copilot
AI
Apr 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The benchmark proceeds once containers are merely status=running. Since Squid has a Docker healthcheck (and the agent depends on it), sampling memory before Squid is healthy can make the metric noisy and can mask startup failures/restarts. Consider waiting for Squid to be healthy (e.g., via docker ps --filter health=healthy or docker inspect health status) before taking the memory sample.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
killBackground()sends SIGTERM to the process group but then only SIGKILLs the direct child PID. If the group spawns additional processes that ignore SIGTERM, they can survive. Consider sending SIGKILL to the process group as well (optionally after a short grace period), instead of only killing the child process.