Skip to content

Commit 2c15eb6

Browse files
committed
fix(性能): 优化主进程波形分析卡顿
1 parent 4c4f3de commit 2c15eb6

6 files changed

Lines changed: 95 additions & 24 deletions

File tree

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "frkb",
3-
"version": "1.2.4-rc.202609100927",
3+
"version": "1.2.4-rc.202609101109",
44
"description": "Track Studio — Rapid Audio Organization Tool",
55
"license": "PolyForm-Noncommercial-1.0.0",
66
"main": "./out/main/index.js",

‎src/main/libraryCacheDb/unifiedDisplayWaveformCache.ts‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { deflateSync, inflateSync } from 'node:zlib'
1+
import { promisify } from 'node:util'
2+
import { deflate, inflate } from 'node:zlib'
23
import {
34
UNIFIED_DISPLAY_WAVEFORM_CACHE_VERSION,
45
UNIFIED_DISPLAY_WAVEFORM_PARAMETER_VERSION,
@@ -40,6 +41,8 @@ type UnifiedDisplayWaveformRow = {
4041

4142
const TABLE = 'unified_display_waveform_cache'
4243
const MAGIC = 'UDW1'
44+
const deflateAsync = promisify(deflate)
45+
const inflateAsync = promisify(inflate)
4346

4447
const normalizeMeta = (row: unknown): UnifiedDisplayWaveformMeta | null => {
4548
if (!isSqliteRow(row)) return null
@@ -81,16 +84,16 @@ const toBuffer = (value: unknown): Buffer | null => {
8184
return null
8285
}
8386

84-
const writeHeader = (payload: Buffer, header: Record<string, unknown>) => {
87+
const writeHeader = async (payload: Buffer, header: Record<string, unknown>) => {
8588
const headerBuffer = Buffer.from(JSON.stringify(header), 'utf8')
8689
const prefix = Buffer.alloc(8)
8790
prefix.write(MAGIC, 0, 4, 'ascii')
8891
prefix.writeUInt32LE(headerBuffer.byteLength, 4)
89-
return deflateSync(Buffer.concat([prefix, headerBuffer, payload]))
92+
return await deflateAsync(Buffer.concat([prefix, headerBuffer, payload]))
9093
}
9194

92-
const readPayload = (compressed: Buffer) => {
93-
const decoded = inflateSync(compressed)
95+
const readPayload = async (compressed: Buffer) => {
96+
const decoded = await inflateAsync(compressed)
9497
if (decoded.byteLength < 8 || decoded.subarray(0, 4).toString('ascii') !== MAGIC) return null
9598
const headerLength = decoded.readUInt32LE(4)
9699
const payloadStart = 8 + headerLength
@@ -121,9 +124,9 @@ const readPayload = (compressed: Buffer) => {
121124
return { header, readBytes }
122125
}
123126

124-
function encodeUnifiedDisplayWaveformCacheData(
127+
async function encodeUnifiedDisplayWaveformCacheData(
125128
data: UnifiedDisplayWaveformDetailData
126-
): Buffer | null {
129+
): Promise<Buffer | null> {
127130
if (!data || data.version !== UNIFIED_DISPLAY_WAVEFORM_CACHE_VERSION) return null
128131
if (data.parameterVersion !== UNIFIED_DISPLAY_WAVEFORM_PARAMETER_VERSION) return null
129132
if (!data.height?.length || !data.attack?.length || !data.colorIndex?.length) return null
@@ -160,10 +163,10 @@ function encodeUnifiedDisplayWaveformCacheData(
160163
})
161164
}
162165

163-
function decodeUnifiedDisplayWaveformCacheData(
166+
async function decodeUnifiedDisplayWaveformCacheData(
164167
meta: UnifiedDisplayWaveformMeta,
165168
payload: Buffer
166-
): UnifiedDisplayWaveformDetailData | null {
169+
): Promise<UnifiedDisplayWaveformDetailData | null> {
167170
if (!meta || !payload) return null
168171
if (
169172
meta.cacheVersion !== UNIFIED_DISPLAY_WAVEFORM_CACHE_VERSION ||
@@ -172,7 +175,7 @@ function decodeUnifiedDisplayWaveformCacheData(
172175
) {
173176
return null
174177
}
175-
const decoded = readPayload(payload)
178+
const decoded = await readPayload(payload)
176179
if (!decoded) return null
177180
const height = decoded.readBytes(decoded.header.heightLength)
178181
const attack = decoded.readBytes(decoded.header.attackLength)
@@ -329,7 +332,7 @@ export async function loadUnifiedDisplayWaveformCacheData(
329332
await removeUnifiedDisplayWaveformCacheEntry(listRoot, filePath)
330333
return null
331334
}
332-
const decoded = decodeUnifiedDisplayWaveformCacheData(meta, payload)
335+
const decoded = await decodeUnifiedDisplayWaveformCacheData(meta, payload)
333336
if (!decoded) {
334337
await removeUnifiedDisplayWaveformCacheEntry(listRoot, filePath)
335338
return null
@@ -350,7 +353,7 @@ export async function upsertUnifiedDisplayWaveformCacheEntry(
350353
const db = getLibraryDb()
351354
const keys = resolveCacheKeys(listRoot, filePath)
352355
if (!db || !keys || !data) return false
353-
const payload = encodeUnifiedDisplayWaveformCacheData(data)
356+
const payload = await encodeUnifiedDisplayWaveformCacheData(data)
354357
if (!payload) return false
355358
try {
356359
const upsertMain = db.prepare(

‎src/main/services/keyAnalysis/background.ts‎

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import nodeFs from 'node:fs'
1+
import type { Dirent } from 'node:fs'
22
import fs from 'node:fs/promises'
33
import path from 'node:path'
44
import type { EventEmitter } from 'node:events'
@@ -286,13 +286,13 @@ export const createKeyAnalysisBackground = (deps: KeyAnalysisBackgroundDeps) =>
286286
return roots
287287
}
288288

289-
const resolveSongListRootsFromFs = (): string[] => {
289+
const resolveSongListRootsFromFs = async (): Promise<string[]> => {
290290
const rootDir = store.databaseDir
291291
if (!rootDir) return []
292292
const libraryRoot = path.join(rootDir, 'library')
293-
let libraryEntries: nodeFs.Dirent[] = []
293+
let libraryEntries: Dirent[] = []
294294
try {
295-
libraryEntries = nodeFs.readdirSync(libraryRoot, { withFileTypes: true })
295+
libraryEntries = await fs.readdir(libraryRoot, { withFileTypes: true })
296296
} catch {
297297
return []
298298
}
@@ -305,9 +305,9 @@ export const createKeyAnalysisBackground = (deps: KeyAnalysisBackgroundDeps) =>
305305
continue
306306
}
307307
const libraryDir = path.join(libraryRoot, libraryEntry.name)
308-
let songListEntries: nodeFs.Dirent[] = []
308+
let songListEntries: Dirent[] = []
309309
try {
310-
songListEntries = nodeFs.readdirSync(libraryDir, { withFileTypes: true })
310+
songListEntries = await fs.readdir(libraryDir, { withFileTypes: true })
311311
} catch {
312312
continue
313313
}
@@ -335,13 +335,13 @@ export const createKeyAnalysisBackground = (deps: KeyAnalysisBackgroundDeps) =>
335335
.map(([, root]) => root)
336336
}
337337

338-
const refreshBackgroundRoots = (): string[] => {
338+
const refreshBackgroundRoots = async (): Promise<string[]> => {
339339
const now = Date.now()
340340
const shouldRefresh =
341341
now - backgroundRootsLastRefresh >= BACKGROUND_FS_REFRESH_MS || backgroundRoots.length === 0
342342
if (!shouldRefresh) return backgroundRoots
343343
const nodeRoots = resolveSongListRoots()
344-
const fsRoots = resolveSongListRootsFromFs()
344+
const fsRoots = await resolveSongListRootsFromFs()
345345
const nextRoots = mergeSongListRoots(nodeRoots, fsRoots)
346346
const signature = nextRoots.join('|')
347347
if (signature !== backgroundRootsSignature) {
@@ -378,7 +378,7 @@ export const createKeyAnalysisBackground = (deps: KeyAnalysisBackgroundDeps) =>
378378
}
379379

380380
const cleanupOrphanedCovers = async () => {
381-
const roots = refreshBackgroundRoots()
381+
const roots = await refreshBackgroundRoots()
382382
if (roots.length === 0) return
383383

384384
const audioExts = getAudioExtensions()
@@ -522,7 +522,7 @@ export const createKeyAnalysisBackground = (deps: KeyAnalysisBackgroundDeps) =>
522522
const collectBackgroundFsCandidates = async (limit: number): Promise<string[]> => {
523523
const results: string[] = []
524524
if (limit <= 0 || !store.databaseDir || deps.hasForegroundWork()) return results
525-
const roots = refreshBackgroundRoots()
525+
const roots = await refreshBackgroundRoots()
526526
if (roots.length === 0) return results
527527
const audioExts = getAudioExtensions()
528528
if (audioExts.size === 0) return results

‎src/main/workers/keyAnalysisWorker.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { parentPort } from 'node:worker_threads'
22
import type { MixxxWaveformData } from '../waveformCodec'
3+
import { collectTransferableArrayBuffers } from './transferableBuffers'
34
import { COMPACT_VISUAL_WAVEFORM_COLOR_RAW_RATE } from '../../shared/compactVisualWaveform'
45
import {
56
buildUnifiedDisplayWaveformDetailFromMixxx,
@@ -689,5 +690,5 @@ parentPort?.on('message', async (job: KeyJob) => {
689690
})
690691
}
691692

692-
parentPort?.postMessage(response)
693+
parentPort?.postMessage(response, collectTransferableArrayBuffers(response.result))
693694
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { collectTransferableArrayBuffers } from './transferableBuffers'
3+
4+
describe('collectTransferableArrayBuffers', () => {
5+
it('collects nested typed-array buffers once', () => {
6+
const shared = new ArrayBuffer(16)
7+
const other = new Uint8Array(8)
8+
const result = collectTransferableArrayBuffers({
9+
bands: [new Uint8Array(shared), new Uint8Array(shared, 4, 4)],
10+
detail: { height: other }
11+
})
12+
13+
expect(result).toHaveLength(2)
14+
expect(result).toContain(shared)
15+
expect(result).toContain(other.buffer)
16+
})
17+
18+
it('skips pooled Node buffers and handles cycles', () => {
19+
const sharedWithBuffer = new ArrayBuffer(8)
20+
const cyclic: Record<string, unknown> = {
21+
typed: new Uint8Array(sharedWithBuffer),
22+
payload: Buffer.from(sharedWithBuffer)
23+
}
24+
cyclic.self = cyclic
25+
26+
expect(collectTransferableArrayBuffers(cyclic)).toEqual([])
27+
})
28+
})
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const isTransferableArrayBuffer = (value: unknown): value is ArrayBuffer =>
2+
value instanceof ArrayBuffer
3+
4+
/**
5+
* 收集 worker 结果中的独占 ArrayBuffer,避免大型波形在回传主进程时被结构化克隆。
6+
* Buffer 可能来自 Node 共享内存池,不能安全转移,因此明确跳过。
7+
*/
8+
export const collectTransferableArrayBuffers = (value: unknown): ArrayBuffer[] => {
9+
const buffers = new Set<ArrayBuffer>()
10+
const blockedBuffers = new Set<ArrayBuffer>()
11+
const visited = new WeakSet<object>()
12+
13+
const visit = (current: unknown): void => {
14+
if (isTransferableArrayBuffer(current)) {
15+
if (!blockedBuffers.has(current)) buffers.add(current)
16+
return
17+
}
18+
if (ArrayBuffer.isView(current)) {
19+
if (!isTransferableArrayBuffer(current.buffer)) return
20+
if (Buffer.isBuffer(current)) {
21+
blockedBuffers.add(current.buffer)
22+
buffers.delete(current.buffer)
23+
} else if (!blockedBuffers.has(current.buffer)) {
24+
buffers.add(current.buffer)
25+
}
26+
return
27+
}
28+
if (!current || typeof current !== 'object' || visited.has(current)) return
29+
visited.add(current)
30+
if (Array.isArray(current)) {
31+
for (const item of current) visit(item)
32+
return
33+
}
34+
for (const item of Object.values(current as Record<string, unknown>)) visit(item)
35+
}
36+
37+
visit(value)
38+
return [...buffers]
39+
}

0 commit comments

Comments
 (0)