mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 09:10:45 +00:00
perf(gateway): trim startup imports and sentinel checks
This commit is contained in:
@@ -580,7 +580,10 @@ function parseStartupTraceMetrics(raw: string): Array<{ key: string; value: numb
|
||||
const value = Number(metricMatch[2]);
|
||||
if (
|
||||
!Number.isFinite(value) ||
|
||||
(key !== "eventLoopMax" && !key.endsWith("Ms") && !key.endsWith("Mb"))
|
||||
(key !== "eventLoopMax" &&
|
||||
!key.endsWith("Ms") &&
|
||||
!key.endsWith("Mb") &&
|
||||
!key.endsWith("Count"))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { normalizePluginsConfig } from "../plugins/config-state.js";
|
||||
import { clearActivatedPluginRuntimeState, loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { loadPluginLookUpTable, type PluginLookUpTable } from "../plugins/plugin-lookup-table.js";
|
||||
import { getPluginModuleLoaderStats } from "../plugins/plugin-module-loader-cache.js";
|
||||
import { createEmptyPluginRegistry } from "../plugins/registry-empty.js";
|
||||
import { setActivePluginRegistry } from "../plugins/runtime.js";
|
||||
import { getPluginRuntimeGatewayRequestScope } from "../plugins/runtime/gateway-request-scope.js";
|
||||
@@ -599,6 +600,7 @@ export function loadGatewayPlugins(params: {
|
||||
};
|
||||
}
|
||||
const beforeLoad = performance.now();
|
||||
const loaderStatsBefore = getPluginModuleLoaderStats();
|
||||
const pluginRegistry = loadOpenClawPlugins({
|
||||
config: resolvedConfig,
|
||||
activationSourceConfig: params.activationSourceConfig ?? params.cfg,
|
||||
@@ -624,6 +626,7 @@ export function loadGatewayPlugins(params: {
|
||||
: {}),
|
||||
});
|
||||
const loadMs = performance.now() - beforeLoad;
|
||||
const loaderStatsAfter = getPluginModuleLoaderStats();
|
||||
const pluginMethods = Object.keys(pluginRegistry.gatewayHandlers);
|
||||
const gatewayMethods = Array.from(new Set([...params.baseMethods, ...pluginMethods]));
|
||||
params.startupTrace?.detail("plugins.gateway-load", [
|
||||
@@ -633,6 +636,24 @@ export function loadGatewayPlugins(params: {
|
||||
["loadMs", loadMs],
|
||||
["pluginIds", String(pluginIds.length)],
|
||||
["gatewayHandlers", String(pluginMethods.length)],
|
||||
["loaderCallsCount", loaderStatsAfter.calls - loaderStatsBefore.calls],
|
||||
["loaderNativeHitsCount", loaderStatsAfter.nativeHits - loaderStatsBefore.nativeHits],
|
||||
["loaderNativeMissesCount", loaderStatsAfter.nativeMisses - loaderStatsBefore.nativeMisses],
|
||||
[
|
||||
"loaderSourceTransformForcedCount",
|
||||
loaderStatsAfter.sourceTransformForced - loaderStatsBefore.sourceTransformForced,
|
||||
],
|
||||
[
|
||||
"loaderSourceTransformFallbacksCount",
|
||||
loaderStatsAfter.sourceTransformFallbacks - loaderStatsBefore.sourceTransformFallbacks,
|
||||
],
|
||||
[
|
||||
"loaderTopSourceTransformTargets",
|
||||
loaderStatsAfter.topSourceTransformTargets
|
||||
.slice(0, 3)
|
||||
.map((entry) => `${entry.count}:${entry.target}`)
|
||||
.join(","),
|
||||
],
|
||||
]);
|
||||
return { pluginRegistry, gatewayMethods };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type {
|
||||
PluginHookGatewayContext,
|
||||
@@ -21,7 +24,9 @@ const hoisted = vi.hoisted(() => {
|
||||
const scheduleSubagentOrphanRecovery = vi.fn();
|
||||
const shouldWakeFromRestartSentinel = vi.fn(() => false);
|
||||
const scheduleRestartSentinelWake = vi.fn();
|
||||
const refreshLatestUpdateRestartSentinel = vi.fn(async () => null);
|
||||
const refreshLatestUpdateRestartSentinel = vi.fn<
|
||||
typeof import("./server-restart-sentinel.js").refreshLatestUpdateRestartSentinel
|
||||
>(async () => null);
|
||||
const getAcpRuntimeBackend = vi.fn<(id?: string) => unknown>(() => null);
|
||||
const reconcilePendingSessionIdentities = vi.fn(async () => ({
|
||||
checked: 0,
|
||||
@@ -281,6 +286,61 @@ describe("startGatewayPostAttachRuntime", () => {
|
||||
expect(events).toEqual(["sidecars", "returned", "sentinel"]);
|
||||
});
|
||||
|
||||
it("skips heavy restart sentinel refresh when no sentinel file exists", async () => {
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-no-sentinel-"));
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
|
||||
const result = await __testing.refreshLatestUpdateRestartSentinelIfPresent();
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(hoisted.refreshLatestUpdateRestartSentinel).not.toHaveBeenCalled();
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("refreshes the restart sentinel when the sentinel file exists", async () => {
|
||||
const stateDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sentinel-"));
|
||||
fs.writeFileSync(path.join(stateDir, "restart-sentinel.json"), "{}\n");
|
||||
vi.stubEnv("OPENCLAW_STATE_DIR", stateDir);
|
||||
const sentinel = { kind: "update", status: "ok", ts: 1 } as const;
|
||||
hoisted.refreshLatestUpdateRestartSentinel.mockResolvedValue(sentinel);
|
||||
|
||||
const result = await __testing.refreshLatestUpdateRestartSentinelIfPresent();
|
||||
|
||||
expect(result).toBe(sentinel);
|
||||
expect(hoisted.refreshLatestUpdateRestartSentinel).toHaveBeenCalledOnce();
|
||||
fs.rmSync(stateDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("expands tilde-based restart sentinel state paths", () => {
|
||||
const osHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-home-"));
|
||||
try {
|
||||
const openclawHome = path.join(osHome, "openclaw-home");
|
||||
const stateDirFromHome = path.join(openclawHome, ".openclaw");
|
||||
fs.mkdirSync(stateDirFromHome, { recursive: true });
|
||||
fs.writeFileSync(path.join(stateDirFromHome, "restart-sentinel.json"), "{}\n");
|
||||
|
||||
expect(
|
||||
__testing.hasRestartSentinelFileFast({
|
||||
HOME: osHome,
|
||||
OPENCLAW_HOME: "~/openclaw-home",
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe(true);
|
||||
|
||||
const backslashStateDir = path.resolve(`${osHome}\\openclaw-state`);
|
||||
fs.mkdirSync(backslashStateDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(backslashStateDir, "restart-sentinel.json"), "{}\n");
|
||||
|
||||
expect(
|
||||
__testing.hasRestartSentinelFileFast({
|
||||
HOME: osHome,
|
||||
OPENCLAW_STATE_DIR: "~\\openclaw-state",
|
||||
} as NodeJS.ProcessEnv),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
fs.rmSync(osHome, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("loads deferred startup plugins before channel sidecars", async () => {
|
||||
const events: string[] = [];
|
||||
const loadedPluginRegistry = {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { setTimeout as sleep } from "node:timers/promises";
|
||||
import type { CliDeps } from "../cli/deps.types.js";
|
||||
import type { GatewayTailscaleMode } from "../config/types.gateway.js";
|
||||
@@ -8,6 +11,7 @@ import type { scheduleGatewayUpdateCheck } from "../infra/update-startup.js";
|
||||
import type { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
|
||||
import type { PluginHookGatewayCronService } from "../plugins/hook-types.js";
|
||||
import type { loadOpenClawPlugins } from "../plugins/loader.js";
|
||||
import { getPluginModuleLoaderStats } from "../plugins/plugin-module-loader-cache.js";
|
||||
import type { PluginRegistry } from "../plugins/registry.js";
|
||||
import type { PluginServicesHandle } from "../plugins/services.js";
|
||||
import {
|
||||
@@ -26,10 +30,12 @@ const PRIMARY_MODEL_PREWARM_TIMEOUT_MS = 5_000;
|
||||
const STARTUP_PROVIDER_DISCOVERY_TIMEOUT_MS = 5_000;
|
||||
const SKIP_STARTUP_MODEL_PREWARM_ENV = "OPENCLAW_SKIP_STARTUP_MODEL_PREWARM";
|
||||
const QMD_STARTUP_IDLE_DELAY_MS = 120_000;
|
||||
const RESTART_SENTINEL_FILENAME = "restart-sentinel.json";
|
||||
|
||||
type Awaitable<T> = T | Promise<T>;
|
||||
|
||||
type GatewayStartupTrace = {
|
||||
detail: (name: string, metrics: ReadonlyArray<readonly [string, number | string]>) => void;
|
||||
mark: (name: string) => void;
|
||||
measure: <T>(name: string, run: () => Awaitable<T>) => Promise<T>;
|
||||
};
|
||||
@@ -124,6 +130,61 @@ function schedulePostAttachUpdateSentinelRefresh(params: {
|
||||
handle.unref?.();
|
||||
}
|
||||
|
||||
function resolveRestartSentinelPathFast(env: NodeJS.ProcessEnv = process.env): string {
|
||||
const normalizePathEnv = (value: string | undefined) => {
|
||||
const trimmed = value?.trim();
|
||||
return trimmed && trimmed !== "undefined" && trimmed !== "null" ? trimmed : undefined;
|
||||
};
|
||||
const resolveRawOsHome = () => normalizePathEnv(env.HOME) ?? normalizePathEnv(env.USERPROFILE);
|
||||
const expandHomePrefix = (input: string, home: string) => input.replace(/^~(?=$|[\\/])/, home);
|
||||
const resolveHome = () => {
|
||||
const explicitHome = normalizePathEnv(env.OPENCLAW_HOME);
|
||||
if (explicitHome) {
|
||||
const osHome = resolveRawOsHome() ?? os.homedir();
|
||||
return path.resolve(expandHomePrefix(explicitHome, osHome));
|
||||
}
|
||||
return path.resolve(resolveRawOsHome() ?? os.homedir());
|
||||
};
|
||||
const resolveUserPath = (input: string) => {
|
||||
const trimmed = input.trim();
|
||||
if (trimmed.startsWith("~")) {
|
||||
return path.resolve(expandHomePrefix(trimmed, resolveHome()));
|
||||
}
|
||||
return path.resolve(trimmed);
|
||||
};
|
||||
const override = normalizePathEnv(env.OPENCLAW_STATE_DIR);
|
||||
if (override) {
|
||||
return path.join(resolveUserPath(override), RESTART_SENTINEL_FILENAME);
|
||||
}
|
||||
const home = resolveHome();
|
||||
const newStateDir = path.join(home, ".openclaw");
|
||||
if (env.OPENCLAW_TEST_FAST === "1" || fs.existsSync(newStateDir)) {
|
||||
return path.join(newStateDir, RESTART_SENTINEL_FILENAME);
|
||||
}
|
||||
const legacyStateDir = path.join(home, ".clawdbot");
|
||||
if (fs.existsSync(legacyStateDir)) {
|
||||
return path.join(legacyStateDir, RESTART_SENTINEL_FILENAME);
|
||||
}
|
||||
return path.join(newStateDir, RESTART_SENTINEL_FILENAME);
|
||||
}
|
||||
|
||||
function hasRestartSentinelFileFast(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
try {
|
||||
return fs.existsSync(resolveRestartSentinelPathFast(env));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshLatestUpdateRestartSentinelIfPresent(): Promise<Awaited<
|
||||
ReturnType<typeof refreshLatestUpdateRestartSentinel>
|
||||
> | null> {
|
||||
if (!hasRestartSentinelFileFast()) {
|
||||
return null;
|
||||
}
|
||||
return await (await import("./server-restart-sentinel.js")).refreshLatestUpdateRestartSentinel();
|
||||
}
|
||||
|
||||
function hasGatewayStartHooks(pluginRegistry: ReturnType<typeof loadOpenClawPlugins>): boolean {
|
||||
return pluginRegistry.typedHooks.some((hook) => hook.hookName === "gateway_start");
|
||||
}
|
||||
@@ -507,8 +568,7 @@ export async function startGatewaySidecars(params: {
|
||||
if (!shouldCheckRestartSentinel()) {
|
||||
return;
|
||||
}
|
||||
const { hasRestartSentinel } = await import("../infra/restart-sentinel.js");
|
||||
if (!(await hasRestartSentinel())) {
|
||||
if (!hasRestartSentinelFileFast()) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
@@ -556,8 +616,7 @@ const defaultGatewayPostAttachRuntimeDeps: GatewayPostAttachRuntimeDeps = {
|
||||
(await import("../plugins/hook-runner-global.js")).getGlobalHookRunner(),
|
||||
logGatewayStartup: async (params) =>
|
||||
(await import("./server-startup-log.js")).logGatewayStartup(params),
|
||||
refreshLatestUpdateRestartSentinel: async () =>
|
||||
(await import("./server-restart-sentinel.js")).refreshLatestUpdateRestartSentinel(),
|
||||
refreshLatestUpdateRestartSentinel: refreshLatestUpdateRestartSentinelIfPresent,
|
||||
scheduleGatewayUpdateCheck: async (...args) =>
|
||||
(await import("../infra/update-startup.js")).scheduleGatewayUpdateCheck(...args),
|
||||
startGatewaySidecars,
|
||||
@@ -676,6 +735,7 @@ export async function startGatewayPostAttachRuntime(
|
||||
? Promise.resolve({ pluginServices: null, pluginRegistry })
|
||||
: new Promise<void>((resolve) => setImmediate(resolve)).then(async () => {
|
||||
params.log.info("starting channels and sidecars...");
|
||||
const loaderStatsBefore = getPluginModuleLoaderStats();
|
||||
const result = await measureStartup(params.startupTrace, "sidecars.total", () =>
|
||||
runtimeDeps.startGatewaySidecars({
|
||||
cfg: params.gatewayPluginConfigAtStart,
|
||||
@@ -689,6 +749,20 @@ export async function startGatewayPostAttachRuntime(
|
||||
startupTrace: params.startupTrace,
|
||||
}),
|
||||
);
|
||||
const loaderStatsAfter = getPluginModuleLoaderStats();
|
||||
params.startupTrace?.detail("sidecars.plugin-loader", [
|
||||
["callsCount", loaderStatsAfter.calls - loaderStatsBefore.calls],
|
||||
["nativeHitsCount", loaderStatsAfter.nativeHits - loaderStatsBefore.nativeHits],
|
||||
["nativeMissesCount", loaderStatsAfter.nativeMisses - loaderStatsBefore.nativeMisses],
|
||||
[
|
||||
"sourceTransformForcedCount",
|
||||
loaderStatsAfter.sourceTransformForced - loaderStatsBefore.sourceTransformForced,
|
||||
],
|
||||
[
|
||||
"sourceTransformFallbacksCount",
|
||||
loaderStatsAfter.sourceTransformFallbacks - loaderStatsBefore.sourceTransformFallbacks,
|
||||
],
|
||||
]);
|
||||
for (const method of STARTUP_UNAVAILABLE_GATEWAY_METHODS) {
|
||||
params.unavailableGatewayMethods.delete(method);
|
||||
}
|
||||
@@ -758,8 +832,10 @@ export async function startGatewayPostAttachRuntime(
|
||||
}
|
||||
|
||||
export const __testing = {
|
||||
hasRestartSentinelFileFast,
|
||||
prewarmConfiguredPrimaryModel,
|
||||
prewarmConfiguredPrimaryModelWithTimeout,
|
||||
refreshLatestUpdateRestartSentinelIfPresent,
|
||||
resolveGatewayMemoryStartupPolicy,
|
||||
schedulePrimaryModelPrewarm,
|
||||
shouldSkipStartupModelPrewarm,
|
||||
|
||||
@@ -60,28 +60,15 @@ import {
|
||||
listChannelPluginConfigTargetIds,
|
||||
pluginConfigTargetsChanged,
|
||||
} from "./plugin-channel-reload-targets.js";
|
||||
import { createGatewayAuxHandlers } from "./server-aux-handlers.js";
|
||||
import { createChannelManager } from "./server-channels.js";
|
||||
import { resolveGatewayControlUiRootState } from "./server-control-ui-root.js";
|
||||
import { createLazyGatewayCronState } from "./server-cron-lazy.js";
|
||||
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
|
||||
import { createGatewayServerLiveState, type GatewayServerLiveState } from "./server-live-state.js";
|
||||
import { GATEWAY_EVENTS } from "./server-methods-list.js";
|
||||
import type { GatewayRequestHandlers } from "./server-methods/types.js";
|
||||
import { loadGatewayModelCatalog } from "./server-model-catalog.js";
|
||||
import { bootstrapGatewayNetworkRuntime } from "./server-network-runtime.js";
|
||||
import { createGatewayNodeSessionRuntime } from "./server-node-session-runtime.js";
|
||||
import { setFallbackGatewayContextResolver } from "./server-plugins.js";
|
||||
import type { GatewayPluginReloadResult } from "./server-reload-handlers.js";
|
||||
import { createGatewayRequestContext } from "./server-request-context.js";
|
||||
import { resolveGatewayRuntimeConfig } from "./server-runtime-config.js";
|
||||
import {
|
||||
activateGatewayScheduledServices,
|
||||
startGatewayCronWithLogging,
|
||||
startGatewayRuntimeServices,
|
||||
} from "./server-runtime-services.js";
|
||||
import { createGatewayRuntimeState } from "./server-runtime-state.js";
|
||||
import { startGatewayEventSubscriptions } from "./server-runtime-subscriptions.js";
|
||||
import { resolveSessionKeyForRun } from "./server-session-key.js";
|
||||
import {
|
||||
enforceSharedGatewaySessionGenerationForConfigWrite,
|
||||
@@ -104,7 +91,6 @@ import {
|
||||
startGatewayPostAttachRuntime,
|
||||
} from "./server-startup.js";
|
||||
import { createWizardSessionTracker } from "./server-wizard-sessions.js";
|
||||
import { attachGatewayWsHandlers } from "./server-ws-runtime.js";
|
||||
import { createGatewayEventLoopHealthMonitor } from "./server/event-loop-health.js";
|
||||
import {
|
||||
getHealthCache,
|
||||
@@ -172,6 +158,17 @@ function loadGatewayCloseModule(): Promise<typeof import("./server-close.js")> {
|
||||
return gatewayCloseModulePromise;
|
||||
}
|
||||
|
||||
type LoadGatewayModelCatalog = typeof import("./server-model-catalog.js").loadGatewayModelCatalog;
|
||||
|
||||
let gatewayModelCatalogModulePromise: Promise<typeof import("./server-model-catalog.js")> | null =
|
||||
null;
|
||||
|
||||
const loadGatewayModelCatalog: LoadGatewayModelCatalog = async (...args) => {
|
||||
gatewayModelCatalogModulePromise ??= import("./server-model-catalog.js");
|
||||
const mod = await gatewayModelCatalogModulePromise;
|
||||
return mod.loadGatewayModelCatalog(...args);
|
||||
};
|
||||
|
||||
const logHealth = log.child("health");
|
||||
const logCron = log.child("cron");
|
||||
const logReload = log.child("reload");
|
||||
@@ -490,6 +487,7 @@ export async function startGatewayServer(
|
||||
port = 18789,
|
||||
opts: GatewayServerOptions = {},
|
||||
): Promise<GatewayServer> {
|
||||
const { bootstrapGatewayNetworkRuntime } = await import("./server-network-runtime.js");
|
||||
bootstrapGatewayNetworkRuntime();
|
||||
|
||||
const minimalTestGateway =
|
||||
@@ -660,8 +658,9 @@ export async function startGatewayServer(
|
||||
...listChannelPlugins().flatMap((plugin) => plugin.gatewayMethods ?? []),
|
||||
]),
|
||||
);
|
||||
const runtimeConfig = await startupTrace.measure("runtime.config", () =>
|
||||
resolveGatewayRuntimeConfig({
|
||||
const runtimeConfig = await startupTrace.measure("runtime.config", async () => {
|
||||
const { resolveGatewayRuntimeConfig } = await import("./server-runtime-config.js");
|
||||
return resolveGatewayRuntimeConfig({
|
||||
cfg: cfgAtStart,
|
||||
port,
|
||||
bind: opts.bind,
|
||||
@@ -671,8 +670,8 @@ export async function startGatewayServer(
|
||||
openResponsesEnabled: opts.openResponsesEnabled,
|
||||
auth: opts.auth,
|
||||
tailscale: opts.tailscale,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
const {
|
||||
bindHost,
|
||||
controlUiEnabled,
|
||||
@@ -755,6 +754,7 @@ export async function startGatewayServer(
|
||||
const readinessEventLoopHealth = createGatewayEventLoopHealthMonitor();
|
||||
let startupSidecarsReady = minimalTestGateway;
|
||||
let startupPendingReason = "startup-sidecars";
|
||||
const { createChannelManager } = await import("./server-channels.js");
|
||||
const channelManager = createChannelManager({
|
||||
getRuntimeConfig: () =>
|
||||
applyPluginAutoEnable({
|
||||
@@ -832,6 +832,7 @@ export async function startGatewayServer(
|
||||
getReadiness,
|
||||
}),
|
||||
);
|
||||
const { createGatewayNodeSessionRuntime } = await import("./server-node-session-runtime.js");
|
||||
const {
|
||||
nodeRegistry,
|
||||
nodePresenceTimers,
|
||||
@@ -980,6 +981,10 @@ export async function startGatewayServer(
|
||||
getActiveTaskCount = earlyRuntime.getActiveTaskCount;
|
||||
runtimeState.skillsChangeUnsub = earlyRuntime.skillsChangeUnsub;
|
||||
|
||||
const [{ startGatewayEventSubscriptions }, gatewayRuntimeServices] = await Promise.all([
|
||||
import("./server-runtime-subscriptions.js"),
|
||||
import("./server-runtime-services.js"),
|
||||
]);
|
||||
Object.assign(
|
||||
runtimeState,
|
||||
startGatewayEventSubscriptions({
|
||||
@@ -999,7 +1004,7 @@ export async function startGatewayServer(
|
||||
|
||||
Object.assign(
|
||||
runtimeState,
|
||||
startGatewayRuntimeServices({
|
||||
gatewayRuntimeServices.startGatewayRuntimeServices({
|
||||
minimalTestGateway,
|
||||
cfgAtStart,
|
||||
channelManager,
|
||||
@@ -1007,6 +1012,7 @@ export async function startGatewayServer(
|
||||
}),
|
||||
);
|
||||
|
||||
const { createGatewayAuxHandlers } = await import("./server-aux-handlers.js");
|
||||
const { execApprovalManager, pluginApprovalManager, extraHandlers } = createGatewayAuxHandlers({
|
||||
log,
|
||||
activateRuntimeSecrets,
|
||||
@@ -1179,6 +1185,7 @@ export async function startGatewayServer(
|
||||
const unavailableGatewayMethods = new Set<string>(
|
||||
minimalTestGateway ? [] : STARTUP_UNAVAILABLE_GATEWAY_METHODS,
|
||||
);
|
||||
const { createGatewayRequestContext } = await import("./server-request-context.js");
|
||||
const gatewayRequestContext = createGatewayRequestContext({
|
||||
deps,
|
||||
runtimeState,
|
||||
@@ -1272,6 +1279,7 @@ export async function startGatewayServer(
|
||||
}
|
||||
}
|
||||
|
||||
const { attachGatewayWsHandlers } = await import("./server-ws-runtime.js");
|
||||
attachGatewayWsHandlers({
|
||||
wss,
|
||||
clients,
|
||||
@@ -1311,7 +1319,7 @@ export async function startGatewayServer(
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const activated = activateGatewayScheduledServices({
|
||||
const activated = gatewayRuntimeServices.activateGatewayScheduledServices({
|
||||
minimalTestGateway,
|
||||
cfgAtStart,
|
||||
deps,
|
||||
@@ -1445,7 +1453,7 @@ export async function startGatewayServer(
|
||||
runtimeState.dedupeCleanup = maintenance.dedupeCleanup;
|
||||
runtimeState.mediaCleanup = maintenance.mediaCleanup;
|
||||
}
|
||||
startGatewayCronWithLogging({
|
||||
gatewayRuntimeServices.startGatewayCronWithLogging({
|
||||
cron: runtimeState.cronState.cron,
|
||||
logCron,
|
||||
});
|
||||
|
||||
@@ -371,7 +371,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
p.endsWith(".js") || p.endsWith(".mjs") || p.endsWith(".cjs"),
|
||||
tryNativeRequireJavaScriptModule: nativeStub,
|
||||
}));
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-fastpath");
|
||||
|
||||
@@ -393,6 +393,13 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
expect(nativeStub).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js", {
|
||||
allowWindows: true,
|
||||
});
|
||||
expect(getPluginModuleLoaderStats()).toMatchObject({
|
||||
calls: 1,
|
||||
nativeHits: 1,
|
||||
nativeMisses: 0,
|
||||
sourceTransformFallbacks: 0,
|
||||
sourceTransformForced: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to source transform when the native-require helper declines", async () => {
|
||||
@@ -403,7 +410,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: () => ({ ok: false }),
|
||||
}));
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-fallback");
|
||||
|
||||
@@ -418,6 +425,14 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
const result = loader("/repo/dist/extensions/demo/api.js") as { fromSourceTransform: boolean };
|
||||
expect(result.fromSourceTransform).toBe(true);
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
expect(getPluginModuleLoaderStats()).toMatchObject({
|
||||
calls: 1,
|
||||
nativeHits: 0,
|
||||
nativeMisses: 1,
|
||||
sourceTransformFallbacks: 1,
|
||||
sourceTransformForced: 0,
|
||||
topSourceTransformTargets: [{ target: "/repo/dist/extensions/demo/api.js", count: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes Windows absolute paths before creating and calling the source transformer", async () => {
|
||||
@@ -462,7 +477,7 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
isJavaScriptModulePath: () => true,
|
||||
tryNativeRequireJavaScriptModule: nativeStub,
|
||||
}));
|
||||
const { getCachedPluginModuleLoader } = await importFreshModule<
|
||||
const { getCachedPluginModuleLoader, getPluginModuleLoaderStats } = await importFreshModule<
|
||||
typeof import("./plugin-module-loader-cache.js")
|
||||
>(import.meta.url, "./plugin-module-loader-cache.js?scope=native-require-opt-out");
|
||||
|
||||
@@ -482,6 +497,14 @@ describe("getCachedPluginModuleLoader", () => {
|
||||
// so its alias rewrites still apply; native require must not be consulted.
|
||||
expect(nativeStub).not.toHaveBeenCalled();
|
||||
expect(fromSourceTransformer).toHaveBeenCalledWith("/repo/dist/extensions/demo/api.js");
|
||||
expect(getPluginModuleLoaderStats()).toMatchObject({
|
||||
calls: 1,
|
||||
nativeHits: 0,
|
||||
nativeMisses: 0,
|
||||
sourceTransformFallbacks: 0,
|
||||
sourceTransformForced: 1,
|
||||
topSourceTransformTargets: [{ target: "/repo/dist/extensions/demo/api.js", count: 1 }],
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes Windows absolute paths when native loading is disabled", async () => {
|
||||
|
||||
@@ -34,8 +34,67 @@ export type PluginModuleLoaderCacheEntry = {
|
||||
cacheKey: string;
|
||||
scopedCacheKey: string;
|
||||
};
|
||||
export type PluginModuleLoaderStatsSnapshot = {
|
||||
calls: number;
|
||||
nativeHits: number;
|
||||
nativeMisses: number;
|
||||
sourceTransformForced: number;
|
||||
sourceTransformFallbacks: number;
|
||||
topSourceTransformTargets: Array<{ target: string; count: number }>;
|
||||
};
|
||||
|
||||
const DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES = 128;
|
||||
const MAX_TRACKED_SOURCE_TRANSFORM_TARGETS = 24;
|
||||
const pluginModuleLoaderStats = {
|
||||
calls: 0,
|
||||
nativeHits: 0,
|
||||
nativeMisses: 0,
|
||||
sourceTransformForced: 0,
|
||||
sourceTransformFallbacks: 0,
|
||||
sourceTransformTargets: new Map<string, number>(),
|
||||
};
|
||||
|
||||
function recordSourceTransformTarget(target: string): void {
|
||||
const current = pluginModuleLoaderStats.sourceTransformTargets.get(target) ?? 0;
|
||||
pluginModuleLoaderStats.sourceTransformTargets.set(target, current + 1);
|
||||
if (pluginModuleLoaderStats.sourceTransformTargets.size <= MAX_TRACKED_SOURCE_TRANSFORM_TARGETS) {
|
||||
return;
|
||||
}
|
||||
let leastUsedTarget: string | undefined;
|
||||
let leastUsedCount = Number.POSITIVE_INFINITY;
|
||||
for (const [candidate, count] of pluginModuleLoaderStats.sourceTransformTargets) {
|
||||
if (count < leastUsedCount) {
|
||||
leastUsedTarget = candidate;
|
||||
leastUsedCount = count;
|
||||
}
|
||||
}
|
||||
if (leastUsedTarget) {
|
||||
pluginModuleLoaderStats.sourceTransformTargets.delete(leastUsedTarget);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPluginModuleLoaderStats(): PluginModuleLoaderStatsSnapshot {
|
||||
return {
|
||||
calls: pluginModuleLoaderStats.calls,
|
||||
nativeHits: pluginModuleLoaderStats.nativeHits,
|
||||
nativeMisses: pluginModuleLoaderStats.nativeMisses,
|
||||
sourceTransformForced: pluginModuleLoaderStats.sourceTransformForced,
|
||||
sourceTransformFallbacks: pluginModuleLoaderStats.sourceTransformFallbacks,
|
||||
topSourceTransformTargets: [...pluginModuleLoaderStats.sourceTransformTargets]
|
||||
.toSorted((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
||||
.slice(0, 8)
|
||||
.map(([target, count]) => ({ target, count })),
|
||||
};
|
||||
}
|
||||
|
||||
export function resetPluginModuleLoaderStatsForTest(): void {
|
||||
pluginModuleLoaderStats.calls = 0;
|
||||
pluginModuleLoaderStats.nativeHits = 0;
|
||||
pluginModuleLoaderStats.nativeMisses = 0;
|
||||
pluginModuleLoaderStats.sourceTransformForced = 0;
|
||||
pluginModuleLoaderStats.sourceTransformFallbacks = 0;
|
||||
pluginModuleLoaderStats.sourceTransformTargets.clear();
|
||||
}
|
||||
|
||||
export function createPluginModuleLoaderCache(
|
||||
maxEntries = DEFAULT_PLUGIN_MODULE_LOADER_CACHE_ENTRIES,
|
||||
@@ -139,11 +198,15 @@ function createPluginModuleLoader(params: {
|
||||
// jiti's alias rewriting to surface a narrow SDK slice), route every
|
||||
// target through jiti so those alias rewrites still apply.
|
||||
if (!params.tryNative) {
|
||||
return ((target: string, ...rest: unknown[]) =>
|
||||
(getLoadWithSourceTransform() as (t: string, ...a: unknown[]) => unknown)(
|
||||
return ((target: string, ...rest: unknown[]) => {
|
||||
pluginModuleLoaderStats.calls += 1;
|
||||
pluginModuleLoaderStats.sourceTransformForced += 1;
|
||||
recordSourceTransformTarget(target);
|
||||
return (getLoadWithSourceTransform() as (t: string, ...a: unknown[]) => unknown)(
|
||||
target,
|
||||
...rest,
|
||||
)) as PluginModuleLoader;
|
||||
);
|
||||
}) as PluginModuleLoader;
|
||||
}
|
||||
// Otherwise prefer native require() for already-compiled JS artifacts
|
||||
// (the bundled plugin public surfaces shipped in dist/). jiti's transform
|
||||
@@ -153,10 +216,15 @@ function createPluginModuleLoader(params: {
|
||||
// async-module fallbacks `tryNativeRequireJavaScriptModule` declines to
|
||||
// handle.
|
||||
return ((target: string, ...rest: unknown[]) => {
|
||||
pluginModuleLoaderStats.calls += 1;
|
||||
const native = tryNativeRequireJavaScriptModule(target, { allowWindows: true });
|
||||
if (native.ok) {
|
||||
pluginModuleLoaderStats.nativeHits += 1;
|
||||
return native.moduleExport;
|
||||
}
|
||||
pluginModuleLoaderStats.nativeMisses += 1;
|
||||
pluginModuleLoaderStats.sourceTransformFallbacks += 1;
|
||||
recordSourceTransformTarget(target);
|
||||
return (getLoadWithSourceTransform() as (t: string, ...a: unknown[]) => unknown)(
|
||||
target,
|
||||
...rest,
|
||||
|
||||
Reference in New Issue
Block a user