mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
fix(log): rotate core/sub-store logs by date and avoid O(n²) rewrite at size cap (#1913)
内核与 sub-store 日志通过长连接 Writable 流写入,其路径在流创建时只解析一次, 因此永远不按日期轮转——所有输出都持续追加到以内核启动日命名的文件(例如 core-2026-05-27.log 里写满了 06-12 的日志)。 当该文件达到大小上限后,之后每写一行都会触发「读取尾部 + 重写整个文件」 (每行约 2×maxBytes 的 I/O)。配合 info 级别内核日志(每秒数百行),这会打满 磁盘 I/O 并反压内核进程,导致整个 App 卡顿。 - createCappedLogWritableStream 现在接受路径工厂函数 `() => string`,在每次写入 时求值,使内核/sub-store 日志能像 app 日志一样按日期轮转;仍兼容字符串路径。 - 触顶压缩后只保留约一半上限的内容,使后续写入回到廉价的 appendFile 分支,把 压缩频率从「每次写入」降到「每写满约半个上限一次」,整体 I/O 由 O(n²) 降为 O(n)。 Refs #1912 Co-authored-by: Tinglong Yang <11896584+ytlm@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -110,8 +110,8 @@ export async function startSubStoreBackendServer(): Promise<void> {
|
||||
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,
|
||||
|
||||
@@ -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<void>
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user