mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:10:45 +00:00
perf(plugins): use native require for compiled JS before jiti
Every CLI invocation reads the config snapshot, which pulls bundled channel doctor contracts and setup surfaces through `getCachedPluginJitiLoader`. jiti's TS→JS transform pipeline adds several seconds of per-load overhead on slower hosts (NAS profiling shows ~78% of `openclaw config get` wall time spent inside the jiti library), and that overhead is pure waste for the already-compiled `.js` artifacts shipped in dist/. Wrap the loader returned by `getCachedPluginJitiLoader` so that compiled JS targets go through `tryNativeRequireJavaScriptModule` first. Jiti stays on the hot path for: - TS/TSX/MTS/CTS sources - paths the native-require helper declines (Windows by default, or module-resolution fallbacks) This centralises the fast path that already existed — inside `doctor-contract-registry` and `channel-entry-contract` — and extends it to every caller that goes through the jiti loader cache. Benchmark on a modest NAS (Node 22.22, ZFS, telegram + discord configured): | command | before | after | |------------------|-------:|------:| | config get X | 24s | 6s | | status | 45s | 18s | | devices list | 55s | 26s | | nodes status | 55s | 26s | Fixes the slow config/status/devices/nodes read paths reported in openclaw#62842. Remaining time is dominated by non-jiti code paths (config schema validation, eager provider-plugin module eval) that are out of scope for this patch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -972,6 +972,14 @@ describe("bundled channel entry shape guards", () => {
|
||||
vi.doMock("../../plugins/channel-catalog-registry.js", () => ({
|
||||
listChannelCatalogEntries: () => [],
|
||||
}));
|
||||
// jiti-loader-cache prefers native require() for compiled .js before
|
||||
// falling back to jiti. This test drives plugin loading via the jiti
|
||||
// mock — disable the native-require fast path so the mocked jiti loader
|
||||
// is exercised instead of loading the on-disk fixture directly.
|
||||
vi.doMock("../../plugins/native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => false,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
}));
|
||||
|
||||
let reentered = false;
|
||||
vi.doMock("jiti", () => ({
|
||||
|
||||
@@ -202,4 +202,60 @@ describe("getCachedPluginJitiLoader", () => {
|
||||
expect(firstAlias?.beta).toBe("/repo/alpha/sub");
|
||||
expect((firstAlias as Record<symbol, unknown>)[marker]).toBe(true);
|
||||
});
|
||||
|
||||
it("serves compiled .js targets from native require without invoking the jiti loader", async () => {
|
||||
const jitiLoader = vi.fn();
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: (p: string) =>
|
||||
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
|
||||
tryNativeRequireJavaScriptModule: (target: string) => ({
|
||||
ok: true,
|
||||
moduleExport: { loadedFrom: target },
|
||||
}),
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-fastpath");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
|
||||
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
|
||||
// jiti is created eagerly, but its loader must NOT be invoked for .js
|
||||
// targets that `tryNativeRequireJavaScriptModule` resolves.
|
||||
expect(jitiLoader).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to jiti when the native-require helper declines", async () => {
|
||||
const jitiLoader = vi.fn(() => ({ fromJiti: true }));
|
||||
const createJiti = vi.fn(() => jitiLoader);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
vi.doMock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
}));
|
||||
const { getCachedPluginJitiLoader } = await importFreshModule<
|
||||
typeof import("./jiti-loader-cache.js")
|
||||
>(import.meta.url, "./jiti-loader-cache.js?scope=native-require-fallback");
|
||||
|
||||
const cache = new Map();
|
||||
const loader = getCachedPluginJitiLoader({
|
||||
cache,
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
jitiFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromJiti: boolean };
|
||||
expect(result.fromJiti).toBe(true);
|
||||
expect(jitiLoader).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createJiti } from "jiti";
|
||||
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
|
||||
import {
|
||||
buildPluginLoaderJitiOptions,
|
||||
createPluginLoaderJitiCacheKey,
|
||||
@@ -74,10 +75,24 @@ export function getCachedPluginJitiLoader(params: {
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const loader = (params.createLoader ?? createJiti)(jitiFilename, {
|
||||
const jitiLoader = (params.createLoader ?? createJiti)(jitiFilename, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
tryNative,
|
||||
});
|
||||
// The returned loader prefers native require() for already-compiled JS
|
||||
// artifacts (the bundled plugin public surfaces shipped in dist/) because
|
||||
// jiti's transform pipeline provides no value for output that is already
|
||||
// plain JS and adds several seconds of per-load overhead on slower hosts.
|
||||
// Jiti stays on the hot path for TS / TSX and for the small set of
|
||||
// require(esm)/async-module fallbacks `tryNativeRequireJavaScriptModule`
|
||||
// declines to handle.
|
||||
const loader = ((target: string) => {
|
||||
const native = tryNativeRequireJavaScriptModule(target);
|
||||
if (native.ok) {
|
||||
return native.moduleExport;
|
||||
}
|
||||
return jitiLoader(target);
|
||||
}) as PluginJitiLoader;
|
||||
params.cache.set(scopedCacheKey, loader);
|
||||
return loader;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,15 @@ import {
|
||||
resetRegistryJitiMocks,
|
||||
} from "./test-helpers/registry-jiti-mocks.js";
|
||||
|
||||
// jiti-loader-cache prefers native require() for compiled .js before falling
|
||||
// back to jiti. These tests scripts plugin-loading behaviour through the
|
||||
// jiti mock — disable the native-require fast path so the mocked jiti loader
|
||||
// stays authoritative for the test fixture files on disk.
|
||||
vi.mock("./native-module-require.js", () => ({
|
||||
isJavaScriptModulePath: (_modulePath: string) => false,
|
||||
tryNativeRequireJavaScriptModule: (_modulePath: string) => ({ ok: false }),
|
||||
}));
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
const mocks = getRegistryJitiMocks();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user