feat: report openclaw lifecycle timings
This commit is contained in:
parent
cc89d7cea7
commit
eb251cbae4
@ -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
|
||||
|
||||
|
||||
@ -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") {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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+\|/);
|
||||
});
|
||||
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user