perf: speed up app startup and shutdown

- Cache admin privilege check; bypass cmd.exe wrapper via execFile
- Reuse the synchronous fltmc result from setupPlatformSpecifics to prime
  the admin privilege cache, avoiding a second elevated-state probe
- Parallelize startup init: appConfig, i18n, admin check now run concurrently
- Skip high-privilege core check when current process is admin
- Coalesce mihomo process scan to a single tasklist + batched powershell
- Parallelize the four mihomo websocket streams
- Light-path cleanup of Windows named pipes; reserve PowerShell scan for retry
- Defer Sub-Store frontend/backend startup until after the main window is
  created, freeing the critical startup path
- Run post-core background tasks (profile updater, webdav, tun follow-up) in parallel
- Tighten shutdown timeouts and skip recoverDNS on non-macOS
This commit is contained in:
zjdndjf
2026-05-27 02:09:38 +08:00
parent 3207c1f73e
commit c958845f71
8 changed files with 232 additions and 137 deletions

View File

@@ -5,6 +5,7 @@
## 性能优化 (Performance)
- 重构简化连接页
- 优化软件启动和退出速度
# 1.9.5

View File

@@ -1,16 +1,29 @@
import { exec } from 'child_process'
import { execFile } from 'child_process'
import { promisify } from 'util'
import { managerLogger } from '../utils/logger'
const execPromise = promisify(exec)
const execFilePromise = promisify(execFile)
// admin 状态在 Node 进程生命周期内不会变化,永久缓存
let adminPrivilegePromise: Promise<boolean> | null = null
// 允许 lifecycle.ts 同步检测的结果直接填入缓存,避免重复跑一次 fltmc
export function primeAdminPrivilegesCache(value: boolean): void {
if (adminPrivilegePromise) return
adminPrivilegePromise = Promise.resolve(value)
managerLogger.info(`Admin privileges primed from sync check: ${value}`)
}
export async function checkAdminPrivileges(): Promise<boolean> {
if (process.platform !== 'win32') {
return true
}
if (adminPrivilegePromise) return adminPrivilegePromise
adminPrivilegePromise = (async () => {
try {
await execPromise('chcp 65001 >nul 2>&1 && fltmc', { encoding: 'utf8' })
await execFilePromise('fltmc', [], { windowsHide: true, timeout: 1500 })
managerLogger.info('Admin privileges confirmed via fltmc')
return true
} catch (fltmcError: unknown) {
@@ -18,7 +31,7 @@ export async function checkAdminPrivileges(): Promise<boolean> {
managerLogger.debug(`fltmc failed with code ${errorCode}, trying net session as fallback`)
try {
await execPromise('chcp 65001 >nul 2>&1 && net session', { encoding: 'utf8' })
await execFilePromise('net', ['session'], { windowsHide: true, timeout: 1500 })
managerLogger.info('Admin privileges confirmed via net session')
return true
} catch (netSessionError: unknown) {
@@ -29,4 +42,7 @@ export async function checkAdminPrivileges(): Promise<boolean> {
return false
}
}
})()
return adminPrivilegePromise
}

View File

@@ -308,7 +308,7 @@ function setupCoreListeners(
if (process.platform === 'win32') {
managerLogger.info('Attempting Windows pipe cleanup and retry...')
try {
await cleanupWindowsNamedPipes()
await cleanupWindowsNamedPipes(true)
await new Promise((r) => setTimeout(r, 2000))
} catch (cleanupError) {
managerLogger.error('Pipe cleanup failed:', cleanupError)
@@ -350,10 +350,12 @@ function setupCoreListeners(
await waitForCoreReady()
await getAxios(true)
await startMihomoTraffic()
await startMihomoConnections()
await startMihomoLogs()
await startMihomoMemory()
await Promise.all([
startMihomoTraffic(),
startMihomoConnections(),
startMihomoLogs(),
startMihomoMemory()
])
retry = 10
}
})
@@ -380,13 +382,13 @@ export async function startCore(detached = false, skipStop = false): Promise<Pro
// 停止核心
export async function stopCore(force = false): Promise<void> {
if (!force && process.platform === 'darwin') {
try {
if (!force) {
await recoverDNS()
}
} catch (error) {
managerLogger.error('recover dns failed', error)
}
}
if (child) {
child.removeAllListeners()

View File

@@ -133,41 +133,64 @@ async function checkHighPrivilegeMihomoProcess(): Promise<boolean> {
try {
if (process.platform === 'win32') {
for (const executable of mihomoExecutables) {
let stdout = ''
try {
const { stdout } = await execPromise(
`chcp 65001 >nul 2>&1 && tasklist /FI "IMAGENAME eq ${executable}" /FO CSV`,
{ encoding: 'utf8' }
)
const lines = stdout.split('\n').filter((line) => line.includes(executable))
const result = await execFilePromise('tasklist', ['/FO', 'CSV', '/NH'], {
windowsHide: true,
timeout: 3000,
maxBuffer: 4 * 1024 * 1024
})
stdout = result.stdout
} catch (error) {
managerLogger.error('Failed to list processes via tasklist', error)
return false
}
if (lines.length > 0) {
managerLogger.info(`Found ${lines.length} ${executable} processes running`)
const candidatePids: { pid: string; image: string }[] = []
for (const line of stdout.split('\n')) {
const match = line.match(/^"([^"]+)","(\d+)"/)
if (!match) continue
const image = match[1].toLowerCase()
if (mihomoExecutables.includes(image)) {
candidatePids.push({ pid: match[2], image })
}
}
for (const line of lines) {
const parts = line.split(',')
if (parts.length >= 2) {
const pid = parts[1].replace(/"/g, '').trim()
if (candidatePids.length === 0) {
managerLogger.info('No mihomo processes found running')
return false
}
managerLogger.info(`Found ${candidatePids.length} mihomo processes running`)
const pidArgs = candidatePids.map(({ pid }) => pid).join(',')
try {
const { stdout: processInfo } = await execPromise(
`powershell -NoProfile -Command "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; Get-Process -Id ${pid} | Select-Object Name,Id,Path,CommandLine | ConvertTo-Json"`,
{ encoding: 'utf8' }
const { stdout: processInfo } = await execFilePromise(
'powershell',
[
'-NoProfile',
'-Command',
`Get-Process -Id ${pidArgs} -ErrorAction SilentlyContinue | Select-Object Name,Id,Path | ConvertTo-Json -Compress`
],
{ windowsHide: true, timeout: 4000, maxBuffer: 4 * 1024 * 1024 }
)
const processJson = JSON.parse(processInfo)
managerLogger.info(`Process ${pid} info: ${processInfo.substring(0, 200)}`)
if (processJson.Name.includes('mihomo') && processJson.Path === null) {
if (!processInfo.trim()) return false
const parsed = JSON.parse(processInfo)
const list = Array.isArray(parsed) ? parsed : [parsed]
for (const proc of list) {
if (
proc &&
typeof proc.Name === 'string' &&
proc.Name.toLowerCase().includes('mihomo') &&
proc.Path === null
) {
return true
}
} catch {
managerLogger.info(`Cannot get info for process ${pid}, might be high privilege`)
}
}
}
}
} catch (error) {
managerLogger.error(`Failed to check ${executable} processes`, error)
}
managerLogger.info('PowerShell process inspection failed', error)
}
} else {
let foundProcesses = false

View File

@@ -1,4 +1,4 @@
import { exec } from 'child_process'
import { exec, execFile } from 'child_process'
import { promisify } from 'util'
import { rm } from 'fs/promises'
import { existsSync } from 'fs'
@@ -6,6 +6,7 @@ import { managerLogger } from '../utils/logger'
import { getAxios } from './mihomoApi'
const execPromise = promisify(exec)
const execFilePromise = promisify(execFile)
// 常量
const CORE_READY_MAX_RETRIES = 30
@@ -19,7 +20,38 @@ export async function cleanupSocketFile(): Promise<void> {
}
}
export async function cleanupWindowsNamedPipes(): Promise<void> {
// thorough=true 走 PowerShell 慢路径,仅在外部控制器监听冲突时使用
export async function cleanupWindowsNamedPipes(thorough = false): Promise<void> {
if (!thorough) {
try {
const { stdout } = await execFilePromise(
'tasklist',
['/FI', 'IMAGENAME eq mihomo*', '/FO', 'CSV', '/NH'],
{ windowsHide: true, timeout: 1500, maxBuffer: 1 * 1024 * 1024 }
)
const pids: number[] = []
for (const line of stdout.split('\n')) {
const match = line.match(/^"([^"]+)","(\d+)"/)
if (!match) continue
const pid = parseInt(match[2], 10)
if (!isNaN(pid) && pid !== process.pid) pids.push(pid)
}
if (pids.length === 0) return
for (const pid of pids) {
await terminateProcess(pid)
}
// 给进程留出退出窗口,避免 pipe 占用导致后续启动失败
await new Promise((resolve) => setTimeout(resolve, 200))
} catch (error) {
managerLogger.warn('Lightweight pipe cleanup failed:', error)
}
return
}
try {
try {
const { stdout } = await execPromise(

View File

@@ -15,7 +15,7 @@ import {
initCoreWatcher
} from './core/manager'
import { createTray } from './resolve/tray'
import { init, initBasic, safeShowErrorBox } from './utils/init'
import { init, initBasic, safeShowErrorBox, startSubStoreServices } from './utils/init'
import { initShortcut } from './resolve/shortcut'
import { initProfileUpdater } from './core/profileUpdater'
import { startMonitor } from './resolve/trafficMonitor'
@@ -48,7 +48,7 @@ function getWindowsPowerShellMajorVersion(): number | null {
try {
const stdout = execFileSync('reg', ['query', key, '/v', 'PowerShellVersion'], {
encoding: 'utf8',
timeout: 1000
timeout: 800
})
const version = stdout.match(/PowerShellVersion\s+REG_\w+\s+([^\s]+)/)?.[1]
const major = version ? parseInt(version.split('.')[0], 10) : NaN
@@ -61,6 +61,7 @@ function getWindowsPowerShellMajorVersion(): number | null {
return null
}
// PowerShell 版本过低必须在 app 启动前提示并退出,因此保持同步执行
if (process.platform === 'win32') {
try {
const major = getWindowsPowerShellMajorVersion()
@@ -103,50 +104,6 @@ initApp().catch((e) => {
setupPlatformSpecifics()
async function checkHighPrivilegeCoreEarly(): Promise<void> {
if (process.platform !== 'win32') return
try {
await initBasic()
const isCurrentAppAdmin = await checkAdminPrivileges()
if (isCurrentAppAdmin) return
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
if (!hasHighPrivilegeCore) return
try {
const appConfig = await getAppConfig()
const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
await initI18n({ lng: language })
} catch {
await initI18n({ lng: 'zh-CN' })
}
const choice = dialog.showMessageBoxSync({
type: 'warning',
title: i18next.t('core.highPrivilege.title'),
message: i18next.t('core.highPrivilege.message'),
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
defaultId: 0,
cancelId: 1
})
if (choice === 0) {
try {
await restartAsAdmin(false)
app.exit(0)
} catch (error) {
safeShowErrorBox('common.error.adminRequired', `${error}`)
app.exit(1)
}
} else {
app.exit(0)
}
} catch (e) {
mainLogger.error('Failed to check high privilege core', e)
}
}
async function initHardwareAcceleration(): Promise<void> {
try {
await initBasic()
@@ -177,23 +134,69 @@ app.on('open-url', async (_event, url) => {
const initPromise = (async () => {
await initBasic()
await checkHighPrivilegeCoreEarly()
await initAdminStatus()
const adminPromise: Promise<boolean> =
process.platform === 'win32' ? checkAdminPrivileges().catch(() => false) : Promise.resolve(true)
const appConfigPromise = (async () => {
try {
const appConfig = await getAppConfig()
if (!appConfig.language) {
const cfg = await getAppConfig()
if (!cfg.language) {
const systemLanguage = getSystemLanguage()
await patchAppConfig({ language: systemLanguage })
appConfig.language = systemLanguage
cfg.language = systemLanguage
}
await initI18n({ lng: appConfig.language })
return appConfig
await initI18n({ lng: cfg.language })
return cfg
} catch (e) {
safeShowErrorBox('common.error.initFailed', `${e}`)
app.quit()
throw e
}
})()
await adminPromise
await initAdminStatus()
if (process.platform === 'win32') {
const isAdmin = await adminPromise
if (!isAdmin) {
try {
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
if (hasHighPrivilegeCore) {
try {
await appConfigPromise
} catch {
await initI18n({ lng: 'zh-CN' })
}
const choice = dialog.showMessageBoxSync({
type: 'warning',
title: i18next.t('core.highPrivilege.title'),
message: i18next.t('core.highPrivilege.message'),
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
defaultId: 0,
cancelId: 1
})
if (choice === 0) {
try {
await restartAsAdmin(false)
app.exit(0)
} catch (error) {
safeShowErrorBox('common.error.adminRequired', `${error}`)
app.exit(1)
}
} else {
app.exit(0)
}
}
} catch (e) {
mainLogger.error('Failed to check high privilege core', e)
}
}
}
return appConfigPromise
})()
app.whenReady().then(async () => {
@@ -219,9 +222,15 @@ app.whenReady().then(async () => {
const startPromises = await startCore()
if (startPromises.length > 0) {
startPromises[0].then(async () => {
await initProfileUpdater()
await initWebdavBackupScheduler()
await checkAdminRestartForTun()
await Promise.allSettled([
initProfileUpdater().catch((e) => mainLogger.warn('Failed to init profile updater', e)),
initWebdavBackupScheduler().catch((e) =>
mainLogger.warn('Failed to init webdav backup scheduler', e)
),
checkAdminRestartForTun().catch((e) =>
mainLogger.warn('Failed admin-restart-for-tun follow-up', e)
)
])
})
}
coreStarted = true
@@ -240,6 +249,10 @@ app.whenReady().then(async () => {
await createWindowPromise
void startSubStoreServices().catch((e) =>
mainLogger.warn('Failed to start sub-store services', e)
)
const { showFloatingWindow: showFloating = false, disableTray = false } = appConfig
const uiTasks: Promise<void>[] = [initShortcut()]

View File

@@ -1,9 +1,10 @@
import { spawn, exec, execSync } from 'child_process'
import { spawn, exec, execFileSync } from 'child_process'
import { promisify } from 'util'
import { stat } from 'fs/promises'
import { existsSync } from 'fs'
import { app, powerMonitor } from 'electron'
import { stopCore, cleanupCoreWatcher } from './core/manager'
import { primeAdminPrivilegesCache } from './core/admin'
import { triggerSysProxy, disableSysProxySync } from './sys/sysproxy'
import { exePath } from './utils/dirs'
@@ -55,15 +56,19 @@ export function setupPlatformSpecifics(): void {
app.commandLine.appendSwitch('in-process-gpu')
}
if (process.platform === 'win32' && isWindowsElevatedSync()) {
if (process.platform === 'win32') {
const elevated = isWindowsElevatedSync()
if (elevated) {
primeAdminPrivilegesCache(true)
app.commandLine.appendSwitch('disable-gpu-sandbox')
}
}
}
function isWindowsElevatedSync(): boolean {
if (process.platform !== 'win32') return false
try {
execSync('fltmc', { stdio: 'ignore', windowsHide: true })
execFileSync('fltmc', [], { stdio: 'ignore', windowsHide: true, timeout: 800 })
return true
} catch {
return false
@@ -107,7 +112,7 @@ export function setupAppLifecycle(): void {
}),
stopCore()
]).then(() => {}),
3000
1200
)
}

View File

@@ -147,7 +147,11 @@ async function killOldMihomoProcesses(): Promise<void> {
try {
const execFilePromise = promisify(execFile)
const coreNames = new Set(['mihomo.exe', 'mihomo-alpha.exe', 'mihomo-smart.exe'])
const { stdout } = await execFilePromise('tasklist', ['/FO', 'CSV', '/NH'])
const { stdout } = await execFilePromise('tasklist', ['/FO', 'CSV', '/NH'], {
windowsHide: true,
timeout: 3000,
maxBuffer: 4 * 1024 * 1024
})
const pids = stdout
.split('\n')
@@ -445,13 +449,7 @@ export async function ensureRuntimeFiles(): Promise<void> {
export async function init(): Promise<void> {
const { sysProxy } = await getAppConfig()
const initTasks: Promise<void>[] = [
(async (): Promise<void> => {
await ensureRuntimeFiles()
await Promise.all([startSubStoreFrontendServer(), startSubStoreBackendServer()])
})(),
startSSIDCheck()
]
const initTasks: Promise<void>[] = [ensureRuntimeFiles(), startSSIDCheck()]
initTasks.push(
(async (): Promise<void> => {
@@ -469,3 +467,8 @@ export async function init(): Promise<void> {
await Promise.all(initTasks)
initDeeplink()
}
export async function startSubStoreServices(): Promise<void> {
await ensureRuntimeFiles()
await Promise.all([startSubStoreFrontendServer(), startSubStoreBackendServer()])
}