Skip to content

perf(slack): narrow runtime-setter + lazy-load 4 modules + narrow 2 SDK surfaces#69317

Merged
amknight merged 7 commits into
mainfrom
aknight/perf-slack-lazy-load
Apr 22, 2026
Merged

perf(slack): narrow runtime-setter + lazy-load 4 modules + narrow 2 SDK surfaces#69317
amknight merged 7 commits into
mainfrom
aknight/perf-slack-lazy-load

Conversation

@amknight

@amknight amknight commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

Slack startup-perf changes that drop slack's cold register() from 14.1s → 6.2s (-56%) on top of #69316. Work is shifted off the cold path so it only happens when actually exercised.

Validated end-to-end via dist-mode gateway profiling on origin/main (with #69316 merged).

What changed

  • Narrow the bundled runtime specifier at extensions/slack/index.ts to a new runtime-setter-api.ts that only re-exports setSlackRuntime, instead of the full runtime-api.js barrel that pulled in ~284KB during synchronous register().
  • Narrow the HTTP-routes specifier the same way via a new http-routes-api.ts so registerSlackPluginHttpRoutes no longer drags the broad barrel.
  • Lazy-load four slack subsystems behind their existing async seams:
    • ./setup-surface.js (via createSlackSetupWizardProxy)
    • ./security-audit.js
    • ./scopes.js
    • ./outbound-adapter.js
  • Lazy-load two SDK barrels behind structural-type loaders:
    • openclaw/plugin-sdk/extension-shared
    • openclaw/plugin-sdk/target-resolver-runtime
  • Kept eager where required by sync contracts: approval-native, setup-core, channel-policy (already eager elsewhere).
  • Tests: regression coverage for every new lazy seam plus a guardrail pinning runtime-setter-api.ts to its single export so the perf intent can't be silently re-broadened.

No public-contract or runtime-semantics changes — purely load-time deferral.

Measured impact

End-to-end slack register() (cold, dist-mode, on origin/main with #69316):

Phase Before After Δ
setChannelRuntime 13183ms 67ms −13116ms (−99.5%)
loadChannelPlugin 913ms 6072ms* shifted, not new work
registerChannel 0.2ms 0.2ms 0
registerFull 2ms 40ms negligible
Total 14102ms 6180ms −7922ms (−56%)

*loadChannelPlugin increased only because setChannelRuntime no longer pre-warms the slack chunk graph; net wall-clock dropped 7.9s.

This composes with #69316: this PR shrinks what is loaded at register-time, while #69316 shrinks the per-module cost of every load.

What this PR does NOT fix

The remaining ~6s in loadChannelPlugin is the genuine cost of jiti walking + transpiling 30+ modules to construct slackPlugin. Further wins there need either deferred subsystem construction inside slackPlugin, pre-bundling channel-plugin-api.js, or replacing jiti for dist/*.js. Tracked as follow-up.

Verification

Manual testing end to end via Slack app to verify no regressions

  • pnpm tsgo:extensions / tsgo:extensions:test / tsgo (core) — clean
  • pnpm test extensions/slack/src/channel.ts — 657 tests pass
  • pnpm test src/plugins/contracts/plugin-entry-guardrails.test.ts — 7/7
  • pnpm lint extensions/slack/src/channel.ts — 0 warnings, 0 errors
  • Fresh dist/extensions/slack/channel-*.js confirms the new dynamic-import boundaries land in the bundler's dynamic-imports section while channel-policy, setup-core, and approval-native stay static as intended.

Refs #68983, #65444.

@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack size: S maintainer Maintainer-authored PR labels Apr 20, 2026
@amknight amknight force-pushed the aknight/perf-slack-lazy-load branch from 1d3c26c to d219186 Compare April 20, 2026 12:17
@amknight amknight changed the title perf(slack): lazy-load 6 modules + narrow 3 SDK surfaces perf(slack): lazy-load 4 modules + narrow 2 SDK surfaces Apr 20, 2026
@amknight amknight force-pushed the aknight/perf-slack-lazy-load branch 2 times, most recently from 6ca72d6 to d17e97e Compare April 20, 2026 12:32
@amknight amknight changed the title perf(slack): lazy-load 4 modules + narrow 2 SDK surfaces perf(slack): narrow runtime-setter + lazy-load 4 modules + narrow 2 SDK surfaces Apr 20, 2026
@amknight amknight marked this pull request as ready for review April 20, 2026 12:36
@greptile-apps

greptile-apps Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR introduces three focused load-time optimizations for the Slack extension's cold-start register() path: two new narrow entry points (runtime-setter-api.ts, http-routes-api.ts) that replace broad barrel specifiers in index.ts, and a batch of lazy-load conversions in channel.ts for four subsystems and two SDK surfaces. The changes are purely load-time deferral — no public contracts or runtime semantics are altered, every lazy-wrapped callback was already async, and pnpm test / tsgo / lint all pass.

Confidence Score: 5/5

Safe to merge — no P0/P1 issues found; all changes are load-time deferrals with no semantic impact.

The changes are mechanical and well-scoped: two new re-export files, two specifier swaps, and lazy-load wrappers in already-async callbacks. The PR includes extensive profiling data, passing tests (78 files / 657 tests), bundler verification of dynamic-import boundaries, and lint results. No logic changes touch runtime behavior. The structural-alias drift risk pattern (SlackScopesResultShape) was already flagged in a prior thread; the ExtensionSharedSurface and TargetResolverRuntimeSurface aliases follow the same known tradeoff, documented in comments.

No files require special attention.

Reviews (2): Last reviewed commit: "perf(slack): narrow registerSlackPluginH..." | Re-trigger Greptile

Comment thread extensions/slack/src/channel.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d17e97e95f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/slack/package.json Outdated
"extensions": [
"./index.ts"
"./index.ts",
"./runtime-setter-api.ts"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep runtime helper out of openclaw.extensions list

openclaw.extensions is treated as a list of plugin entry candidates, not arbitrary sidecar files. Adding ./runtime-setter-api.ts means discovery will enqueue it (src/plugins/discovery.ts iterates every entry), and loader then sees a duplicate slack candidate and records an extra overridden plugin record (src/plugins/loader.ts duplicate branch), which pollutes plugin status/diagnostics; this is fragile because the helper itself does not export a plugin entry contract. Use an entrypoint-specific packaging mechanism instead of advertising the helper as an extension entry.

Useful? React with 👍 / 👎.

@amknight amknight marked this pull request as draft April 20, 2026 12:49

@martingarramon martingarramon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Structural approach (narrow entries + lazy-load) is sound. Three concerns — one blocker on commit 1's wiring, plus amplifications of the Codex + Greptile findings.

1. BLOCKER — Commit 1 not actually wired for setSlackRuntime

The PR body says "Adds a narrow runtime-setter-api.ts entry point that re-exports only setSlackRuntime, and points the contract at it." The file is created, but the channel entry contract still points at the full barrel on both references:

  • extensions/slack/index.ts:29-30 (the defineBundledChannelEntry({ runtime: { specifier: "./runtime-api.js", exportName: "setSlackRuntime" } }) block) — unchanged by the PR
  • extensions/slack/channel-entry.ts:17-18not in the PR's file list at all

Only index.ts:9 (the registerSlackPluginHttpRoutes specifier) was updated. The 284KB runtime-api barrel is still loaded synchronously through loadBundledEntryExportSync for setSlackRuntime on every register(). Commit 1's stated goal — "avoid pulling in the full runtime-api barrel during plugin register()" — is only half achieved (http routes yes, runtime setter no).

Missing updates:

  • extensions/slack/index.ts:29-30specifier: "./runtime-setter-api.js"
  • extensions/slack/channel-entry.ts:17-18 → same

If channel-entry.ts is the canonical contract and index.ts is legacy (or vice versa), only one needs to change. Worth confirming with @zimeg / maintainer which is authoritative before pushing the fix.

2. Codex P2 — worth verifying extensions-list duplicate-discovery concern

Codex flagged that openclaw.extensions is iterated by src/plugins/discovery.ts's resolvePackageExtensionEntries (manifest.ts:916-928), with each entry becoming a plugin candidate. The loader's duplicate-detection branch at src/plugins/loader.ts:1685 / :2448 produces "overridden by X plugin" records for candidates with the same idHint.

Whether this actually triggers depends on loadPluginManifest's treatment of entries that are pure re-exports with no plugin contract. If it rejects them cleanly, they're filtered out; if it accepts them as weak candidates, "overridden" diagnostics appear.

Easiest check: run openclaw status --plugins (or equivalent plugin-registry dump) on a build of this branch and confirm slack appears exactly once with no "overridden by" entries. If duplicates appear, the right mechanism is a dedicated packaging field rather than the extensions list.

3. Greptile P2 + same concern for the two SDK surfaces

Greptile correctly flagged SlackScopesResultShape as a silent-drift risk. A satisfies SlackScopesResultShape on the return value in scopes.ts catches drift at compile time.

Same concern applies to ExtensionSharedSurface and TargetResolverRuntimeSurface in channel.ts:78-105. Both are hand-written structural aliases over SDK module shapes. If the SDK evolves either (adds an overload, makes a field optional, changes a generic bound), the typed loader silently accepts values the runtime would then mishandle — pre-existing tests won't catch this because the lazy loaders never invoke tsgo against the real SDK shape.

Two low-cost options:

  • Export the shapes from the SDK and import them (type-only imports don't break the lazy-load bundling boundary).
  • Add a .test-d.ts that imports the real SDK surfaces and asserts expectTypeOf<ExtensionSharedSurface>().toEqualTypeOf<ActualSDKShape>() for each. ~10 lines, locks the alias shape.

Minor, non-blocking

  • Latency tradeoff framing. Commit 2 moves load time from register() to first-use. For outbound, that's "first Slack send pays the lazy-load." Worth calling out in the profile-driven validation explicitly — net win = Σ(startup saving - per-user-first-send cost) depending on workload, and the sign isn't obvious without measurement.
  • Test coverage of lazy boundaries. 78 files / 657 tests pass, but the PR doesn't say which tests exercise the lazy-loaded codepaths (first outbound send, first collectAuditFindings, first wizard invocation). Worth one sentence confirming coverage of the dynamic-import sites.

Commit 2 is solid — the lazy-loading architecture, createLazyRuntimeModule usage, and proxy pattern for setupWizard look right. Once commit 1's runtime specifier is actually wired and Codex's extensions-list concern verified, this looks shippable.

@amknight amknight force-pushed the aknight/perf-slack-lazy-load branch 2 times, most recently from 1667161 to 88b1e46 Compare April 20, 2026 20:33
@amknight amknight marked this pull request as ready for review April 21, 2026 01:37

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88b1e461fe

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/slack/package.json Outdated
Comment on lines +18 to +20
"./index.ts",
"./runtime-setter-api.ts",
"./http-routes-api.ts"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove helper sidecars from openclaw.extensions list

These added entries are treated as channel entrypoints by release smoke: scripts/test-built-bundled-channel-entry-smoke.mjs iterates every openclaw.extensions item (collectBundledChannelEntryFiles) and then asserts each imported module has a default bundled channel entry shape (entry.kind === "bundled-channel-entry"). runtime-setter-api.ts and http-routes-api.ts only provide named helper exports, so prepack/release checks that run this smoke will fail when they try to load these files as channel entries.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0a95467aa0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/slack/index.ts
},
runtime: {
specifier: "./runtime-api.js",
specifier: "./runtime-setter-api.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Register runtime-setter sidecar as critical runtime artifact

Switching runtime.specifier to ./runtime-setter-api.js makes startup depend on this new sidecar, but runtime-sidecar classification still only recognizes names like runtime-api.js (src/plugins/bundled-plugin-scan.ts), and update integrity checks (collectCriticalInstalledPackageDistPaths in src/infra/update-global.ts) only enforce files in that list. In a partial/trimmed global update, dist/extensions/slack/runtime-setter-api.js can now be missing without being caught by preflight checks, and Slack registration will fail when loadBundledEntryExportSync resolves the runtime setter at startup.

Useful? React with 👍 / 👎.

amknight added a commit that referenced this pull request Apr 21, 2026
…l 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
gdibble pushed a commit to gdibble/openclaw that referenced this pull request Apr 21, 2026
…l entries (openclaw#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 openclaw#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
…arrel

The slack extension's BundledChannelEntryContract pointed its `runtime`
specifier at `./runtime-api.js` — a barrel module that re-exports the
full slack runtime surface (29 chunks, ~284KB). The framework calls
`setChannelRuntime` synchronously during `register()`, so this barrel
was loaded on every gateway startup just to obtain the 23-line
`setSlackRuntime` setter function.

Add a narrow `runtime-setter-api.ts` entry point that re-exports only
`setSlackRuntime`, and point the contract at it. The full runtime-api
tree is still loaded — but only when an actual slack message arrives,
not during register().

Refs #68983, #65444
Single-file change to extensions/slack/src/channel.ts that defers
loading slack subsystems and SDK runtime barrels not needed at gateway
register() time. Addresses PERF-STARTUP-PLAN.md Phases 2A and 2B.

Phase 2A — slack subsystems now lazy-loaded on first use:
- ./setup-surface.js  (slackSetupWizard) — wired through the existing
  createSlackSetupWizardProxy helper so the wizard (and its heavy
  setup-runtime SDK barrel) only loads inside an `openclaw setup` flow.
- ./security-audit.js (collectSlackSecurityAuditFindings) — hidden
  behind an async wrapper on `security.collectAuditFindings`.
- ./scopes.js         (fetchSlackScopes) — loaded inside
  `buildCapabilitiesDiagnostics`. A local SlackScopesResultShape alias
  keeps the result type from dragging the module back in for type
  resolution.
- ./outbound-adapter.js (slackOutbound) — loaded inside
  `outbound.base.sendPayload` on the first outbound message.

Kept eager (with explanatory comments where non-obvious):
- ./approval-native.js — ChannelApprovalCapability has many synchronous
  methods consumed sync by core (authorizeActorAction etc.); lazy
  wrapping requires a framework-wide contract change and is out of
  scope here.
- ./setup-core.js — small/local; slackSetupAdapter is consumed sync by
  commands/channels/add.ts, and the new createSlackSetupWizardProxy
  helper that wraps setup-surface lives there.

Phase 2B — SDK barrels now lazy-loaded behind structural-type loaders.
The dynamic import is hidden behind a string-literal module id and the
returned shape is described with a hand-written structural type alias,
so TypeScript does not have to crawl the SDK module's full type graph
just to type the loader:
- openclaw/plugin-sdk/extension-shared
  (buildPassiveProbedChannelStatusSummary → async buildChannelSummary)
- openclaw/plugin-sdk/target-resolver-runtime
  (resolveTargetsWithOptionalToken → async resolver.resolveTargets)

Intentionally NOT lazy:
- openclaw/plugin-sdk/channel-policy — already imported eagerly via
  ./group-policy.js, so deferring the channel.ts import would not
  change the load graph (verified by grep + bundler output).

Verification:
- pnpm tsgo:extensions / tsgo:extensions:test / tsgo (core): no
  slack-related errors.
- pnpm test extensions/slack/src/channel.ts: 78 files / 657 tests pass.
- Static-analysis diff vs origin/main:
    BEFORE static imports → AFTER:
      setup-surface       static → dynamic (createLazyRuntimeModule)
      security-audit      static → dynamic
      scopes              static → dynamic
      outbound-adapter    static → dynamic
      extension-shared    static → dynamic (string-literal id)
      target-resolver-rt  static → dynamic (string-literal id)
      channel-policy      static → static  (kept; already eager elsewhere)
      approval-native     static → static  (kept; sync API contract)
      setup-core          static → static  (kept; sync API + wizard proxy)
- Bundler verification: dist/extensions/slack/channel-*.js shows
  `import("./setup-surface-*.js")`, `import("./security-audit-*.js")`,
  `import("./scopes-*.js")`, `import("./outbound-adapter-*.js")`,
  plus `import(EXTENSION_SHARED_MODULE_ID)` /
  `import(TARGET_RESOLVER_RUNTIME_MODULE_ID)` in the dynamic-imports
  section, and channel-policy / setup-core / approval-native in the
  static-imports section as expected.
…loading runtime barrel

The slack extension's `registerFull` callback in `extensions/slack/index.ts`
loaded `./runtime-api.js` synchronously to obtain `registerSlackPluginHttpRoutes`.
That barrel re-exports the entire slack runtime surface (~284KB, 13 chunks),
so every gateway startup paid that cost just to register a few HTTP routes.

This is the same problem #69325 fixed for `setSlackRuntime` — but the
`registerFull` path was overlooked, so the cost simply shifted from
`setChannelRuntime` (~13s) to `registerFull` (~8s) once #69325 landed.

Add a narrow `http-routes-api.ts` entry point that re-exports only
`registerSlackPluginHttpRoutes`, and point `extensions/slack/index.ts`
at it. The narrow entry only pulls in `accounts`, `paths`, and the
`account-id` SDK module — three chunks instead of thirteen.

Per-step cold-start measurement (gateway with all three commits):

  bundled-register:setChannelRuntime                812ms (was 13183ms)
  bundled-register:loadChannelPlugin               5057ms (unchanged)
  slack:registerFull:loadHttpRoutesExport            80ms (was 8389ms)
  TOTAL slack register()                           5956ms (was 14102ms, -8.1s, -58%)

Refs #68983, #65444.
Adds focused regression tests guarding the cold-path lazy-loading
boundaries introduced for Slack startup-perf work:

- channel.lazy-seams.test.ts: 10 tests asserting that
  buildChannelSummary, buildCapabilitiesDiagnostics,
  collectAuditFindings, and resolver.resolveTargets reach their lazy
  modules with the right inputs and propagate outputs by identity.
  Mocks the SDK seams (extension-shared, target-resolver-runtime) and
  the local lazy modules (security-audit, scopes).

- setup-core.lazy-proxy.test.ts: 4 type-safe unit tests for
  createSlackSetupWizardProxy, asserting it does not eagerly load the
  setup-surface module, forwards allowFrom.resolveEntries through the
  lazy loader, falls back when the wizard omits allowFrom, and uses the
  configured fallback for groupAccess.resolveAllowlist.

- bundled-plugin-metadata: assert runtime-setter-api.js is on Slack's
  public surface so the loader can resolve the new bundled-entry
  runtime.specifier.

- plugin-sdk-runtime-api-guardrails: pin runtime-setter-api.ts to a
  single setSlackRuntime export so the perf intent of the narrow
  entrypoint is not silently re-broadened in future.
@amknight amknight force-pushed the aknight/perf-slack-lazy-load branch from 0a95467 to d73429f Compare April 22, 2026 06:15
@openclaw-barnacle openclaw-barnacle Bot added the scripts Repository scripts label Apr 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d73429f24a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread extensions/slack/index.ts
function registerSlackPluginHttpRoutes(api: OpenClawPluginApi): void {
const register = loadBundledEntryExportSync<(api: OpenClawPluginApi) => void>(import.meta.url, {
specifier: "./runtime-api.js",
specifier: "./http-routes-api.js",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track http-routes sidecar as critical runtime artifact

Switching Slack registerFull to specifier: "./http-routes-api.js" makes full-mode channel registration depend on that new top-level sidecar, but the critical sidecar pipeline still only recognizes runtime-sidecar basenames (src/plugins/bundled-plugin-scan.ts) and integrity checks enforce only BUNDLED_RUNTIME_SIDECAR_PATHS (src/infra/update-global.ts), which this commit did not extend for dist/extensions/slack/http-routes-api.js. In partial/legacy global installs (e.g., missing or unusable dist inventory), this file can be absent without preflight failure, and Slack full registration will fail when loadBundledEntryExportSync tries to load the route registrar.

Useful? React with 👍 / 👎.

@amknight

Copy link
Copy Markdown
Member Author

Rebased onto current main and resolved the conflicts.

Conflict resolution summary:

  • extensions/slack/src/channel.ts
    • Kept the PR's lazy-load seams (setup-surface, scopes, outbound-adapter, plus the lazy SDK loaders).
    • Aligned the file with main's newer extracted setup/security structure instead of re-inlining the older branch-local security block.
  • extensions/slack/src/security.ts
    • Moved the lazy security-audit import here so the rebased result preserves the PR's lazy-loading behavior while matching main's extracted slackSecurityAdapter shape.
  • src/plugins/bundled-plugin-metadata.test.ts
    • Kept both additive assertions: the new Slack runtime-setter coverage from this PR and the existing Telegram/Discord runtime-setter assertions already on main.
  • src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts
    • Kept both additive guardrails: Slack's single-export runtime-setter-api.ts assertion from this PR and Matrix's existing single-export assertion from main.
  • scripts/lib/bundled-runtime-sidecar-paths.json
    • Regenerated the checked-in runtime-sidecar baseline because the Slack runtime-setter sidecar is now intentionally part of the packaged surface.

Verification run after the rebase:

  • pnpm runtime-sidecars:check
  • pnpm test extensions/slack/src/channel.test.ts extensions/slack/src/channel.lazy-seams.test.ts extensions/slack/src/setup-core.lazy-proxy.test.ts src/plugins/bundled-plugin-metadata.test.ts src/plugins/contracts/plugin-sdk-runtime-api-guardrails.test.ts
  • pnpm build

@amknight amknight merged commit 2013855 into main Apr 22, 2026
94 checks passed
@amknight amknight deleted the aknight/perf-slack-lazy-load branch April 22, 2026 06:42
medikoo pushed a commit to medikoo/openclaw that referenced this pull request Apr 24, 2026
…l entries (openclaw#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 openclaw#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
medikoo pushed a commit to medikoo/openclaw that referenced this pull request Apr 24, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…l entries (openclaw#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 openclaw#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
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
zhonghe0615 pushed a commit to zhonghe0615/openclaw that referenced this pull request May 7, 2026
…l entries (openclaw#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 openclaw#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
zhonghe0615 pushed a commit to zhonghe0615/openclaw that referenced this pull request May 7, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…l entries (openclaw#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 openclaw#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
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…l entries (openclaw#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 openclaw#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
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…l entries (openclaw#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 openclaw#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
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…l entries (openclaw#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 openclaw#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
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…l entries (openclaw#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 openclaw#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
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…DK surfaces (openclaw#69317)

Lazy load modules showing a ~50% gateway startup performance improvement
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack maintainer Maintainer-authored PR scripts Repository scripts size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants