diff --git a/src/main/core/manager.ts b/src/main/core/manager.ts index 410b2d36..a77b8ca7 100644 --- a/src/main/core/manager.ts +++ b/src/main/core/manager.ts @@ -248,8 +248,8 @@ function spawnCoreProcess(config: CoreConfig): ChildProcess { } if (!detached) { - const stdout = createCappedLogWritableStream(coreLogPath()) - const stderr = createCappedLogWritableStream(coreLogPath()) + const stdout = createCappedLogWritableStream(coreLogPath) + const stderr = createCappedLogWritableStream(coreLogPath) proc.stdout?.pipe(stdout) proc.stderr?.pipe(stderr) } diff --git a/src/main/resolve/server.ts b/src/main/resolve/server.ts index e0bc81c3..1ea8ea99 100644 --- a/src/main/resolve/server.ts +++ b/src/main/resolve/server.ts @@ -110,8 +110,8 @@ export async function startSubStoreBackendServer(): Promise { subStorePort = await findAvailablePort(38324) const icon = nativeImage.createFromPath(subStoreIcon) icon.toDataURL() - const stdout = createCappedLogWritableStream(substoreLogPath()) - const stderr = createCappedLogWritableStream(substoreLogPath()) + const stdout = createCappedLogWritableStream(substoreLogPath) + const stderr = createCappedLogWritableStream(substoreLogPath) const env = { SUB_STORE_BACKEND_API_PORT: subStorePort.toString(), SUB_STORE_BACKEND_API_HOST: subStoreHost, diff --git a/src/main/utils/logFile.ts b/src/main/utils/logFile.ts index c0e2aa39..69384399 100644 --- a/src/main/utils/logFile.ts +++ b/src/main/utils/logFile.ts @@ -5,6 +5,15 @@ const MB = 1024 * 1024 const DEFAULT_MAX_LOG_FILE_SIZE_MB = 10 const MIN_MAX_LOG_FILE_SIZE_MB = 1 const TRUNCATE_MARKER = Buffer.from('\n[LOG] File truncated because size limit reached.\n') +// 触顶压缩后保留的内容比例。压缩后文件回落到约 maxBytes * RATIO 大小, +// 使后续写入重新走廉价的 appendFile 分支,把压缩频率从「每次写入」降到 +// 「每写满约该比例的上限一次」,整体 I/O 从 O(n²) 降为 O(n)。 +const COMPACTION_RETAIN_RATIO = 0.5 + +// 日志文件路径既可以是固定字符串,也可以是一个在每次写入时求值的工厂函数。 +// 对于内核 / sub-store 这类长连接日志流,必须传入工厂函数,否则路径会在流创建 +// 时被固化,导致日志永远写入同一个文件、无法按日期轮转。 +type LogFilePath = string | (() => string) interface LogFileState { queue: Promise @@ -105,7 +114,11 @@ async function appendToFileWithLimitInternal( return } - const keepBytes = Math.max(0, maxBytes - data.length - TRUNCATE_MARKER.length) + // 触顶后只保留约 maxBytes * COMPACTION_RETAIN_RATIO 的最近内容,使压缩后文件 + // 远低于上限,接下来的多次写入都能走廉价的 appendFile,避免「文件一旦到达上限后 + // 每写一行都重读 + 重写整个文件」的 O(n²) I/O 放大。 + const retainBudget = Math.floor(maxBytes * COMPACTION_RETAIN_RATIO) + const keepBytes = Math.max(0, retainBudget - data.length - TRUNCATE_MARKER.length) const tail = await readTail(filePath, keepBytes) let rewritten = Buffer.concat([tail, TRUNCATE_MARKER, data]) @@ -135,12 +148,12 @@ export async function appendToFileWithLimit( } class CappedLogWritable extends Writable { - private readonly filePath: string + private readonly resolvePath: () => string private readonly maxBytes: number - constructor(filePath: string, maxBytes: number) { + constructor(filePath: LogFilePath, maxBytes: number) { super() - this.filePath = filePath + this.resolvePath = typeof filePath === 'function' ? filePath : (): string => filePath this.maxBytes = maxBytes } @@ -150,7 +163,8 @@ class CappedLogWritable extends Writable { callback: (error?: Error | null) => void ): void { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding) - appendToFileWithLimit(this.filePath, buffer, this.maxBytes).then( + // 每次写入时重新解析路径,使日志能按当前日期自动轮转(与 app 日志一致)。 + appendToFileWithLimit(this.resolvePath(), buffer, this.maxBytes).then( () => callback(), (error) => callback(error as Error) ) @@ -158,7 +172,7 @@ class CappedLogWritable extends Writable { } export function createCappedLogWritableStream( - filePath: string, + filePath: LogFilePath, maxBytes = getGlobalMaxLogFileSizeBytes() ): Writable { return new CappedLogWritable(filePath, maxBytes)