From 27b37705b32c7bd68069b023b57e8dba50713921 Mon Sep 17 00:00:00 2001 From: hungtrinh Date: Thu, 16 Jul 2026 11:17:46 +0700 Subject: [PATCH] perf(startup): skip inactive background services --- src/app/api/settings/route.js | 11 ++-- src/app/api/tunnel/disable/route.js | 5 ++ src/app/api/tunnel/enable/route.js | 5 ++ src/app/api/tunnel/tailscale-disable/route.js | 5 ++ src/app/api/tunnel/tailscale-enable/route.js | 5 ++ src/shared/services/initializeApp.js | 54 +++++++++++++++---- src/shared/services/quotaAutoPing.js | 15 ++++++ tests/unit/quota-auto-ping.test.js | 23 +++++++- 8 files changed, 107 insertions(+), 16 deletions(-) diff --git a/src/app/api/settings/route.js b/src/app/api/settings/route.js index ccbaee3a..6a713433 100644 --- a/src/app/api/settings/route.js +++ b/src/app/api/settings/route.js @@ -2,7 +2,6 @@ import { NextResponse } from "next/server"; import { getSettings, updateSettings } from "@/lib/localDb"; import { applyOutboundProxyEnv } from "@/lib/network/outboundProxy"; import { resetComboRotation } from "open-sse/services/combo.js"; -import { runQuotaAutoPingTick } from "@/shared/services/quotaAutoPing"; import bcrypt from "bcryptjs"; export const dynamic = "force-dynamic"; @@ -101,10 +100,12 @@ export async function PATCH(request) { Object.prototype.hasOwnProperty.call(body, "claudeAutoPing") || Object.prototype.hasOwnProperty.call(body, "codexAutoPing") ) { - // Run once immediately after opt-in changes so users don't wait for the next scheduler tick. - runQuotaAutoPingTick().catch((error) => { - console.warn("[AutoPing] settings-triggered tick failed:", error.message); - }); + // Keep the scheduler absent when no account opted in; load its provider graph only on demand. + import("@/shared/services/quotaAutoPing") + .then(({ configureQuotaAutoPing }) => { + configureQuotaAutoPing(settings); + }) + .catch((error) => console.warn("[AutoPing] settings update failed:", error.message)); } const { password, oidcClientSecret, ...safeSettings } = settings; diff --git a/src/app/api/tunnel/disable/route.js b/src/app/api/tunnel/disable/route.js index 2d49e031..4c15245a 100644 --- a/src/app/api/tunnel/disable/route.js +++ b/src/app/api/tunnel/disable/route.js @@ -1,9 +1,14 @@ import { NextResponse } from "next/server"; import { disableTunnel } from "@/lib/tunnel"; +import { getSettings } from "@/lib/localDb"; +import { configureTunnelMonitoring } from "@/shared/services/initializeApp"; export async function POST() { try { const result = await disableTunnel(); + getSettings() + .then(configureTunnelMonitoring) + .catch((error) => console.warn("Tunnel monitor update failed:", error.message)); return NextResponse.json(result); } catch (error) { console.error("Tunnel disable error:", error); diff --git a/src/app/api/tunnel/enable/route.js b/src/app/api/tunnel/enable/route.js index 910ad617..05d55fc0 100644 --- a/src/app/api/tunnel/enable/route.js +++ b/src/app/api/tunnel/enable/route.js @@ -1,11 +1,16 @@ import { NextResponse } from "next/server"; import { enableTunnel } from "@/lib/tunnel"; +import { getSettings } from "@/lib/localDb"; +import { configureTunnelMonitoring } from "@/shared/services/initializeApp"; const DNS_WARMUP_DELAY_MS = 8000; export async function POST() { try { const result = await enableTunnel(); + getSettings() + .then(configureTunnelMonitoring) + .catch((error) => console.warn("Tunnel monitor start failed:", error.message)); // Wait for DNS warmup to propagate at Cloudflare edge after tunnel registered await new Promise((r) => setTimeout(r, DNS_WARMUP_DELAY_MS)); return NextResponse.json(result); diff --git a/src/app/api/tunnel/tailscale-disable/route.js b/src/app/api/tunnel/tailscale-disable/route.js index 1a0dca08..5258e3dd 100644 --- a/src/app/api/tunnel/tailscale-disable/route.js +++ b/src/app/api/tunnel/tailscale-disable/route.js @@ -1,9 +1,14 @@ import { NextResponse } from "next/server"; import { disableTailscale } from "@/lib/tunnel"; +import { getSettings } from "@/lib/localDb"; +import { configureTunnelMonitoring } from "@/shared/services/initializeApp"; export async function POST() { try { const result = await disableTailscale(); + getSettings() + .then(configureTunnelMonitoring) + .catch((error) => console.warn("Tailscale monitor update failed:", error.message)); return NextResponse.json(result); } catch (error) { console.error("Tailscale disable error:", error); diff --git a/src/app/api/tunnel/tailscale-enable/route.js b/src/app/api/tunnel/tailscale-enable/route.js index 61b7bda1..11e2d5d6 100644 --- a/src/app/api/tunnel/tailscale-enable/route.js +++ b/src/app/api/tunnel/tailscale-enable/route.js @@ -1,9 +1,14 @@ import { NextResponse } from "next/server"; import { enableTailscale } from "@/lib/tunnel"; +import { getSettings } from "@/lib/localDb"; +import { configureTunnelMonitoring } from "@/shared/services/initializeApp"; export async function POST() { try { const result = await enableTailscale(); + getSettings() + .then(configureTunnelMonitoring) + .catch((error) => console.warn("Tailscale monitor start failed:", error.message)); return NextResponse.json(result); } catch (error) { console.error("Tailscale enable error:", error.message); diff --git a/src/shared/services/initializeApp.js b/src/shared/services/initializeApp.js index e8f5acfc..536b6676 100644 --- a/src/shared/services/initializeApp.js +++ b/src/shared/services/initializeApp.js @@ -14,7 +14,6 @@ import { WATCHDOG_INTERVAL_MS, NETWORK_CHECK_INTERVAL_MS, VIRTUAL_IFACE_REGEX, } from "@/lib/tunnel"; import { getMitmStatus, startMitm, loadEncryptedPassword, initDbHooks, restoreToolDNS, removeAllDNSEntriesSync } from "@/mitm/manager"; -import { startQuotaAutoPing } from "@/shared/services/quotaAutoPing"; import { syncToJson as syncMitmAliasCache } from "@/lib/mitmAliasCache"; import { killAllBridges } from "@/lib/mcp/stdioSseBridge"; @@ -98,22 +97,32 @@ async function runHeavyStartup() { safeRestartTailscale("startup").catch((e) => console.log("[InitApp] Tailscale resume failed:", e.message)); } - ensureCloudflared().catch(() => {}); + if (settings.tunnelEnabled) ensureCloudflared().catch(() => {}); - // Sync mitmAlias DB → JSON cache so standalone MITM server can read it - syncMitmAliasCache().catch(() => {}); + if (settings.mitmEnabled) { + // Sync mitmAlias DB → JSON cache so standalone MITM server can read it. + syncMitmAliasCache().catch(() => {}); + autoStartMitm(settings); + } - startWatchdog(); - startNetworkMonitor(); - autoStartMitm(); - startQuotaAutoPing(); + configureTunnelMonitoring(settings); + + if (hasQuotaAutoPingEnabled(settings)) { + import("@/shared/services/quotaAutoPing") + .then(({ startQuotaAutoPing }) => startQuotaAutoPing()) + .catch((e) => console.log("[AutoPing] scheduler start failed:", e.message)); + } } -async function autoStartMitm() { +function hasQuotaAutoPingEnabled(settings) { + return [settings?.claudeAutoPing, settings?.codexAutoPing] + .some((config) => Object.values(config?.connections || {}).some(Boolean)); +} + +async function autoStartMitm(settings) { if (g.mitmStartInProgress) return; g.mitmStartInProgress = true; try { - const settings = await getSettings(); if (!settings.mitmEnabled) return; const mitmStatus = await getMitmStatus(); if (mitmStatus.running) return; @@ -232,6 +241,12 @@ function startWatchdog() { if (g.watchdogInterval.unref) g.watchdogInterval.unref(); } +function stopWatchdog() { + if (!g.watchdogInterval) return; + clearInterval(g.watchdogInterval); + g.watchdogInterval = null; +} + // ─── Network monitor: detect IPv4 fingerprint change + sleep/wake ──────────── function getNetworkFingerprint() { @@ -293,4 +308,23 @@ function startNetworkMonitor() { if (g.networkMonitorInterval.unref) g.networkMonitorInterval.unref(); } + +function stopNetworkMonitor() { + if (!g.networkMonitorInterval) return; + clearInterval(g.networkMonitorInterval); + g.networkMonitorInterval = null; + g.lastNetworkFingerprint = null; + g.lastOnline = null; +} + +export function configureTunnelMonitoring(settings) { + if (settings?.tunnelEnabled || settings?.tailscaleEnabled) { + startWatchdog(); + startNetworkMonitor(); + return; + } + stopWatchdog(); + stopNetworkMonitor(); +} + export default initializeApp; diff --git a/src/shared/services/quotaAutoPing.js b/src/shared/services/quotaAutoPing.js index 694a5e2b..1fbf39f7 100644 --- a/src/shared/services/quotaAutoPing.js +++ b/src/shared/services/quotaAutoPing.js @@ -296,3 +296,18 @@ export function startQuotaAutoPing() { g.interval = setInterval(() => { runQuotaAutoPingTick().catch(() => {}); }, C.tickIntervalMs); if (g.interval.unref) g.interval.unref(); } + +export function stopQuotaAutoPing() { + if (!g.interval) return; + clearInterval(g.interval); + g.interval = null; + console.log("[AutoPing] scheduler stopped"); +} + +export function configureQuotaAutoPing(settings) { + const enabled = Object.values(C.providers).some((providerConfig) => + Object.values(settings?.[providerConfig.settingsKey]?.connections || {}).some(Boolean) + ); + if (enabled) startQuotaAutoPing(); + else stopQuotaAutoPing(); +} diff --git a/tests/unit/quota-auto-ping.test.js b/tests/unit/quota-auto-ping.test.js index de601df5..f7edb771 100644 --- a/tests/unit/quota-auto-ping.test.js +++ b/tests/unit/quota-auto-ping.test.js @@ -72,6 +72,7 @@ vi.mock("open-sse/executors/index.js", () => ({ describe("quota auto-ping", () => { let runQuotaAutoPingTick; + let configureQuotaAutoPing; let deps; let state; let getCodexUsage; @@ -83,11 +84,12 @@ describe("quota auto-ping", () => { vi.resetModules(); vi.clearAllMocks(); vi.useRealTimers(); + delete global.__quotaAutoPing; ({ getCodexUsage } = await import("open-sse/services/usage/codex.js")); ({ getClaudeUsage } = await import("open-sse/services/usage/claude.js")); ({ getExecutor } = await import("open-sse/executors/index.js")); - ({ runQuotaAutoPingTick } = await import("../../src/shared/services/quotaAutoPing.js")); + ({ runQuotaAutoPingTick, configureQuotaAutoPing } = await import("../../src/shared/services/quotaAutoPing.js")); deps = { getSettings: vi.fn(), @@ -117,6 +119,25 @@ describe("quota auto-ping", () => { expect(deps.proxyAwareFetch).not.toHaveBeenCalled(); }); + it("starts the scheduler only when an account opts in", () => { + vi.useFakeTimers(); + + configureQuotaAutoPing({ codexAutoPing: { connections: {} } }); + expect(vi.getTimerCount()).toBe(0); + + configureQuotaAutoPing({ codexAutoPing: { connections: { "codex-1": true } } }); + expect(vi.getTimerCount()).toBe(1); + }); + + it("stops the scheduler when the last account opts out", () => { + vi.useFakeTimers(); + configureQuotaAutoPing({ claudeAutoPing: { connections: { "claude-1": true } } }); + + configureQuotaAutoPing({ claudeAutoPing: { connections: { "claude-1": false } } }); + + expect(vi.getTimerCount()).toBe(0); + }); + it("does not ping Codex on the first resetAt observation", async () => { deps.getSettings.mockResolvedValue({ codexAutoPing: { connections: { "codex-1": true } } }); deps.getProviderConnections.mockImplementation(async ({ provider }) => (