mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
perf(gateway): avoid jiti on native plugin loads
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/config: stop Gateway startup and hot reload from auto-restoring invalid config; invalid config now fails closed and `openclaw doctor --fix` owns last-known-good repair.
|
||||
- Gateway/performance: lazy-load early runtime discovery and shutdown-hook helpers, defer maintenance timers until after readiness, and trim duplicate plugin auto-enable work during Gateway startup.
|
||||
- Gateway/performance: defer non-readiness sidecars until after the ready signal, avoid hot-path channel plugin barrel imports, and fast-path trusted bundled plugin metadata during Gateway startup.
|
||||
- Gateway/performance: avoid importing `jiti` on native-loadable plugin startup paths, so compiled bundled plugin surfaces do not pay source-transform loader cost unless fallback loading is actually needed.
|
||||
- QA/Mantis: add a `pnpm openclaw qa mantis discord-smoke` runner and manual GitHub workflow that verify the Mantis Discord bot can see the configured guild/channel, post a smoke message, add a reaction, and upload artifacts.
|
||||
- QA/Mantis: add `pnpm openclaw qa mantis slack-desktop-smoke` to run Slack live QA inside a Crabbox VNC desktop, open Slack Web, and capture desktop screenshots beside the Slack QA artifacts.
|
||||
- QA/Mantis: pass the runtime env through desktop-browser Crabbox and artifact-copy child commands, so embedded Mantis callers can provide Crabbox credentials without mutating the parent process. Thanks @vincentkoc.
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { createJiti } from "jiti";
|
||||
import type { createJiti } from "jiti";
|
||||
import { buildChannelConfigSchema } from "../src/channels/plugins/config-schema.js";
|
||||
import {
|
||||
buildPluginLoaderJitiOptions,
|
||||
@@ -9,6 +10,23 @@ import {
|
||||
resolvePluginSdkScopedAliasMap,
|
||||
} from "../src/plugins/sdk-alias.js";
|
||||
|
||||
type CreateJiti = typeof createJiti;
|
||||
|
||||
const requireForJiti = createRequire(import.meta.url);
|
||||
let createJitiLoaderFactory: CreateJiti | undefined;
|
||||
|
||||
function loadCreateJitiLoaderFactory(): CreateJiti {
|
||||
if (createJitiLoaderFactory) {
|
||||
return createJitiLoaderFactory;
|
||||
}
|
||||
const loaded = requireForJiti("jiti") as { createJiti?: CreateJiti };
|
||||
if (typeof loaded.createJiti !== "function") {
|
||||
throw new Error("jiti module did not export createJiti");
|
||||
}
|
||||
createJitiLoaderFactory = loaded.createJiti;
|
||||
return createJitiLoaderFactory;
|
||||
}
|
||||
|
||||
function isBuiltChannelConfigSchema(
|
||||
value: unknown,
|
||||
): value is { schema: Record<string, unknown>; uiHints?: Record<string, unknown> } {
|
||||
@@ -137,7 +155,7 @@ export async function loadChannelConfigSurfaceModule(
|
||||
pluginSdkResolution: "src",
|
||||
}),
|
||||
};
|
||||
const jiti = createJiti(import.meta.url, {
|
||||
const jiti = loadCreateJitiLoaderFactory()(import.meta.url, {
|
||||
...buildPluginLoaderJitiOptions(aliasMap),
|
||||
interopDefault: true,
|
||||
tryNative: false,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginModuleLoaderFactory } from "./plugin-module-loader-cache.js";
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
@@ -22,7 +23,17 @@ async function loadCachedPluginModuleLoader(scope: string) {
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, `./plugin-module-loader-cache.js?scope=${scope}`);
|
||||
|
||||
return { createJiti, getCachedPluginModuleLoader };
|
||||
const getCachedPluginModuleLoaderWithMock: typeof getCachedPluginModuleLoader = (params) =>
|
||||
getCachedPluginModuleLoader({
|
||||
...params,
|
||||
createLoader: params.createLoader ?? asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
return { createJiti, getCachedPluginModuleLoader: getCachedPluginModuleLoaderWithMock };
|
||||
}
|
||||
|
||||
function asPluginModuleLoaderFactory(factory: unknown): PluginModuleLoaderFactory {
|
||||
return factory as PluginModuleLoaderFactory;
|
||||
}
|
||||
|
||||
describe("getCachedPluginModuleLoader", () => {
|
||||
@@ -361,7 +372,8 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
it("serves compiled .js targets from native require without invoking the module loader", async () => {
|
||||
const fromSourceTransformer = vi.fn();
|
||||
const createJiti = vi.fn(() => fromSourceTransformer);
|
||||
vi.doMock("jiti", () => ({ createJiti }));
|
||||
const jitiModuleFactory = vi.fn(() => ({ createJiti }));
|
||||
vi.doMock("jiti", jitiModuleFactory);
|
||||
const nativeStub = vi.fn((target: string) => ({
|
||||
ok: true as const,
|
||||
moduleExport: { loadedFrom: target },
|
||||
@@ -381,12 +393,14 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
createLoader: asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { loadedFrom: string };
|
||||
expect(result.loadedFrom).toBe("/repo/dist/extensions/demo/api.js");
|
||||
// Jiti should not be constructed or invoked for .js targets that
|
||||
// `tryNativeRequireJavaScriptModule` resolves.
|
||||
expect(jitiModuleFactory).not.toHaveBeenCalled();
|
||||
expect(createJiti).not.toHaveBeenCalled();
|
||||
expect(fromSourceTransformer).not.toHaveBeenCalled();
|
||||
// allowWindows must be passed so the native fast path works on Windows too.
|
||||
@@ -420,6 +434,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
createLoader: asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
|
||||
@@ -455,6 +470,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
importerUrl: "file:///C:/Users/alice/openclaw/dist/src/plugins/public-surface-loader.js",
|
||||
loaderFilename: "C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js",
|
||||
tryNative: true,
|
||||
createLoader: asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
loader("C:\\Users\\alice\\openclaw\\dist\\extensions\\feishu\\api.js");
|
||||
@@ -489,6 +505,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
loaderFilename: "file:///repo/src/plugins/bundled-capability-runtime.ts",
|
||||
aliasMap: { "openclaw/plugin-sdk": "/repo/shim.js" },
|
||||
tryNative: false,
|
||||
createLoader: asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
|
||||
@@ -528,6 +545,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
importerUrl: "file:///C:/Users/alice/openclaw/src/plugins/loader.ts",
|
||||
loaderFilename: "C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts",
|
||||
tryNative: false,
|
||||
createLoader: asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
loader("C:\\Users\\alice\\openclaw\\extensions\\feishu\\api.ts");
|
||||
@@ -560,6 +578,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
modulePath: "/repo/dist/extensions/demo/api.js",
|
||||
importerUrl: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
loaderFilename: "file:///repo/src/plugins/public-surface-loader.ts",
|
||||
createLoader: asPluginModuleLoaderFactory(createJiti),
|
||||
});
|
||||
|
||||
const loose = loader as unknown as (t: string, ...a: unknown[]) => unknown;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createJiti } from "jiti";
|
||||
import { createRequire } from "node:module";
|
||||
import type { createJiti } from "jiti";
|
||||
import { toSafeImportPath } from "../shared/import-specifier.js";
|
||||
import { tryNativeRequireJavaScriptModule } from "./native-module-require.js";
|
||||
import { PluginLruCache } from "./plugin-cache-primitives.js";
|
||||
@@ -45,6 +46,9 @@ export type PluginModuleLoaderStatsSnapshot = {
|
||||
|
||||
const DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES = 128;
|
||||
const MAX_TRACKED_SOURCE_TRANSFORM_TARGETS = 24;
|
||||
const JITI_FACTORY_OVERRIDE_KEY = Symbol.for("openclaw.pluginModuleLoaderJitiFactoryOverride");
|
||||
const requireForJiti = createRequire(import.meta.url);
|
||||
let createJitiLoaderFactory: PluginModuleLoaderFactory | undefined;
|
||||
const pluginModuleLoaderStats = {
|
||||
calls: 0,
|
||||
nativeHits: 0,
|
||||
@@ -96,6 +100,26 @@ export function resetPluginModuleLoaderStatsForTest(): void {
|
||||
pluginModuleLoaderStats.sourceTransformTargets.clear();
|
||||
}
|
||||
|
||||
function loadCreateJitiLoaderFactory(): PluginModuleLoaderFactory {
|
||||
const override = (
|
||||
globalThis as typeof globalThis & {
|
||||
[JITI_FACTORY_OVERRIDE_KEY]?: PluginModuleLoaderFactory;
|
||||
}
|
||||
)[JITI_FACTORY_OVERRIDE_KEY];
|
||||
if (override) {
|
||||
return override;
|
||||
}
|
||||
if (createJitiLoaderFactory) {
|
||||
return createJitiLoaderFactory;
|
||||
}
|
||||
const loaded = requireForJiti("jiti") as { createJiti?: PluginModuleLoaderFactory };
|
||||
if (typeof loaded.createJiti !== "function") {
|
||||
throw new Error("jiti module did not export createJiti");
|
||||
}
|
||||
createJitiLoaderFactory = loaded.createJiti;
|
||||
return createJitiLoaderFactory;
|
||||
}
|
||||
|
||||
export function createPluginModuleLoaderCache(
|
||||
maxEntries = DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES,
|
||||
): PluginModuleLoaderCache {
|
||||
@@ -166,10 +190,13 @@ function createLazySourceTransformLoader(params: {
|
||||
if (loadWithSourceTransform) {
|
||||
return loadWithSourceTransform;
|
||||
}
|
||||
const jitiLoader = (params.createLoader ?? createJiti)(params.loaderFilename, {
|
||||
...buildPluginLoaderJitiOptions(params.aliasMap),
|
||||
tryNative: params.tryNative,
|
||||
});
|
||||
const jitiLoader = (params.createLoader ?? loadCreateJitiLoaderFactory())(
|
||||
params.loaderFilename,
|
||||
{
|
||||
...buildPluginLoaderJitiOptions(params.aliasMap),
|
||||
tryNative: params.tryNative,
|
||||
},
|
||||
);
|
||||
loadWithSourceTransform = new Proxy(jitiLoader, {
|
||||
apply(target, thisArg, argArray) {
|
||||
const [first, ...rest] = argArray as [unknown, ...unknown[]];
|
||||
|
||||
@@ -6,6 +6,9 @@ const registryJitiMocks = vi.hoisted(() => ({
|
||||
loadPluginManifestRegistry: vi.fn(),
|
||||
loadPluginRegistrySnapshot: vi.fn(),
|
||||
}));
|
||||
const pluginModuleLoaderJitiFactoryOverrideKey = Symbol.for(
|
||||
"openclaw.pluginModuleLoaderJitiFactoryOverride",
|
||||
);
|
||||
|
||||
vi.mock("jiti", () => ({
|
||||
createJiti: (...args: Parameters<typeof registryJitiMocks.createJiti>) =>
|
||||
@@ -43,6 +46,11 @@ vi.mock("../plugin-registry.js", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
export function resetRegistryJitiMocks(): void {
|
||||
(
|
||||
globalThis as typeof globalThis & {
|
||||
[pluginModuleLoaderJitiFactoryOverrideKey]?: typeof registryJitiMocks.createJiti;
|
||||
}
|
||||
)[pluginModuleLoaderJitiFactoryOverrideKey] = registryJitiMocks.createJiti;
|
||||
registryJitiMocks.createJiti.mockReset();
|
||||
registryJitiMocks.discoverOpenClawPlugins.mockReset();
|
||||
registryJitiMocks.loadPluginManifestRegistry.mockReset();
|
||||
|
||||
Reference in New Issue
Block a user