feat(installer-rp): stagger music download starts (2.5s gap)

동시 N개를 모두 t=0 에 띄우면 카드들이 0% 에서 같이 멈춰있다가
한꺼번에 100% 가 되는 "정지된 듯한" 구간이 보였음.

이제 새 다운로드 시작 사이에 최소 2.5초 간격을 두어, 어떤 카드는 70%,
어떤 카드는 30% 식으로 항상 진행 흐름이 이어지게 만든다.

- musicStartChain 으로 acquire 직렬화 → race-free
- nextMusicStartAt 으로 마지막 시작 시점 추적
- 동시성(코어 수 기반) 자체는 그대로 유지, 시작 시점만 분산

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-13 00:24:33 +09:00
parent bb43e8b125
commit df3d0a5cda

View File

@@ -46,6 +46,28 @@ function pickMusicConcurrency(): number {
return 5 return 5
} }
/**
* 새 다운로드 시작 사이의 최소 간격(ms).
* - 동시 N개를 모두 t=0 에 시작하면 카드들이 0% 에서 같이 정지된 듯 보임.
* - 시차를 두고 시작하면 "1번 끝남 → 4번 시작 → 2번 끝남 → 5번 시작" 식으로
* 유저 입장에서 항상 뭔가 새로 시작/완료되는 흐름이 보임.
* - 너무 길면 동시성 이득을 깎아먹음. 2.5s 가 체감/속도 균형점.
*/
const MUSIC_START_STAGGER_MS = 2500
/** start-gate. 여러 worker 가 동시에 acquire 해도 직렬화되어 순차 통과. */
let musicStartChain: Promise<void> = Promise.resolve()
let nextMusicStartAt = 0
function acquireMusicStartSlot(): Promise<void> {
const slot = musicStartChain.then(async () => {
const wait = Math.max(0, nextMusicStartAt - Date.now())
if (wait > 0) await new Promise<void>((r) => setTimeout(r, wait))
nextMusicStartAt = Date.now() + MUSIC_START_STAGGER_MS
})
musicStartChain = slot.catch(() => {})
return slot
}
const DEFAULT_MANIFEST_URL = process.env.MANIFEST_URL ?? 'http://127.0.0.1:3000/manifest.json' const DEFAULT_MANIFEST_URL = process.env.MANIFEST_URL ?? 'http://127.0.0.1:3000/manifest.json'
const state: RpInstallerState = { const state: RpInstallerState = {
@@ -215,13 +237,15 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
sendProgress({ phase: 'prep', message: '준비 완료', done: true }) sendProgress({ phase: 'prep', message: '준비 완료', done: true })
throwIfCancelled() throwIfCancelled()
// 2-2. 음악 다운로드 (CPU 코어 수 기반 자동 동시 다운로드, ogg 변환) // 2-2. 음악 다운로드 (CPU 코어 수 기반 자동 동시 다운로드, 시차 출발, ogg 변환)
const musicDir = path.join(tempRoot, 'music') const musicDir = path.join(tempRoot, 'music')
await fsp.mkdir(musicDir, { recursive: true }) await fsp.mkdir(musicDir, { recursive: true })
const concurrency = pickMusicConcurrency() const concurrency = pickMusicConcurrency()
const cpuCount = os.cpus()?.length ?? 0 const cpuCount = os.cpus()?.length ?? 0
// 첫 음악은 즉시 시작 가능하도록 base 를 현재 시각으로.
nextMusicStartAt = Date.now()
sendLog(`CPU 코어 ${cpuCount}개 감지 → 동시 다운로드 ${concurrency}`) sendLog(`CPU 코어 ${cpuCount}개 감지 → 동시 다운로드 ${concurrency}`)
sendLog(`음악 다운로드 시작 (${musicTotal}곡, 동시 ${concurrency}개)`) sendLog(`음악 다운로드 시작 (${musicTotal}곡, 동시 ${concurrency}, 시차 ${MUSIC_START_STAGGER_MS}ms)`)
// 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias. // 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias.
const musicList = pack.list.music const musicList = pack.list.music
@@ -231,6 +255,9 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
if (state.cancelRequested) return if (state.cancelRequested) return
const i = nextIndex++ const i = nextIndex++
if (i >= musicTotal) return if (i >= musicTotal) return
// 시차 게이트: 새 다운로드 시작은 직전 시작과 최소 MUSIC_START_STAGGER_MS 간격을 둠.
await acquireMusicStartSlot()
if (state.cancelRequested) return
const entry = musicList[i] const entry = musicList[i]
const idx = i + 1 const idx = i + 1
sendLog(`${idx}번 노래 다운로드 시작`) sendLog(`${idx}번 노래 다운로드 시작`)