From ddc1d9aa54fbdeab26ef05e7de45b3f48b0c7d41 Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Tue, 21 Apr 2026 13:24:28 -0400 Subject: [PATCH] perf: speed up telegram channel registration (#69786) Merged via squash. Prepared head SHA: ac03f96e0d13b9d4fe91d1d6a157dbf7053fd623 Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com> Reviewed-by: @gumadeiras --- CHANGELOG.md | 1 + extensions/telegram/index.ts | 2 +- extensions/telegram/runtime-setter-api.ts | 3 + .../lib/bundled-runtime-sidecar-paths.json | 2 + src/plugin-sdk/channel-entry-contract.test.ts | 91 ++++++++++--------- src/plugin-sdk/channel-entry-contract.ts | 18 ++-- src/plugins/bundled-plugin-metadata.test.ts | 10 ++ src/plugins/bundled-plugin-scan.ts | 1 + 8 files changed, 80 insertions(+), 48 deletions(-) create mode 100644 extensions/telegram/runtime-setter-api.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b643466bed..e6709696d9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Docs: https://docs.openclaw.ai - Ollama/onboard: populate the cloud-only model list from `ollama.com/api/tags` so `openclaw onboard` reflects the live cloud catalog instead of a static three-model seed; cap the discovered list at 500 and fall back to the previous hardcoded suggestions when ollama.com is unreachable or returns no models. (#68463) Thanks @BruceMacD. - Matrix/startup: narrow Matrix runtime registration and defer setup/doctor surfaces so cold plugin registration spends about 1.8s less in `setChannelRuntime`. (#69782) Thanks @gumadeiras. - QQBot: extract a self-contained `engine/` architecture with QR-code onboarding, native approval handling via `/bot-approve`, per-account isolated resource stacks and multi-account logger, credential backup/restore, shared `~/.openclaw/media` payload root, and unified API/bridge/gateway modules. (#67960) Thanks @cxyhhhhh. +- Telegram/plugin startup: load Telegram's bundled runtime setter through a narrow sidecar and let built sidecars use native loading before falling back to jiti, cutting the measured setup-runtime registration path by about 14s while preserving runtime API compatibility. (#69786) thanks @gumadeiras. ### Fixes diff --git a/extensions/telegram/index.ts b/extensions/telegram/index.ts index 76ae67a07eb..dc38368e08a 100644 --- a/extensions/telegram/index.ts +++ b/extensions/telegram/index.ts @@ -14,7 +14,7 @@ export default defineBundledChannelEntry({ exportName: "channelSecrets", }, runtime: { - specifier: "./runtime-api.js", + specifier: "./runtime-setter-api.js", exportName: "setTelegramRuntime", }, accountInspect: { diff --git a/extensions/telegram/runtime-setter-api.ts b/extensions/telegram/runtime-setter-api.ts new file mode 100644 index 00000000000..9354e4a7a6b --- /dev/null +++ b/extensions/telegram/runtime-setter-api.ts @@ -0,0 +1,3 @@ +// Keep bundled registration fast: the runtime setter is needed during plugin +// bootstrap, but the broad runtime-api barrel is only for compatibility callers. +export { setTelegramRuntime } from "./src/runtime.js"; diff --git a/scripts/lib/bundled-runtime-sidecar-paths.json b/scripts/lib/bundled-runtime-sidecar-paths.json index 9d4e3264a5a..22df906514a 100644 --- a/scripts/lib/bundled-runtime-sidecar-paths.json +++ b/scripts/lib/bundled-runtime-sidecar-paths.json @@ -15,6 +15,7 @@ "dist/extensions/lobster/runtime-api.js", "dist/extensions/matrix/helper-api.js", "dist/extensions/matrix/runtime-api.js", + "dist/extensions/matrix/runtime-setter-api.js", "dist/extensions/matrix/thread-bindings-runtime.js", "dist/extensions/mattermost/runtime-api.js", "dist/extensions/memory-core/runtime-api.js", @@ -27,6 +28,7 @@ "dist/extensions/signal/runtime-api.js", "dist/extensions/slack/runtime-api.js", "dist/extensions/telegram/runtime-api.js", + "dist/extensions/telegram/runtime-setter-api.js", "dist/extensions/tlon/runtime-api.js", "dist/extensions/twitch/runtime-api.js", "dist/extensions/voice-call/runtime-api.js", diff --git a/src/plugin-sdk/channel-entry-contract.test.ts b/src/plugin-sdk/channel-entry-contract.test.ts index 45b43fd797d..ea842f4f109 100644 --- a/src/plugin-sdk/channel-entry-contract.test.ts +++ b/src/plugin-sdk/channel-entry-contract.test.ts @@ -17,6 +17,50 @@ afterEach(() => { vi.unstubAllEnvs(); }); +async function expectBuiltArtifactNodeRequireFastPath( + scope: string, + artifactRoot = "dist", +): Promise { + vi.stubEnv("OPENCLAW_PLUGIN_LOAD_PROFILE", "1"); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + + try { + const channelEntryContract = await importFreshModule< + typeof import("./channel-entry-contract.js") + >(import.meta.url, `./channel-entry-contract.js?scope=${scope}`); + + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-entry-contract-")); + tempDirs.push(tempRoot); + + const pluginRoot = path.join(tempRoot, artifactRoot, "extensions", "telegram"); + fs.mkdirSync(pluginRoot, { recursive: true }); + + const importerPath = path.join(pluginRoot, "index.js"); + const sidecarPath = path.join(pluginRoot, "fast-path-sidecar.js"); + fs.writeFileSync(importerPath, "export default {};\n", "utf8"); + // CommonJS so `nodeRequire` succeeds without falling back to jiti. + fs.writeFileSync(sidecarPath, "module.exports = { sentinel: 7 };\n", "utf8"); + + expect( + channelEntryContract.loadBundledEntryExportSync(pathToFileURL(importerPath).href, { + specifier: "./fast-path-sidecar.js", + exportName: "sentinel", + }), + ).toBe(7); + + const profileLine = errorSpy.mock.calls + .map((args) => String(args[0] ?? "")) + .find((line) => line.startsWith("[plugin-load-profile] phase=bundled-entry-module-load")); + expect(profileLine, "expected a bundled-entry-module-load profile line").toBeDefined(); + expect(profileLine).toContain("getJitiMs=0.0"); + expect(profileLine).toContain("jitiCallMs=0.0"); + expect(profileLine).not.toMatch(/getJitiMs=-/); + expect(profileLine).not.toMatch(/jitiCallMs=-/); + } finally { + errorSpy.mockRestore(); + } +} + describe("loadBundledEntryExportSync", () => { it("includes importer and resolved path context when a bundled sidecar is missing", () => { const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-entry-contract-")); @@ -133,51 +177,16 @@ describe("loadBundledEntryExportSync", () => { }); }); - it("emits zero jiti sub-step timings on the Win32 nodeRequire fast-path", async () => { - // The Win32 fast-path goes through `nodeRequire` directly and never + it("emits zero jiti sub-step timings on the built-artifact nodeRequire fast-path", async () => { + // The built-artifact fast-path goes through `nodeRequire` directly and never // touches jiti. The plugin-load-profile line must reflect that with // `getJitiMs=0.0 jitiCallMs=0.0` rather than negative or full-elapsed // values that would mis-attribute nodeRequire time to jiti sub-steps. - const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32"); - vi.stubEnv("OPENCLAW_PLUGIN_LOAD_PROFILE", "1"); - const errorSpy = vi.spyOn(console, "error").mockImplementation(() => undefined); + await expectBuiltArtifactNodeRequireFastPath("built-artifact-profile-fast-path"); + }); - try { - const channelEntryContract = await importFreshModule< - typeof import("./channel-entry-contract.js") - >(import.meta.url, "./channel-entry-contract.js?scope=win32-profile-fast-path"); - - const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-channel-entry-contract-")); - tempDirs.push(tempRoot); - - const pluginRoot = path.join(tempRoot, "dist", "extensions", "telegram"); - fs.mkdirSync(pluginRoot, { recursive: true }); - - const importerPath = path.join(pluginRoot, "index.js"); - const sidecarPath = path.join(pluginRoot, "fast-path-sidecar.js"); - fs.writeFileSync(importerPath, "export default {};\n", "utf8"); - // CommonJS so `nodeRequire` succeeds without falling back to jiti. - fs.writeFileSync(sidecarPath, "module.exports = { sentinel: 7 };\n", "utf8"); - - expect( - channelEntryContract.loadBundledEntryExportSync(pathToFileURL(importerPath).href, { - specifier: "./fast-path-sidecar.js", - exportName: "sentinel", - }), - ).toBe(7); - - const profileLine = errorSpy.mock.calls - .map((args) => String(args[0] ?? "")) - .find((line) => line.startsWith("[plugin-load-profile] phase=bundled-entry-module-load")); - expect(profileLine, "expected a bundled-entry-module-load profile line").toBeDefined(); - expect(profileLine).toContain("getJitiMs=0.0"); - expect(profileLine).toContain("jitiCallMs=0.0"); - expect(profileLine).not.toMatch(/getJitiMs=-/); - expect(profileLine).not.toMatch(/jitiCallMs=-/); - } finally { - errorSpy.mockRestore(); - platformSpy.mockRestore(); - } + it("keeps dist-runtime built sidecar loads on the nodeRequire fast-path", async () => { + await expectBuiltArtifactNodeRequireFastPath("dist-runtime-profile-fast-path", "dist-runtime"); }); it("can disable source-tree fallback for dist bundled entry checks", () => { diff --git a/src/plugin-sdk/channel-entry-contract.ts b/src/plugin-sdk/channel-entry-contract.ts index a5da17065e4..f4161b2834c 100644 --- a/src/plugin-sdk/channel-entry-contract.ts +++ b/src/plugin-sdk/channel-entry-contract.ts @@ -316,6 +316,16 @@ function getJiti(modulePath: string) { }); } +function canTryNodeRequireBuiltModule(modulePath: string): boolean { + const isBuiltBundledArtifact = + modulePath.includes(`${path.sep}dist${path.sep}`) || + modulePath.includes(`${path.sep}dist-runtime${path.sep}`); + return ( + isBuiltBundledArtifact && + [".js", ".mjs", ".cjs"].includes(normalizeLowercaseStringOrEmpty(path.extname(modulePath))) + ); +} + function loadBundledEntryModuleSync(importMetaUrl: string, specifier: string): unknown { const modulePath = resolveBundledEntryModulePath(importMetaUrl, specifier); const cached = loadedModuleExports.get(modulePath); @@ -326,11 +336,7 @@ function loadBundledEntryModuleSync(importMetaUrl: string, specifier: string): u const profile = shouldProfilePluginLoader(); const loadStartMs = profile ? performance.now() : 0; let getJitiEndMs = 0; - if ( - process.platform === "win32" && - modulePath.includes(`${path.sep}dist${path.sep}`) && - [".js", ".mjs", ".cjs"].includes(normalizeLowercaseStringOrEmpty(path.extname(modulePath))) - ) { + if (canTryNodeRequireBuiltModule(modulePath)) { try { loaded = nodeRequire(modulePath); } catch { @@ -355,7 +361,7 @@ function loadBundledEntryModuleSync(importMetaUrl: string, specifier: string): u pluginId: "(bundled-entry)", source: modulePath, elapsedMs: endMs - loadStartMs, - // When the Win32 fast-path resolves the module via `nodeRequire`, + // When the built-artifact fast-path resolves the module via `nodeRequire`, // `getJitiEndMs` stays `0` because the `catch` block (the only place // it gets stamped) never runs. Reporting `getJitiMs` / // `jitiCallMs` as `0` for that path keeps the breakdown honest: diff --git a/src/plugins/bundled-plugin-metadata.test.ts b/src/plugins/bundled-plugin-metadata.test.ts index 8fbcfdc6ed5..e52d1de6140 100644 --- a/src/plugins/bundled-plugin-metadata.test.ts +++ b/src/plugins/bundled-plugin-metadata.test.ts @@ -165,6 +165,16 @@ describe("bundled plugin metadata", () => { }); }); + it("keeps Telegram's narrow runtime setter on the bundled runtime sidecar surface", () => { + const telegram = listRepoBundledPluginMetadata().find((entry) => entry.dirName === "telegram"); + expectArtifactPresence(telegram?.publicSurfaceArtifacts, { + contains: ["runtime-setter-api.js"], + }); + expectArtifactPresence(telegram?.runtimeSidecarArtifacts, { + contains: ["runtime-setter-api.js"], + }); + }); + it("loads tlon channel config metadata from the lightweight schema surface", () => { expect(collectRepoBundledChannelConfigsForTest("tlon")?.tlon).toEqual( expect.objectContaining({ diff --git a/src/plugins/bundled-plugin-scan.ts b/src/plugins/bundled-plugin-scan.ts index 73dfd77275f..42e40729979 100644 --- a/src/plugins/bundled-plugin-scan.ts +++ b/src/plugins/bundled-plugin-scan.ts @@ -8,6 +8,7 @@ const RUNTIME_SIDECAR_ARTIFACTS = new Set([ "helper-api.js", "light-runtime-api.js", "runtime-api.js", + "runtime-setter-api.js", "thread-bindings-runtime.js", ]);