diff --git a/bin/cli/commands/serve.mjs b/bin/cli/commands/serve.mjs index 32c898d077..9ba6579c3f 100644 --- a/bin/cli/commands/serve.mjs +++ b/bin/cli/commands/serve.mjs @@ -63,6 +63,8 @@ export function registerServe(program) { } export async function runServe(opts = {}) { + const startedAt = performance.now(); + const { isNativeBinaryCompatible } = await import("../../../scripts/build/native-binary-compat.mjs"); const { getNodeRuntimeSupport, getNodeRuntimeWarning } = @@ -191,7 +193,15 @@ export async function runServe(opts = {}) { } if (opts.noRecovery) { - return runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen); + return runWithoutRecovery( + serverJs, + env, + memoryLimit, + dashboardPort, + apiPort, + noOpen, + startedAt + ); } return runWithSupervisor( @@ -203,6 +213,7 @@ export async function runServe(opts = {}) { noOpen, opts.log === true, opts.maxRestarts ?? 2, + startedAt, useTray ); } @@ -223,7 +234,7 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) { console.log(` \x1b[1mAPI Base:\x1b[0m ${urlScheme}://localhost:${apiPort}/v1`); } -function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen) { +function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, noOpen, startedAt) { // #5238: skip the explicit CLI --max-old-space-size when the user pinned the // heap via NODE_OPTIONS (a CLI arg would shadow/override their value). const server = spawn("node", [...buildNodeHeapArgs(process.env, memoryLimit), serverJs], { @@ -244,7 +255,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, (text.includes("Ready") || text.includes("started") || text.includes("listening")) ) { started = true; - onReady(dashboardPort, apiPort, noOpen); + onReady(dashboardPort, apiPort, noOpen, startedAt); } }); @@ -278,7 +289,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort, setTimeout(() => { if (!started) { started = true; - onReady(dashboardPort, apiPort, noOpen); + onReady(dashboardPort, apiPort, noOpen, startedAt); } }, 15000); } @@ -292,6 +303,7 @@ async function runWithSupervisor( noOpen, showLog, maxRestarts, + startedAt, useTray = false ) { if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1"; @@ -329,7 +341,7 @@ async function runWithSupervisor( waitForServer(dashboardPort, 60000).then(async (up) => { if (up) { if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor); - onReady(dashboardPort, apiPort, noOpen); + onReady(dashboardPort, apiPort, noOpen, startedAt); } }); } @@ -374,12 +386,16 @@ async function maybeStartTray(port, apiPort, supervisor) { } } -async function onReady(dashboardPort, apiPort, noOpen) { +async function onReady(dashboardPort, apiPort, noOpen, startedAt) { const dashboardUrl = `${urlScheme}://localhost:${dashboardPort}`; const apiUrl = `${urlScheme}://localhost:${apiPort}`; + const elapsed = + typeof startedAt === "number" && Number.isFinite(startedAt) + ? ((performance.now() - startedAt) / 1000).toFixed(1) + : "0.0"; console.log(` - \x1b[32m✔ OmniRoute is running!\x1b[0m + \x1b[32m✔ OmniRoute is running!\x1b[0m \x1b[2m(started in ${elapsed}s)\x1b[0m \x1b[1m Dashboard:\x1b[0m ${dashboardUrl} \x1b[1m API Base:\x1b[0m ${apiUrl}/v1 diff --git a/tests/unit/cli-serve-startup-time.test.ts b/tests/unit/cli-serve-startup-time.test.ts new file mode 100644 index 0000000000..2d6c418a55 --- /dev/null +++ b/tests/unit/cli-serve-startup-time.test.ts @@ -0,0 +1,46 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const fs = await import("node:fs"); +const path = await import("node:path"); + +const servePath = path.resolve(import.meta.dirname, "../../bin/cli/commands/serve.mjs"); +const serveSource = fs.readFileSync(servePath, "utf-8"); + +test("serve startup time uses monotonic performance.now()", () => { + assert.match(serveSource, /performance\.now\(\)/); + assert.match(serveSource, /typeof startedAt === "number"/); +}); + +test("serve startup banner includes started in elapsed time", () => { + assert.match(serveSource, /started in/); +}); + +test("serve daemon mode does not accept startedAt", () => { + assert.match( + serveSource, + /function\s+runDaemon\s*\(\s*serverJs\s*,\s*env\s*,\s*memoryLimit\s*,\s*dashboardPort\s*,\s*apiPort\s*\)/ + ); + assert.ok( + !/function\s+runDaemon\s*\(\s*serverJs\s*,\s*env\s*,\s*memoryLimit\s*,\s*dashboardPort\s*,\s*apiPort\s*,\s*startedAt\s*\)/.test( + serveSource + ) + ); +}); + +test("serve runWithSupervisor uses startedAt before defaulted useTray", () => { + const signatureRegex = + /async\s+function\s+runWithSupervisor\s*\([\s\S]*?startedAt\s*,\s*useTray\s*=\s*false\s*\)/; + assert.match( + serveSource, + signatureRegex, + "runWithSupervisor should declare startedAt before the defaulted useTray parameter" + ); + + const callRegex = /runWithSupervisor\s*\([\s\S]*?startedAt\s*,\s*useTray\s*\)/; + assert.match( + serveSource, + callRegex, + "runWithSupervisor should be called with startedAt before useTray" + ); +});