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:
@@ -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}번 노래 다운로드 시작`)
|
||||||
|
|||||||
Reference in New Issue
Block a user