





















@@ -5,6 +5,7 @@ import { createServer } from "node:net";
55import { tmpdir } from "node:os";
66import path from "node:path";
77import { performance } from "node:perf_hooks";
8+import { pathToFileURL } from "node:url";
89910type GatewayBenchCase = {
1011config: Record<string, unknown>;
@@ -16,19 +17,31 @@ type GatewayBenchCase = {
1617};
17181819type ProbeResult = {
20+firstErrorKind: string | null;
21+firstRecoveryMs: number | null;
1922ms: number | null;
2023status: number | null;
24+transitions: ProbeTransition[];
25+};
26+27+type ProbeTransition = {
28+errorKind?: string;
29+ms: number;
30+status: number | null;
2131};
22322333type GatewaySample = {
2434cpuCoreRatio: number | null;
2535cpuMs: number | null;
2636exitCode: number | null;
2737firstOutputMs: number | null;
38+gatewayReadyLogLine: string | null;
39+gatewayReadyLogMs: number | null;
2840healthz: ProbeResult;
41+httpListenLogLine: string | null;
42+httpListenLogMs: number | null;
2943maxRssMb: number | null;
3044outputTail: string;
31-readyLogMs: number | null;
3245readyz: ProbeResult;
3346signal: string | null;
3447startupTrace: Record<string, number>;
@@ -50,14 +63,20 @@ type CaseResult = {
5063firstOutputMs: SummaryStats | null;
5164cpuCoreRatio: SummaryStats | null;
5265cpuMs: SummaryStats | null;
66+gatewayReadyLogMs: SummaryStats | null;
5367healthzMs: SummaryStats | null;
68+httpListenLogMs: SummaryStats | null;
5469maxRssMb: SummaryStats | null;
55-readyLogMs: SummaryStats | null;
5670readyzMs: SummaryStats | null;
5771startupTrace: Record<string, SummaryStats>;
5872};
5973};
607475+type PluginFixtureResult = {
76+pluginIds: string[];
77+pluginsDir: string;
78+};
79+6180type CliOptions = {
6281cases: GatewayBenchCase[];
6382cpuProfDir?: string;
@@ -135,6 +154,7 @@ const GATEWAY_CASES: readonly GatewayBenchCase[] = [
135154id: "fiftyPlugins",
136155name: "gateway, 50 manifest plugins",
137156env: { OPENCLAW_SKIP_CHANNELS: "1" },
157+pluginActivationOnStartup: true,
138158pluginCount: 50,
139159config: BASE_CONFIG,
140160},
@@ -302,19 +322,24 @@ function summarizeCase(benchCase: GatewayBenchCase, samples: GatewaySample[]): C
302322.map((sample) => sample.cpuMs)
303323.filter((value): value is number => typeof value === "number"),
304324),
325+gatewayReadyLogMs: summarizeNumbers(
326+samples
327+.map((sample) => sample.gatewayReadyLogMs)
328+.filter((value): value is number => typeof value === "number"),
329+),
305330healthzMs: summarizeNumbers(
306331samples
307332.map((sample) => sample.healthz.ms)
308333.filter((value): value is number => typeof value === "number"),
309334),
310-maxRssMb: summarizeNumbers(
335+httpListenLogMs: summarizeNumbers(
311336samples
312-.map((sample) => sample.maxRssMb)
337+.map((sample) => sample.httpListenLogMs)
313338.filter((value): value is number => typeof value === "number"),
314339),
315-readyLogMs: summarizeNumbers(
340+maxRssMb: summarizeNumbers(
316341samples
317-.map((sample) => sample.readyLogMs)
342+.map((sample) => sample.maxRssMb)
318343.filter((value): value is number => typeof value === "number"),
319344),
320345readyzMs: summarizeNumbers(
@@ -399,19 +424,86 @@ async function waitForProbe(params: {
399424port: number;
400425startAt: number;
401426}): Promise<ProbeResult> {
427+let firstErrorKind: string | null = null;
428+let firstRecoveryMs: number | null = null;
402429let lastStatus: number | null = null;
430+let lastStateKey: string | null = null;
431+let sawUnreadyState = false;
432+const transitions: ProbeTransition[] = [];
403433while (performance.now() < params.deadlineAt) {
404434if (params.isDone?.()) {
405435break;
406436}
407-const status = await requestStatus(params.port, params.path).catch(() => null);
408-lastStatus = status;
409-if (status === 200) {
410-return { ms: performance.now() - params.startAt, status };
437+const attempt = await requestProbeStatus(params.port, params.path);
438+const now = performance.now();
439+const elapsedMs = now - params.startAt;
440+lastStatus = attempt.status;
441+const stateKey = `${attempt.status ?? "none"}:${attempt.errorKind ?? "ok"}`;
442+if (stateKey !== lastStateKey) {
443+transitions.push({
444+ms: elapsedMs,
445+status: attempt.status,
446+ ...(attempt.errorKind ? { errorKind: attempt.errorKind } : {}),
447+});
448+lastStateKey = stateKey;
449+}
450+if (attempt.errorKind && firstErrorKind == null) {
451+firstErrorKind = attempt.errorKind;
452+}
453+if (attempt.status !== 200) {
454+sawUnreadyState = true;
455+}
456+if (attempt.status === 200) {
457+if (sawUnreadyState && firstRecoveryMs == null) {
458+firstRecoveryMs = elapsedMs;
459+}
460+return {
461+ firstErrorKind,
462+ firstRecoveryMs,
463+ms: elapsedMs,
464+status: attempt.status,
465+ transitions,
466+};
411467}
412468await delay(25);
413469}
414-return { ms: null, status: lastStatus };
470+return { firstErrorKind, firstRecoveryMs, ms: null, status: lastStatus, transitions };
471+}
472+473+async function requestProbeStatus(
474+port: number,
475+pathname: string,
476+): Promise<{ errorKind: string | null; status: number | null }> {
477+try {
478+const status = await requestStatus(port, pathname);
479+return {
480+errorKind: status === 200 ? null : `http-${status}`,
481+ status,
482+};
483+} catch (error) {
484+return {
485+errorKind: classifyProbeErrorKind(error),
486+status: null,
487+};
488+}
489+}
490+491+function classifyProbeErrorKind(error: unknown): string {
492+if (typeof error === "object" && error !== null) {
493+const code = (error as { code?: unknown }).code;
494+if (typeof code === "string" && code.trim()) {
495+return code.trim().toLowerCase();
496+}
497+const message = (error as { message?: unknown }).message;
498+if (typeof message === "string" && /probe timeout/iu.test(message)) {
499+return "timeout";
500+}
501+const name = (error as { name?: unknown }).name;
502+if (typeof name === "string" && name.trim()) {
503+return name.trim().toLowerCase();
504+}
505+}
506+return "error";
415507}
416508417509function requestStatus(port: number, pathname: string): Promise<number> {
@@ -435,12 +527,17 @@ function delay(ms: number): Promise<void> {
435527return new Promise((resolve) => setTimeout(resolve, ms));
436528}
437529438-function writePluginFixtures(root: string, count: number, activationOnStartup?: boolean): string[] {
439-const files: string[] = [];
530+function writePluginFixtures(
531+root: string,
532+count: number,
533+activationOnStartup?: boolean,
534+): PluginFixtureResult {
535+const pluginIds: string[] = [];
440536const pluginsDir = path.join(root, "plugins");
441537mkdirSync(pluginsDir, { recursive: true });
442538for (let index = 0; index < count; index += 1) {
443539const id = `bench-plugin-${String(index + 1).padStart(2, "0")}`;
540+pluginIds.push(id);
444541const pluginDir = path.join(pluginsDir, id);
445542mkdirSync(pluginDir, { recursive: true });
446543const entry = path.join(pluginDir, "index.cjs");
@@ -459,23 +556,22 @@ function writePluginFixtures(root: string, count: number, activationOnStartup?:
459556 2,
460557 )}\n`,
461558);
462-files.push(entry);
463559}
464-return files;
560+return { pluginIds, pluginsDir };
465561}
466562467563function writeConfig(root: string, benchCase: GatewayBenchCase): string {
468-const pluginPaths = benchCase.pluginCount
564+const pluginFixtures = benchCase.pluginCount
469565 ? writePluginFixtures(root, benchCase.pluginCount, benchCase.pluginActivationOnStartup)
470- : [];
566+ : null;
471567const config = {
472568 ...benchCase.config,
473569plugins: {
474570 ...(benchCase.config.plugins as Record<string, unknown> | undefined),
475- ...(pluginPaths.length > 0
571+ ...(pluginFixtures
476572 ? {
477-load: { paths: pluginPaths },
478-allow: pluginPaths.map((file) => path.basename(path.dirname(file))),
573+load: { paths: [pluginFixtures.pluginsDir] },
574+allow: pluginFixtures.pluginIds,
479575}
480576 : {}),
481577},
@@ -565,8 +661,14 @@ function collectStartupTrace(line: string, startupTrace: Record<string, number>)
565661}
566662}
567663568-function hasGatewayReadyLog(line: string): boolean {
569-return /\[gateway\] (?:http server listening|ready \()/.test(line);
664+function classifyGatewayReadyLog(line: string): "gateway-ready" | "http-listen" | null {
665+if (/\[gateway\] http server listening \(/.test(line)) {
666+return "http-listen";
667+}
668+if (/\[gateway\] ready(?:\s*\(|\s*$)/.test(line)) {
669+return "gateway-ready";
670+}
671+return null;
570672}
571673572674function parseStartupTraceMetrics(raw: string): Array<{ key: string; value: number }> {
@@ -688,8 +790,11 @@ async function runGatewaySample(options: {
688790const startupTrace: Record<string, number> = {};
689791const output: string[] = [];
690792let firstOutputMs: number | null = null;
793+let gatewayReadyLogLine: string | null = null;
794+let gatewayReadyLogMs: number | null = null;
795+let httpListenLogLine: string | null = null;
796+let httpListenLogMs: number | null = null;
691797let maxRssMb: number | null = null;
692-let readyLogMs: number | null = null;
693798let childExited = false;
694799695800const childArgs = [
@@ -749,8 +854,14 @@ async function runGatewaySample(options: {
749854output.splice(0, output.length - 20);
750855}
751856for (const line of text.split(/\r?\n/u)) {
752-if (hasGatewayReadyLog(line) && readyLogMs == null) {
753-readyLogMs = performance.now() - startAt;
857+const readyLogKind = classifyGatewayReadyLog(line);
858+if (readyLogKind === "http-listen" && httpListenLogMs == null) {
859+httpListenLogMs = performance.now() - startAt;
860+httpListenLogLine = line;
861+}
862+if (readyLogKind === "gateway-ready" && gatewayReadyLogMs == null) {
863+gatewayReadyLogMs = performance.now() - startAt;
864+gatewayReadyLogLine = line;
754865}
755866collectStartupTrace(line, startupTrace);
756867}
@@ -789,10 +900,13 @@ async function runGatewaySample(options: {
789900 cpuMs,
790901exitCode: exit.exitCode,
791902 firstOutputMs,
903+ gatewayReadyLogLine,
904+ gatewayReadyLogMs,
792905 healthz,
906+ httpListenLogLine,
907+ httpListenLogMs,
793908 maxRssMb,
794909outputTail: output.join("").split(/\r?\n/u).slice(-20).join("\n"),
795- readyLogMs,
796910 readyz,
797911signal: exit.signal,
798912 startupTrace,
@@ -821,7 +935,7 @@ async function runCase(options: {
821935samples.push(sample);
822936const heapUsedMb = sample.startupTrace["memory.ready.heapUsedMb"] ?? null;
823937console.log(
824-`[gateway-startup-bench] ${options.benchCase.id} run ${samples.length}/${options.runs}: healthz=${formatMs(sample.healthz.ms)} readyz=${formatMs(sample.readyz.ms)} readyLog=${formatMs(sample.readyLogMs)} cpu=${formatMs(sample.cpuMs)} cpuCore=${formatRatio(sample.cpuCoreRatio)} rss=${formatMb(sample.maxRssMb)} heap=${formatMb(heapUsedMb)}`,
938+`[gateway-startup-bench] ${options.benchCase.id} run ${samples.length}/${options.runs}: healthz=${formatMs(sample.healthz.ms)} readyz=${formatMs(sample.readyz.ms)} httpListen=${formatMs(sample.httpListenLogMs)} gatewayReady=${formatMs(sample.gatewayReadyLogMs)} cpu=${formatMs(sample.cpuMs)} cpuCore=${formatRatio(sample.cpuCoreRatio)} rss=${formatMb(sample.maxRssMb)} heap=${formatMb(heapUsedMb)}`,
825939);
826940} else {
827941const heapUsedMb = sample.startupTrace["memory.ready.heapUsedMb"] ?? null;
@@ -839,7 +953,8 @@ function printResult(result: CaseResult): void {
839953console.log(` CPU: ${formatStats(result.summary.cpuMs)}`);
840954console.log(` CPU core: ${formatRatioStats(result.summary.cpuCoreRatio)}`);
841955console.log(` /healthz: ${formatStats(result.summary.healthzMs)}`);
842-console.log(` ready log: ${formatStats(result.summary.readyLogMs)}`);
956+console.log(` http listen: ${formatStats(result.summary.httpListenLogMs)}`);
957+console.log(` gateway ready: ${formatStats(result.summary.gatewayReadyLogMs)}`);
843958console.log(` /readyz: ${formatStats(result.summary.readyzMs)}`);
844959console.log(` max RSS: ${formatMemoryStats(result.summary.maxRssMb)}`);
845960console.log(
@@ -902,7 +1017,18 @@ async function main() {
9021017}
9031018}
9041019905-main().catch((err) => {
906-console.error(err instanceof Error ? err.stack : String(err));
907-process.exitCode = 1;
908-});
1020+export const __testing = {
1021+ classifyGatewayReadyLog,
1022+ classifyProbeErrorKind,
1023+ collectStartupTrace,
1024+ summarizeCase,
1025+ waitForProbe,
1026+ writeConfig,
1027+};
1028+1029+if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
1030+main().catch((err) => {
1031+console.error(err instanceof Error ? err.stack : String(err));
1032+process.exitCode = 1;
1033+});
1034+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。