Skip to content

Commit 4c77890

Browse files
committed
fix(perf): 修复缓存与卡顿诊断
1 parent 668a344 commit 4c77890

11 files changed

Lines changed: 175 additions & 20 deletions

File tree

‎.github/workflows/release.yml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -917,3 +917,21 @@ jobs:
917917
gh release create "$TAG_NAME" "${assets[@]}" --title "$TAG_NAME" --latest --verify-tag --generate-notes
918918
fi
919919
fi
920+
921+
- name: Remove superseded RC releases
922+
if: contains(github.ref_name, '-rc.')
923+
env:
924+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
925+
GH_REPO: ${{ github.repository }}
926+
TAG_NAME: ${{ github.ref_name }}
927+
run: |
928+
set -euo pipefail
929+
base_version="${TAG_NAME%%-rc.*}"
930+
mapfile -t stale_tags < <(
931+
gh release list --limit 100 --json tagName,isPrerelease \
932+
--jq ".[] | select(.isPrerelease and .tagName != \"${TAG_NAME}\" and (.tagName | startswith(\"${base_version}-rc.\"))) | .tagName"
933+
)
934+
for stale_tag in "${stale_tags[@]}"; do
935+
echo "Removing superseded RC release and tag: ${stale_tag}"
936+
gh release delete "${stale_tag}" --yes --cleanup-tag
937+
done

‎AGENTS.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
- PRs should include a clear summary, testing notes, and screenshots for UI changes; link related issues and call out packaging or native-module impacts.
7070

7171
## Release Rules
72-
- 如果我说“发布预发布版本”,默认执行:更新 `package.json` 版本号为 `X.Y.Z-rc.<当前时间>`(时间格式 `YYYYMMDDHHmm`),提交,并**先 push main**,再打同名 tag 并 push tag 触发发布流程。
72+
- 如果我说“发布预发布版本”,默认执行:更新 `package.json` 版本号为 `X.Y.Z-rc.<当前时间>`(时间格式 `YYYYMMDDHHmm`),提交,并**先 push main**,再打同名 tag 并 push tag 触发发布流程。新 RC 的 GitHub Release 资产发布成功后,工作流自动删除同一 `X.Y.Z` 的旧 RC Release 与 tag;禁止在新 RC 发布成功前手工清理旧 RC。
7373
- 如果当前版本是正式版(不含 `-rc`)且我要求预发布版本,自动把版本号 **补丁位 +1**(`Z+1`,逢 10 进 1,例如 `1.1.9 -> 1.2.0`),再加 `-rc.<当前时间>` 并发布。
7474
- 如果我说“发布正式版”,就把 `-rc.` 以及后面的时间后缀去掉,仅保留 `X.Y.Z`,提交,并**先 push main**,再打同名 tag 并 push tag 触发发布流程。
7575
- 发布顺序固定:提交 → push main → 打 tag → push tag。

‎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.202609171805",
3+
"version": "1.2.4-rc.202609181220",
44
"description": "Track Studio — Rapid Audio Organization Tool",
55
"license": "PolyForm-Noncommercial-1.0.0",
66
"main": "./out/main/index.js",

‎src/main/index.ts‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -775,7 +775,11 @@ app.on('window-all-closed', async () => {
775775
app.quit()
776776
})
777777

778-
const RC_DIAGNOSTIC_OUTPUT_LOG_SCOPES = new Set(['playlist-open-perf', 'delete-all-above-perf'])
778+
const RC_DIAGNOSTIC_OUTPUT_LOG_SCOPES = new Set([
779+
'playlist-open-perf',
780+
'delete-all-above-perf',
781+
'renderer-stall-diagnostic'
782+
])
779783

780784
ipcMain.on('outputLog', (_event, logMsg) => {
781785
if (

‎src/main/services/scanSongs.spec.ts‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, expect, it } from 'vitest'
22
import type { ISongInfo } from '../../types/globals'
33
import { createSongBeatGridMapV2FromFixedGrid } from '../../shared/songBeatGridMapV2'
4-
import { preserveCachedGridAnalysisFields } from './scanSongs'
4+
import { prepareFullMetadataCacheInfo, preserveCachedGridAnalysisFields } from './scanSongs'
55

66
const createGrid = () => {
77
const grid = createSongBeatGridMapV2FromFixedGrid({
@@ -88,3 +88,23 @@ describe('preserveCachedGridAnalysisFields', () => {
8888
expect(target.timeBasisOffsetAlgorithmVersion).toBeUndefined()
8989
})
9090
})
91+
92+
describe('prepareFullMetadataCacheInfo', () => {
93+
it('turns an analysis-only cache entry into a regular cache hit after metadata parsing', () => {
94+
const cached: ISongInfo = {
95+
...createSong(),
96+
analysisOnly: true,
97+
key: '8A',
98+
beatGridMap: createGrid(),
99+
energyScore: 74,
100+
energyAlgorithmVersion: 1
101+
}
102+
103+
const result = prepareFullMetadataCacheInfo(createSong(), cached, true)
104+
105+
expect(result.analysisOnly).toBe(false)
106+
expect(result.key).toBe('8A')
107+
expect(result.beatGridMap).toEqual(cached.beatGridMap)
108+
expect(result.energyScore).toBe(74)
109+
})
110+
})

‎src/main/services/scanSongs.ts‎

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,27 @@ const preserveCachedAnalysisFields = (target: ISongInfo, cachedInfo?: ISongInfo
279279
preserveBestAvailableSongStructure(target, cachedInfo)
280280
}
281281

282+
/**
283+
* A metadata scan has read the complete track record from disk. Even when its previous cache
284+
* row only contained analysis results, the replacement row must become a normal cache hit on
285+
* the next scan.
286+
*/
287+
export const prepareFullMetadataCacheInfo = (
288+
info: ISongInfo,
289+
cachedInfo?: ISongInfo | null,
290+
cachedStatMatches = false
291+
): ISongInfo => {
292+
const nextInfo: ISongInfo = { ...info, analysisOnly: false }
293+
if (!cachedInfo) return nextInfo
294+
preserveCachedKeyAndBpm(nextInfo, cachedInfo)
295+
if (cachedStatMatches) {
296+
preserveCachedGridAnalysisFields(nextInfo, cachedInfo)
297+
preserveCachedEnergyAnalysisFields(nextInfo, cachedInfo)
298+
}
299+
preserveCachedUserListFields(nextInfo, cachedInfo)
300+
return nextInfo
301+
}
302+
282303
const preserveCachedUserListFields = (target: ISongInfo, cachedInfo?: ISongInfo | null) => {
283304
const cachedPlaylistTrackNumber = normalizePlaylistTrackNumber(cachedInfo?.playlistTrackNumber)
284305
if (
@@ -567,21 +588,10 @@ export async function scanSongList(
567588
for (const st of filesStatList) {
568589
const info = infoMap.get(st.key)
569590
if (!info) continue
570-
const nextInfo = { ...info }
571591
const cached = cacheMap.get(st.key)
572-
if (cached?.info) {
573-
preserveCachedKeyAndBpm(nextInfo, cached.info)
574-
const cachedStatMatches =
575-
cached.size === st.size && Math.abs(cached.mtimeMs - st.mtimeMs) < 1
576-
if (cachedStatMatches) {
577-
preserveCachedGridAnalysisFields(nextInfo, cached.info)
578-
preserveCachedEnergyAnalysisFields(nextInfo, cached.info)
579-
}
580-
if (nextInfo.analysisOnly === undefined && cached.info.analysisOnly) {
581-
nextInfo.analysisOnly = true
582-
}
583-
preserveCachedUserListFields(nextInfo, cached.info)
584-
}
592+
const cachedStatMatches =
593+
!!cached && cached.size === st.size && Math.abs(cached.mtimeMs - st.mtimeMs) < 1
594+
const nextInfo = prepareFullMetadataCacheInfo(info, cached?.info, cachedStatMatches)
585595
newEntriesMap.set(st.file, {
586596
size: st.size,
587597
mtimeMs: st.mtimeMs,

‎src/main/window/mainWindow/responsivenessDiagnostics.ts‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { isPackagedRcBuild } from '../../services/rcDiagnostics'
99
const MAIN_PROCESS_STALL_THRESHOLD_MS = 3_000
1010
const MAIN_PROCESS_HEARTBEAT_INTERVAL_MS = 1_000
1111
const MAIN_PROCESS_STALL_INCIDENT_GRACE_MS = 30_000
12+
const MIN_MEANINGFUL_TRACED_ACTIVITY_DURATION_MS = 50
1213

1314
type MainProcessStallIncident = {
1415
startedAtMs: number
@@ -226,10 +227,16 @@ export const attachMainWindowResponsivenessDiagnostics = (
226227
return
227228
}
228229
const overlappingActivity = snapshot.activity.longest
229-
const stallClassification = overlappingActivity
230+
const meaningfulOverlappingActivity =
231+
overlappingActivity &&
232+
(overlappingActivity.pending ||
233+
overlappingActivity.durationMs >= MIN_MEANINGFUL_TRACED_ACTIVITY_DURATION_MS)
234+
? overlappingActivity
235+
: undefined
236+
const stallClassification = meaningfulOverlappingActivity
230237
? {
231238
kind: 'tracked-main-thread-activity-overlap',
232-
activity: overlappingActivity
239+
activity: meaningfulOverlappingActivity
233240
}
234241
: processCpuRatio >= 0.7
235242
? {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest'
2+
import { startHorizontalBrowseUserTiming } from './horizontalBrowseUserTiming'
3+
4+
afterEach(() => {
5+
vi.restoreAllMocks()
6+
})
7+
8+
describe('startHorizontalBrowseUserTiming', () => {
9+
it('clears its measure after recording it so repeated render work cannot retain entries', () => {
10+
const mark = vi.spyOn(performance, 'mark')
11+
const measure = vi.spyOn(performance, 'measure')
12+
const clearMeasures = vi.spyOn(performance, 'clearMeasures')
13+
14+
const finish = startHorizontalBrowseUserTiming('frkb:hb:test')
15+
finish()
16+
17+
expect(mark).toHaveBeenCalledTimes(2)
18+
expect(measure).toHaveBeenCalledWith('frkb:hb:test', expect.any(String), expect.any(String))
19+
expect(clearMeasures).toHaveBeenCalledWith('frkb:hb:test')
20+
})
21+
})

‎src/renderer/src/composables/horizontalBrowse/horizontalBrowseUserTiming.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ export const startHorizontalBrowseUserTiming = (name: string) => {
1919
performance.measure(name, startMark, endMark)
2020
performance.clearMarks(startMark)
2121
performance.clearMarks(endMark)
22+
performance.clearMeasures(name)
2223
}
2324
}

‎src/renderer/src/main.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
watchUiSettings
1515
} from '@renderer/utils/uiSettingsStorage'
1616
import { installConsoleLogBridge } from '@renderer/utils/installConsoleLogBridge'
17+
import { installRendererStallDiagnostics } from '@renderer/utils/rendererStallDiagnostics'
1718
import type { ICuratedArtistFavorite } from 'src/types/globals'
1819

1920
declare global {
@@ -26,6 +27,7 @@ const pinia = createPinia()
2627
const app = createApp(App)
2728

2829
installConsoleLogBridge('main-window')
30+
installRendererStallDiagnostics()
2931

3032
app.directive('dialog-drag', dialogDrag)
3133

0 commit comments

Comments
 (0)