|
11 | 11 | * Outputs structured JSON with mean, median, p95, p99 per metric. |
12 | 12 | */ |
13 | 13 |
|
14 | | -import { execSync, ExecSyncOptions } from "child_process"; |
| 14 | +import { execSync, ExecSyncOptions, spawn, ChildProcess } from "child_process"; |
15 | 15 |
|
16 | 16 | // ── Configuration ────────────────────────────────────────────────── |
17 | 17 |
|
@@ -159,45 +159,115 @@ function benchmarkHttpsLatency(): BenchmarkResult { |
159 | 159 | return { metric: "squid_https_latency", unit: "ms", values, ...stats(values) }; |
160 | 160 | } |
161 | 161 |
|
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> { |
163 | 228 | console.error(" Benchmarking memory footprint..."); |
164 | 229 | const values: number[] = []; |
165 | 230 |
|
166 | 231 | for (let i = 0; i < ITERATIONS; i++) { |
167 | 232 | cleanup(); |
168 | | - // Start containers, measure memory, then stop |
| 233 | + let child: ChildProcess | null = null; |
169 | 234 | 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 | + } |
174 | 243 | ); |
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 |
176 | 252 | const squidMem = exec( |
177 | 253 | "sudo docker stats awf-squid --no-stream --format '{{.MemUsage}}' 2>/dev/null || echo '0MiB'" |
178 | 254 | ); |
179 | 255 | const agentMem = exec( |
180 | 256 | "sudo docker stats awf-agent --no-stream --format '{{.MemUsage}}' 2>/dev/null || echo '0MiB'" |
181 | 257 | ); |
182 | 258 |
|
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 | | - |
194 | 259 | const totalMb = Math.round(parseMb(squidMem) + parseMb(agentMem)); |
195 | 260 | values.push(totalMb); |
196 | 261 | 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(); |
199 | 270 | } |
200 | | - cleanup(); |
201 | 271 | } |
202 | 272 |
|
203 | 273 | if (values.length === 0) { |
@@ -248,7 +318,7 @@ async function main(): Promise<void> { |
248 | 318 | results.push(benchmarkWarmStart()); |
249 | 319 | results.push(benchmarkColdStart()); |
250 | 320 | results.push(benchmarkHttpsLatency()); |
251 | | - results.push(benchmarkMemory()); |
| 321 | + results.push(await benchmarkMemory()); |
252 | 322 |
|
253 | 323 | // Final cleanup |
254 | 324 | cleanup(); |
|
0 commit comments