Skip to content

perf: memoize plugin loader alias maps#69316

Merged
steipete merged 5 commits into
mainfrom
perf/startup-phase1
Apr 20, 2026
Merged

perf: memoize plugin loader alias maps#69316
steipete merged 5 commits into
mainfrom
perf/startup-phase1

Conversation

@amknight

@amknight amknight commented Apr 20, 2026

Copy link
Copy Markdown
Member

Summary

  • memoize plugin loader alias maps by effective resolution context
  • reuse the resolved Jiti loader config/cache key on hot loader-cache hits
  • bound the loader alias/config caches to avoid unbounded growth on unique module paths
  • keep cache keys sensitive to cwd, production mode, and private QA alias enablement

Why

OpenClaw hits this in real plugin/channel loader paths. A cached Jiti loader hit still had to rebuild the plugin SDK alias map and stringify the alias map into a Jiti cache key before it could discover that the loader was already cached.

Reading local jiti@2.6.1 source showed that this is not about reusing Jiti's internal alias sentinel across fresh createJiti() calls. Jiti normalizes aliases into its own internal object. The useful optimization is avoiding repeated OpenClaw alias/config resolution before returning an already-cached loader.

Measurements

Local synthetic measurements on this machine; medians across repeated samples.

Benchmark main first PR patch refined PR
buildPluginLoaderAliasMap() same module, 5,000 calls 3676.79ms 4.74ms 4.74ms
getCachedPluginJitiLoader() same module, 5,000 calls 13770.14ms 1392.08ms 41.71ms
loadChannelPluginModule() same module, 5,000 hot loads 17174.67ms 3791.66ms 583.18ms

Tradeoff

This helps repeated same-context loader resolution: long-running processes, channel/package-state probes, runtime boundary loads, and import-heavy tests. It is not a broad cold-start win when every module path is unique. Unique paths still pay first-resolution cost, with cache entries capped to avoid retaining arbitrary module paths forever.

Verification

  • OPENCLAW_VITEST_MAX_WORKERS=1 pnpm test src/plugins/sdk-alias.test.ts src/plugins/jiti-loader-cache.test.ts
  • pnpm build
  • commit hook pnpm check

@openclaw-barnacle openclaw-barnacle Bot added channel: slack Channel integration: slack size: XS maintainer Maintainer-authored PR labels Apr 20, 2026
@amknight amknight marked this pull request as ready for review April 20, 2026 12:04
@greptile-apps

greptile-apps Bot commented Apr 20, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR implements two startup-time performance optimizations: memoizing buildPluginLoaderAliasMap to avoid repeated O(N²) jiti alias normalization, and creating a narrow runtime-setter-api.ts entry point for the Slack plugin to bypass the 284 KB barrel during register(). Both changes are well-scoped and correctly implemented.

The only thing to verify is whether extensions/slack/channel-entry.ts (not touched in this PR) is also a live code path for plugin registration — if so, it still points at the wide ./runtime-api.js barrel and would miss the optimization.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 suggestions with no correctness or data-integrity impact.

The memoization logic correctly covers all inputs to the underlying resolvers, and the new Slack entry point cleanly re-exports only the needed symbol. The only open question is whether channel-entry.ts is a live path that should also be updated — that is a follow-up improvement, not a blocker.

extensions/slack/channel-entry.ts — still references the wide runtime-api.js barrel; worth confirming if this path is active.

Comments Outside Diff (1)

  1. extensions/slack/channel-entry.ts, line 17 (link)

    P2 channel-entry.ts still references the wide barrel

    channel-entry.ts is a separate entry point that still points runtime.specifier at ./runtime-api.js. If this file is used in any plugin register() path, it will continue pulling in the 284 KB / 29-chunk barrel that this PR is specifically trying to avoid. Should ./runtime-setter-api.js be used here too?

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: extensions/slack/channel-entry.ts
    Line: 17
    
    Comment:
    **`channel-entry.ts` still references the wide barrel**
    
    `channel-entry.ts` is a separate entry point that still points `runtime.specifier` at `./runtime-api.js`. If this file is used in any plugin `register()` path, it will continue pulling in the 284 KB / 29-chunk barrel that this PR is specifically trying to avoid. Should `./runtime-setter-api.js` be used here too?
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/slack/channel-entry.ts
Line: 17

Comment:
**`channel-entry.ts` still references the wide barrel**

`channel-entry.ts` is a separate entry point that still points `runtime.specifier` at `./runtime-api.js`. If this file is used in any plugin `register()` path, it will continue pulling in the 284 KB / 29-chunk barrel that this PR is specifically trying to avoid. Should `./runtime-setter-api.js` be used here too?

```suggestion
    specifier: "./runtime-setter-api.js",
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "test: add memoization safety tests for b..." | Re-trigger Greptile

@amknight amknight changed the title perf: memoize buildPluginLoaderAliasMap + narrow slack runtime setter perf: memoize buildPluginLoaderAliasMap Apr 20, 2026
@amknight amknight force-pushed the perf/startup-phase1 branch from cb1d934 to 11e3b49 Compare April 20, 2026 12:15
@openclaw-barnacle openclaw-barnacle Bot removed the channel: slack Channel integration: slack label Apr 20, 2026
@amknight amknight changed the title perf: memoize buildPluginLoaderAliasMap perf: memoize buildPluginLoaderAliasMap to enable jiti sentinel reuse Apr 20, 2026
@steipete steipete changed the title perf: memoize buildPluginLoaderAliasMap to enable jiti sentinel reuse perf: memoize plugin loader alias maps Apr 20, 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: f9c19e35d1

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/plugins/sdk-alias.ts Outdated
Comment on lines +501 to +502
aliasMapCache.set(cacheKey, result);
return result;

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 Cache the Jiti-normalized alias map, not the raw alias object

This memoization does not actually unlock Jiti’s normalizeAliases sentinel fast-path: Jiti tags the normalized clone with Symbol.for("pathe:normalizedAlias"), not the input alias object, so returning the same raw result reference here still causes re-normalization on each createJiti call. In practice, the startup-cost regression this commit targets remains, while we still introduce process-lifetime alias caching behavior. To make this optimization effective, the cached value needs to be the normalized alias object that already carries Jiti’s sentinel.

Useful? React with 👍 / 👎.

amknight and others added 5 commits April 20, 2026 14:50
buildPluginLoaderAliasMap() creates a new alias object via spread on every
call. jiti's normalizeAliases() uses a reference-identity sentinel
(`if (e[pt]) return e`) to skip its O(N²) normalization work — but fresh
object refs defeat the sentinel, causing the full cycle to repeat on
every call.

This change caches alias maps by their inputs (modulePath, argv1,
moduleUrl, pluginSdkResolution) so identical parameters return the same
object reference. Subsequent jiti calls hit the sentinel fast-path
instead of re-running normalization.

Includes 5 new tests covering:
  - reference identity for identical inputs
  - cache isolation (different modulePath, pluginSdkResolution, argv1
    each produce distinct objects)
  - content equivalence between cached and freshly-computed results

Refs #68983, #63948
@steipete steipete force-pushed the perf/startup-phase1 branch from 7fac8da to c5fc4ef Compare April 20, 2026 13:51
@steipete steipete merged commit 8a66009 into main Apr 20, 2026
48 checks passed
@steipete steipete deleted the perf/startup-phase1 branch April 20, 2026 14:00
@steipete

Copy link
Copy Markdown
Contributor

Thanks @amknight, landed via rebase merge.

Landed on main at 8a660099f2b3c2bc4dd9a90b2820106562c97913.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer Maintainer-authored PR size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants