feat: report openclaw lifecycle timings

This commit is contained in:
Vincent Koc 2026-04-29 19:38:38 -07:00
parent cc89d7cea7
commit eb251cbae4
No known key found for this signature in database
5 changed files with 51 additions and 3 deletions

View File

@ -5,6 +5,7 @@
### Changed
- Report import-loop RSS and CPU as baseline-adjusted plugin deltas alongside raw subprocess metrics so Crabpot dashboards do not treat harness import cost as plugin runtime cost.
- Include optional OpenClaw loader lifecycle timings for import and full activation when a capture runner provides them.
## 0.3.5 - 2026-04-29

View File

@ -56,6 +56,9 @@ export async function buildCiSummary(options = {}) {
loaderJitiCandidates: reports.platform?.summary?.jitiAlternativeCount ?? 0,
importLoopP50Ms: reports.importLoop?.summary?.p50WallMs ?? 0,
importLoopP95Ms: reports.importLoop?.summary?.p95WallMs ?? 0,
importLoopOpenClawLifecycleCount: reports.importLoop?.summary?.openClawLifecycleCount ?? 0,
importLoopOpenClawImportP50Ms: reports.importLoop?.summary?.p50OpenClawImportMs ?? 0,
importLoopOpenClawActivationP50Ms: reports.importLoop?.summary?.p50OpenClawActivationMs ?? 0,
importLoopMetricBasis: reports.importLoop?.summary?.maxPluginPeakRssDeltaMb === undefined ? "raw" : "baseline-adjusted",
importLoopMaxRssMb: reports.importLoop?.summary?.maxPluginPeakRssDeltaMb ?? reports.importLoop?.summary?.maxPeakRssMb ?? 0,
importLoopMaxCpuMs: reports.importLoop?.summary?.maxPluginCpuDeltaMsEstimate ?? reports.importLoop?.summary?.maxCpuMsEstimate ?? 0,
@ -252,7 +255,11 @@ function inferSampleCount(samples = [], kind) {
function importLoopSummaryLabel(summary) {
const metricLabel = summary.importLoopMetricBasis === "baseline-adjusted" ? "plugin delta" : "raw";
return `p50 ${summary.importLoopP50Ms} ms / p95 ${summary.importLoopP95Ms} ms / ${metricLabel} RSS ${formatSampledMetric(summary.importLoopMaxRssMb, summary.importLoopRssSampleCount)} / ${metricLabel} CPU ${formatSampledMetric(summary.importLoopMaxCpuMs, summary.importLoopCpuSampleCount, "ms")}`;
const lifecycle =
summary.importLoopOpenClawLifecycleCount > 0
? ` / OpenClaw import ${summary.importLoopOpenClawImportP50Ms} ms / activate ${summary.importLoopOpenClawActivationP50Ms} ms`
: "";
return `p50 ${summary.importLoopP50Ms} ms / p95 ${summary.importLoopP95Ms} ms / ${metricLabel} RSS ${formatSampledMetric(summary.importLoopMaxRssMb, summary.importLoopRssSampleCount)} / ${metricLabel} CPU ${formatSampledMetric(summary.importLoopMaxCpuMs, summary.importLoopCpuSampleCount, "ms")}${lifecycle}`;
}
function formatSampledMetric(value, count, unit = "MB") {

View File

@ -33,6 +33,8 @@ export async function buildImportLoopProfile(options = {}) {
const wallMs = samples.map((sample) => sample.wallMs).sort((left, right) => left - right);
const pluginWallDeltaMs = samples.map((sample) => sample.pluginWallDeltaMs).sort((left, right) => left - right);
const openClawImportMs = openClawLifecycleMetric(samples, "importMs");
const openClawActivationMs = openClawLifecycleMetric(samples, "activationMs");
const rssSampleCount = samples.reduce((sum, sample) => sum + (sample.rssSampleCount ?? (sample.peakRssMb > 0 ? 1 : 0)), 0);
const cpuSampleCount = samples.reduce((sum, sample) => sum + (sample.cpuSampleCount ?? 0), 0);
const statSampleCount = samples.reduce((sum, sample) => sum + (sample.statSampleCount ?? 0), 0);
@ -49,6 +51,11 @@ export async function buildImportLoopProfile(options = {}) {
p95WallMs: percentile(wallMs, 0.95),
p50PluginWallDeltaMs: percentile(pluginWallDeltaMs, 0.5),
p95PluginWallDeltaMs: percentile(pluginWallDeltaMs, 0.95),
openClawLifecycleCount: openClawImportMs.length,
p50OpenClawImportMs: percentile(openClawImportMs, 0.5),
p95OpenClawImportMs: percentile(openClawImportMs, 0.95),
p50OpenClawActivationMs: percentile(openClawActivationMs, 0.5),
p95OpenClawActivationMs: percentile(openClawActivationMs, 0.95),
maxPeakRssMb: Math.max(0, ...samples.map((sample) => sample.peakRssMb)),
maxCpuMsEstimate: Math.max(0, ...samples.map((sample) => sample.cpuMsEstimate)),
maxPluginPeakRssDeltaMb: Math.max(0, ...samples.map((sample) => sample.pluginPeakRssDeltaMb)),
@ -120,6 +127,8 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
sample.index,
sample.status,
sample.capturedCount,
formatOpenClawLifecycleMetric(sample.openClawLifecycle?.importMs),
formatOpenClawLifecycleMetric(sample.openClawLifecycle?.activationMs),
formatOptionalMetric(sample.pluginWallDeltaMs, "ms"),
formatSampledMetric(sample.pluginPeakRssDeltaMb, sample.rssSampleCount),
formatSampledMetric(sample.pluginCpuDeltaMsEstimate, sample.cpuSampleCount, "ms"),
@ -133,6 +142,8 @@ export function renderImportLoopProfileMarkdown(report, options = {}) {
"Run",
"Status",
"Captured",
"OpenClaw Import",
"OpenClaw Activate",
"Plugin Wall Delta",
"Plugin RSS Delta",
"Plugin CPU Delta",
@ -258,6 +269,7 @@ async function runCaptureSample(options) {
exitCode: profile.exitCode,
status: output?.status ?? "failed",
capturedCount: output?.captured?.length ?? 0,
openClawLifecycle: output?.openClawLifecycle ?? null,
wallMs: profile.wallMs,
peakRssMb: profile.peakRssMb,
peakCpuPercent: profile.peakCpuPercent,
@ -287,6 +299,15 @@ function summaryRows(report) {
],
]
: []),
...((report.summary.openClawLifecycleCount ?? 0) > 0
? [
["openClawLifecycleCount", report.summary.openClawLifecycleCount],
["p50OpenClawImportMs", `${report.summary.p50OpenClawImportMs} ms`],
["p95OpenClawImportMs", `${report.summary.p95OpenClawImportMs} ms`],
["p50OpenClawActivationMs", `${report.summary.p50OpenClawActivationMs} ms`],
["p95OpenClawActivationMs", `${report.summary.p95OpenClawActivationMs} ms`],
]
: []),
["maxPeakRssMb", formatSampledMetric(report.summary.maxPeakRssMb, report.summary.rssSampleCount)],
["maxCpuMsEstimate", formatSampledMetric(report.summary.maxCpuMsEstimate, report.summary.cpuSampleCount, "ms")],
...(Number.isFinite(report.summary.baselineReferenceWallMs)
@ -338,6 +359,17 @@ function formatOptionalMetric(value, unit) {
return `${value} ${unit}`;
}
function formatOpenClawLifecycleMetric(value) {
return Number.isFinite(value) ? `${value} ms` : "n/a";
}
function openClawLifecycleMetric(samples, field) {
return samples
.map((sample) => sample.openClawLifecycle?.[field])
.filter((value) => Number.isFinite(value))
.sort((left, right) => left - right);
}
function applyBaselineAdjustment(sample, baseline) {
return {
...sample,

View File

@ -99,6 +99,9 @@ test("ci summary rolls up compatibility, policy, ref diff, and profile findings"
maxCpuMsEstimate: 30,
maxPluginPeakRssDeltaMb: 8,
maxPluginCpuDeltaMsEstimate: 6,
openClawLifecycleCount: 2,
p50OpenClawImportMs: 12,
p50OpenClawActivationMs: 3,
rssSampleCount: 2,
cpuSampleCount: 2,
},
@ -114,9 +117,10 @@ test("ci summary rolls up compatibility, policy, ref diff, and profile findings"
assert.equal(summary.summary.importLoopP50Ms, 50);
assert.equal(summary.summary.importLoopMetricBasis, "baseline-adjusted");
assert.equal(summary.summary.importLoopMaxRssMb, 8);
assert.equal(summary.summary.importLoopOpenClawImportP50Ms, 12);
assert.match(renderCiSummaryMarkdown(summary), /Crabpot CI Summary/);
assert.match(renderCiSummaryMarkdown(summary), /Windows portability risks/);
assert.match(renderCiSummaryMarkdown(summary), /p50 50 ms \/ p95 75 ms \/ plugin delta RSS 8 MB \/ plugin delta CPU 6 ms/);
assert.match(renderCiSummaryMarkdown(summary), /p50 50 ms \/ p95 75 ms \/ plugin delta RSS 8 MB \/ plugin delta CPU 6 ms \/ OpenClaw import 12 ms \/ activate 3 ms/);
assert.match(renderCiSummaryMarkdown(summary), /\| P0 issues\s+\| 1\s+\|/);
});

View File

@ -57,7 +57,7 @@ test("import loop profile can use a custom capture script and opt-in env", async
"const [entrypoint,, outputPath] = process.argv.slice(2);",
"if (process.env.CUSTOM_IMPORT_LOOP !== '1') throw new Error('missing opt-in');",
"await mkdir(path.dirname(outputPath), { recursive: true });",
"await writeFile(outputPath, JSON.stringify({ status: 'captured', entrypoint, captured: [{ kind: 'hook', name: 'before_tool_call' }] }));",
"await writeFile(outputPath, JSON.stringify({ status: 'captured', entrypoint, captured: [{ kind: 'hook', name: 'before_tool_call' }], openClawLifecycle: { importMs: 12, activationMs: 3, importPhase: 'full', activationPhase: 'full:register' } }));",
].join("\n"),
"utf8",
);
@ -73,6 +73,10 @@ test("import loop profile can use a custom capture script and opt-in env", async
assert.equal(profile.summary.failCount, 0);
assert.equal(profile.summary.baselineRuns, 1);
assert.equal(profile.summary.capturedCount, 1);
assert.equal(profile.summary.openClawLifecycleCount, 1);
assert.equal(profile.summary.p50OpenClawImportMs, 12);
assert.equal(profile.summary.p50OpenClawActivationMs, 3);
assert.match(renderImportLoopProfileMarkdown(profile), /OpenClaw Import/);
});
test("import loop profile validation rejects failed or empty captures", () => {