mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:40:49 +00:00
perf(plugin-sdk): per-phase + per-jiti-call probes for bundled channel entries (#69537)
* perf(plugin-sdk): per-phase + per-jiti-call probes for bundled channel entries Extends the existing OPENCLAW_PLUGIN_LOAD_PROFILE infrastructure (see src/plugins/loader.ts `profilePluginLoaderSync` and src/plugins/source-loader.ts) with two new probe sites inside src/plugin-sdk/channel-entry-contract.ts: 1. `bundled-register:<phase>` — wraps each phase of `defineBundledChannelEntry`'s register() callback (`setChannelRuntime`, `loadChannelPlugin`, `registerChannel`, `registerCliMetadata`, `registerFull`). Lets us pinpoint which phase of plugin registration is responsible for cold-start cost on a per-plugin basis. 2. `bundled-entry-module-load` — instruments `loadBundledEntryModuleSync` and reports `getJitiMs` (jiti loader factory) vs `jitiCallMs` (actual graph walk + transpile + ESM linking) separately. Lets us distinguish alias-map / loader setup overhead from import-graph traversal cost on a per-module basis. Both probes are gated on OPENCLAW_PLUGIN_LOAD_PROFILE=1 and have zero overhead when the env flag is unset (early return before any `performance.now()` call). Log format matches the existing `[plugin-load-profile]` line shape so existing log scrapers continue to work. The helper is a file-local mirror of `profilePluginLoaderSync` rather than a new SDK export — keeps the SDK boundary narrow per src/plugin-sdk/AGENTS.md and avoids cross-importing host internals. Used to validate PR #69317 (slack startup perf) — measurements showed slack `setChannelRuntime` dropping from 13183ms to 67ms after barrel narrowing, which would have been undiagnosable without these per-phase probes. * perf(plugins): per-plugin register() probe in plugin loader Adds a `phase=${registrationMode}:register` probe wrapping each call to `runPluginRegisterSync(register, api)` in src/plugins/loader.ts. Emits the established `[plugin-load-profile]` line shape via `profilePluginLoaderSync`, gated on OPENCLAW_PLUGIN_LOAD_PROFILE=1. Two call sites are wrapped: - The main load path (registrationMode is dynamic: "snapshot", "validate", "full") at the post-snapshot register block. Emits e.g. `phase=full:register plugin=slack elapsedMs=14102.1 source=...` - The cli-metadata-only path (registrationMode hardcoded to "cli-metadata") for fast `--metadata` boot flows. Together with the existing `phase=full` (entire load) and `phase=source-loader` probes plus the `bundled-register:*` and `bundled-entry-module-load` probes added in the previous commit, this gives a full breakdown: - `phase=full plugin=slack` — total cost from import through register return - `phase=full:register plugin=slack` — just the register() callback (NEW) - `phase=bundled-register:setChannelRuntime plugin=slack` — sub-phase - `phase=bundled-register:loadChannelPlugin plugin=slack` — sub-phase - `phase=bundled-entry-module-load plugin=(bundled-entry)` — per-module load Lets you `sort -k4 -n -r` the log output to find the slowest plugin's register() call across all bundled+third-party plugins, then drill in via the sub-phase probes for bundled entries. * perf(plugins): consolidate plugin-load-profile primitives in shared module Extracts the previously duplicated `shouldProfilePluginLoader` / `profilePluginLoaderSync` helpers into a new `src/plugins/plugin-load-profile.ts` module. Removes 3 file-local copies of the same env-flag check and 2 near-duplicate `try { run() } finally { console.error(...) }` wrappers. Files updated: - NEW src/plugins/plugin-load-profile.ts — sole owner of: shouldProfilePluginLoader() profilePluginLoaderSync<T>({phase, pluginId?, source, run, extras?}) formatPluginLoadProfileLine({phase, pluginId?, source, elapsedMs, extras?}) - src/plugins/loader.ts — drop file-local copies, import shared helper (existing 4 + new 2 call sites unchanged in shape) - src/plugins/source-loader.ts — drop renamed local copy (`shouldProfilePluginSourceLoader`), use shared helper with `pluginId: "(direct)"` to preserve the existing `plugin=(direct)` field - src/plugin-sdk/channel-entry-contract.ts — drop file-local copies and inline `profileStep` closure; use shared `profilePluginLoaderSync` directly at all 5 `bundled-register:*` call sites; dual-timing `bundled-entry-module-load` probe uses `formatPluginLoadProfileLine` with ordered `extras` for `getJitiMs`/`jitiCallMs` Log line format is byte-for-byte identical to before (validated against 3 cases: standard, with pluginId, dual-timing). The `extras` API is intentionally an ordered tuple list (not a record) so that scrapers see deterministic field order between `elapsedMs=` and `source=`. Net: +155/-87 lines across 4 files, removing ~60 lines of duplication while exposing a stable, documented probe surface. Verified: - pnpm tsgo (core) — 0 errors - pnpm lint on all 4 files — 0 warnings, 0 errors - pnpm test src/plugins/loader.test.ts — 102/102 - pnpm test src/plugins/contracts/plugin-entry-guardrails.test.ts — 7/7 - pnpm test src/plugin-sdk/channel-entry-contract.test.ts — 4/4 - Standalone formatter smoke test — output matches existing format byte-for-byte * refactor(plugins): rename profilePluginLoaderSync to withProfile and bind scope at register sites * fix(plugin-sdk): zero jiti sub-step timings on Win32 nodeRequire fast-path
This commit is contained in:
@@ -76,6 +76,7 @@ import {
|
||||
} from "./memory-state.js";
|
||||
import { unwrapDefaultModuleExport } from "./module-export.js";
|
||||
import { isPathInside, safeStatSync } from "./path-safety.js";
|
||||
import { withProfile } from "./plugin-load-profile.js";
|
||||
import {
|
||||
createPluginIdScopeSet,
|
||||
hasExplicitPluginIdScope,
|
||||
@@ -244,30 +245,6 @@ export function clearPluginLoaderCache(): void {
|
||||
|
||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
||||
|
||||
function shouldProfilePluginLoader(): boolean {
|
||||
return process.env.OPENCLAW_PLUGIN_LOAD_PROFILE === "1";
|
||||
}
|
||||
|
||||
function profilePluginLoaderSync<T>(params: {
|
||||
phase: string;
|
||||
pluginId?: string;
|
||||
source: string;
|
||||
run: () => T;
|
||||
}): T {
|
||||
if (!shouldProfilePluginLoader()) {
|
||||
return params.run();
|
||||
}
|
||||
const startMs = performance.now();
|
||||
try {
|
||||
return params.run();
|
||||
} finally {
|
||||
const elapsedMs = performance.now() - startMs;
|
||||
console.error(
|
||||
`[plugin-load-profile] phase=${params.phase} plugin=${params.pluginId ?? "(core)"} elapsedMs=${elapsedMs.toFixed(1)} source=${params.source}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
||||
return (
|
||||
(typeof value === "object" || typeof value === "function") &&
|
||||
@@ -1503,14 +1480,14 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
throw new Error("Unable to resolve plugin runtime module");
|
||||
}
|
||||
const safeRuntimePath = toSafeImportPath(runtimeModulePath);
|
||||
const runtimeModule = profilePluginLoaderSync({
|
||||
phase: "runtime-module",
|
||||
source: runtimeModulePath,
|
||||
run: () =>
|
||||
const runtimeModule = withProfile(
|
||||
{ source: runtimeModulePath },
|
||||
"runtime-module",
|
||||
() =>
|
||||
getJiti(runtimeModulePath)(safeRuntimePath) as {
|
||||
createPluginRuntime?: (options?: CreatePluginRuntimeOptions) => PluginRuntime;
|
||||
},
|
||||
});
|
||||
);
|
||||
if (typeof runtimeModule.createPluginRuntime !== "function") {
|
||||
throw new Error("Plugin runtime module missing createPluginRuntime export");
|
||||
}
|
||||
@@ -1961,12 +1938,11 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
// Track the plugin as imported once module evaluation begins. Top-level
|
||||
// code may have already executed even if evaluation later throws.
|
||||
recordImportedPluginId(record.id);
|
||||
mod = profilePluginLoaderSync({
|
||||
phase: registrationMode,
|
||||
pluginId: record.id,
|
||||
source: safeSource,
|
||||
run: () => getJiti(safeSource)(safeImportSource) as OpenClawPluginModule,
|
||||
});
|
||||
mod = withProfile(
|
||||
{ pluginId: record.id, source: safeSource },
|
||||
registrationMode,
|
||||
() => getJiti(safeSource)(safeImportSource) as OpenClawPluginModule,
|
||||
);
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
@@ -2039,13 +2015,11 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const safeRuntimeImportSource = toSafeImportPath(safeRuntimeSource);
|
||||
let runtimeMod: OpenClawPluginModule | null = null;
|
||||
try {
|
||||
runtimeMod = profilePluginLoaderSync({
|
||||
phase: "load-setup-runtime-entry",
|
||||
pluginId: record.id,
|
||||
source: safeRuntimeSource,
|
||||
run: () =>
|
||||
getJiti(safeRuntimeSource)(safeRuntimeImportSource) as OpenClawPluginModule,
|
||||
});
|
||||
runtimeMod = withProfile(
|
||||
{ pluginId: record.id, source: safeRuntimeSource },
|
||||
"load-setup-runtime-entry",
|
||||
() => getJiti(safeRuntimeSource)(safeRuntimeImportSource) as OpenClawPluginModule,
|
||||
);
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
@@ -2262,7 +2236,11 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
||||
const previousMemoryRuntime = getMemoryRuntime();
|
||||
|
||||
try {
|
||||
runPluginRegisterSync(register, api);
|
||||
withProfile(
|
||||
{ pluginId: record.id, source: record.source },
|
||||
`${registrationMode}:register`,
|
||||
() => runPluginRegisterSync(register, api),
|
||||
);
|
||||
// Snapshot loads should not replace process-global runtime prompt state.
|
||||
if (!shouldActivate) {
|
||||
restoreRegisteredAgentHarnesses(previousAgentHarnesses);
|
||||
@@ -2587,12 +2565,11 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
|
||||
let mod: OpenClawPluginModule | null = null;
|
||||
try {
|
||||
mod = profilePluginLoaderSync({
|
||||
phase: "cli-metadata",
|
||||
pluginId: record.id,
|
||||
source: safeSource,
|
||||
run: () => getJiti(safeSource)(safeImportSource) as OpenClawPluginModule,
|
||||
});
|
||||
mod = withProfile(
|
||||
{ pluginId: record.id, source: safeSource },
|
||||
"cli-metadata",
|
||||
() => getJiti(safeSource)(safeImportSource) as OpenClawPluginModule,
|
||||
);
|
||||
} catch (err) {
|
||||
recordPluginError({
|
||||
logger,
|
||||
@@ -2683,7 +2660,9 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
|
||||
const registrySnapshot = snapshotPluginRegistry(registry);
|
||||
try {
|
||||
runPluginRegisterSync(register, api);
|
||||
withProfile({ pluginId: record.id, source: record.source }, "cli-metadata:register", () =>
|
||||
runPluginRegisterSync(register, api),
|
||||
);
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user