mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-04 06:34:06 +00:00
136 lines
4.1 KiB
TypeScript
136 lines
4.1 KiB
TypeScript
/**
|
|
* Shared probe primitives for plugin-load profiling.
|
|
*
|
|
* All plugin-load probes — across `src/plugins/loader.ts` and
|
|
* `src/plugin-sdk/channel-entry-contract.ts`
|
|
* — emit a single line per measurement to stderr in the form:
|
|
*
|
|
* [plugin-load-profile] phase=<X> plugin=<Y> elapsedMs=<N> [extras…] source=<S>
|
|
*
|
|
* The same `OPENCLAW_PLUGIN_LOAD_PROFILE=1` env flag activates all probes.
|
|
*
|
|
* Tooling that scrapes these lines (e.g. PERF-STARTUP-PLAN.md profiling
|
|
* methodology) depends on the field order being:
|
|
*
|
|
* 1. `phase=`
|
|
* 2. `plugin=`
|
|
* 3. `elapsedMs=`
|
|
* 4. any caller-supplied extras (in declaration order)
|
|
* 5. `source=` last
|
|
*
|
|
* Keep this contract stable — downstream parsers rely on it.
|
|
*/
|
|
|
|
export function shouldProfilePluginLoader(): boolean {
|
|
return process.env.OPENCLAW_PLUGIN_LOAD_PROFILE === "1";
|
|
}
|
|
|
|
/**
|
|
* An ordered list of `[key, value]` pairs appended between `elapsedMs=` and
|
|
* `source=` on the emitted log line. Ordered tuples (not a record) so that
|
|
* scrapers see a deterministic field order regardless of object iteration
|
|
* quirks.
|
|
*/
|
|
export type PluginLoadProfileExtras = ReadonlyArray<readonly [string, number | string]>;
|
|
|
|
/** Per-call scope: which plugin and which source path the probe is for. */
|
|
export type PluginLoadProfileScope = {
|
|
pluginId?: string;
|
|
source: string;
|
|
};
|
|
|
|
/**
|
|
* A scope-bound profiler — call it with a `phase` + sync `run` to time and
|
|
* emit a `[plugin-load-profile]` line that already includes the bound
|
|
* `pluginId` and `source`. Build one with `createProfiler(scope)`.
|
|
*/
|
|
export type PluginLoadProfiler = <T>(
|
|
phase: string,
|
|
run: () => T,
|
|
extras?: PluginLoadProfileExtras,
|
|
) => T;
|
|
|
|
/**
|
|
* Render a `[plugin-load-profile]` line. Exported so that callers needing
|
|
* custom timing splits (e.g. dual-timer probes in
|
|
* `channel-entry-contract.ts`) can build their own start/stop logic and
|
|
* still emit a line in the canonical format.
|
|
*/
|
|
export function formatPluginLoadProfileLine(params: {
|
|
phase: string;
|
|
pluginId?: string;
|
|
source: string;
|
|
elapsedMs: number;
|
|
extras?: PluginLoadProfileExtras;
|
|
}): string {
|
|
const extras = (params.extras ?? [])
|
|
.map(([k, v]) => `${k}=${typeof v === "number" ? v.toFixed(1) : v}`)
|
|
.join(" ");
|
|
const extrasFragment = extras ? ` ${extras}` : "";
|
|
return (
|
|
`[plugin-load-profile] phase=${params.phase} plugin=${params.pluginId ?? "(core)"}` +
|
|
` elapsedMs=${params.elapsedMs.toFixed(1)}${extrasFragment} source=${params.source}`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Time a single synchronous step and emit a `[plugin-load-profile]` line.
|
|
* Use this when you only need to wrap one call:
|
|
*
|
|
* ```ts
|
|
* const mod = withProfile(
|
|
* { pluginId: id, source },
|
|
* "phase-name",
|
|
* () => loadIt(),
|
|
* );
|
|
* ```
|
|
*
|
|
* For repeated calls that share the same `{ pluginId, source }` scope,
|
|
* prefer `createProfiler(scope)` and call the returned profiler.
|
|
*
|
|
* When the env flag is unset, this runs `run()` directly with no timing
|
|
* overhead. Errors propagate naturally; the log line is still emitted via
|
|
* `try { … } finally { … }`.
|
|
*/
|
|
export function withProfile<T>(
|
|
scope: PluginLoadProfileScope,
|
|
phase: string,
|
|
run: () => T,
|
|
extras?: PluginLoadProfileExtras,
|
|
): T {
|
|
if (!shouldProfilePluginLoader()) {
|
|
return run();
|
|
}
|
|
const startMs = performance.now();
|
|
try {
|
|
return run();
|
|
} finally {
|
|
const elapsedMs = performance.now() - startMs;
|
|
console.error(
|
|
formatPluginLoadProfileLine({
|
|
phase,
|
|
pluginId: scope.pluginId,
|
|
source: scope.source,
|
|
elapsedMs,
|
|
extras,
|
|
}),
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Build a scope-bound profiler. Useful when several consecutive steps share
|
|
* the same `{ pluginId, source }`:
|
|
*
|
|
* ```ts
|
|
* const profile = createProfiler({ pluginId: id, source: importMetaUrl });
|
|
* profile("phase-a", () => stepA());
|
|
* const v = profile("phase-b", () => stepB());
|
|
* ```
|
|
*
|
|
* Each call has the same semantics as `withProfile(scope, phase, run)`.
|
|
*/
|
|
export function createProfiler(scope: PluginLoadProfileScope): PluginLoadProfiler {
|
|
return (phase, run, extras) => withProfile(scope, phase, run, extras);
|
|
}
|