feat(installer-rp): auto-tune music concurrency to CPU core count

os.cpus().length 기준 동시 다운로드 수를 자동 결정:
- 2 코어 이하 → 2 동시
- 3~4 코어 → 3 동시
- 5~8 코어 → 4 동시
- 9 코어 이상 → 5 동시 (YouTube throttle 때문에 상한)

환경변수 MUSIC_CONCURRENCY 로 강제 오버라이드 가능(상한 8).
설치 로그에 감지된 코어 수와 선택된 동시성 노출.

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

View File

@@ -4,6 +4,7 @@ import https from 'node:https'
import path from 'node:path' import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import fsp from 'node:fs/promises' import fsp from 'node:fs/promises'
import os from 'node:os'
import { URL } from 'node:url' import { URL } from 'node:url'
import type { ChildProcess } from 'node:child_process' import type { ChildProcess } from 'node:child_process'
import type { Manifest, PackDefinition, PackList } from '../shared/types.js' import type { Manifest, PackDefinition, PackList } from '../shared/types.js'
@@ -27,8 +28,23 @@ interface RpInstallerState {
activeChildren: Set<ChildProcess> activeChildren: Set<ChildProcess>
} }
/** 동시 yt-dlp 프로세스 수. 너무 높이면 유튜브가 throttle. */ /**
const MUSIC_CONCURRENCY = 3 * 동시 yt-dlp 프로세스 수를 CPU 코어 수로 자동 결정.
* - yt-dlp + ffmpeg 변환이 CPU 바운드라 코어 수가 가장 좋은 프록시.
* - 유튜브가 IP 단위로 throttle 걸기 때문에 5 이상은 효과 없음 → 상한 5.
* - 환경변수 MUSIC_CONCURRENCY 로 강제 오버라이드 가능.
*/
function pickMusicConcurrency(): number {
const override = Number(process.env.MUSIC_CONCURRENCY)
if (Number.isFinite(override) && override >= 1) {
return Math.min(8, Math.floor(override))
}
const cores = os.cpus()?.length ?? 4
if (cores <= 2) return 2
if (cores <= 4) return 3
if (cores <= 8) return 4
return 5
}
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'
@@ -199,10 +215,13 @@ 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. 음악 다운로드 (MUSIC_CONCURRENCY 개씩 병렬, 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 })
sendLog(`음악 다운로드 시작 (${musicTotal}곡, 동시 ${MUSIC_CONCURRENCY}개)`) const concurrency = pickMusicConcurrency()
const cpuCount = os.cpus()?.length ?? 0
sendLog(`CPU 코어 ${cpuCount}개 감지 → 동시 다운로드 ${concurrency}`)
sendLog(`음악 다운로드 시작 (${musicTotal}곡, 동시 ${concurrency}개)`)
// 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias. // 클로저 안에서 narrowing 이 풀리지 않도록 로컬 alias.
const musicList = pack.list.music const musicList = pack.list.music
@@ -252,7 +271,7 @@ ipcMain.handle('rp:install:start', async (): Promise<{ resourcepackPath: string
} }
} }
const workerCount = Math.min(MUSIC_CONCURRENCY, musicTotal) const workerCount = Math.min(concurrency, musicTotal)
const workers: Promise<void>[] = [] const workers: Promise<void>[] = []
for (let w = 0; w < workerCount; w++) workers.push(musicWorker()) for (let w = 0; w < workerCount; w++) workers.push(musicWorker())
await Promise.all(workers) await Promise.all(workers)