개발 회고

[PonsWarp] - 파이프라인 병렬화가 실제로 바꾼 것

AC 2026. 6. 24. 00:22

PonsWarp에서 파이프라인 병렬화는 멋진 성능 용어로 시작한 기능이 아니었습니다.

더 정확히 말하면, WebRTC DataChannel이 열려 있는데도 sender worker가 다음 청크를 준비하느라 멈칫하는 순간을 줄이려는 시도였습니다. 중심 증거는 2025년 11월 27일의 949d921입니다. 커밋 문구부터 Phase 2 - 파이프라인 병렬화 + 이중 버퍼링 + 청크 풀링이었습니다.

PonsWarp old serial path versus pipeline parallel path
Phase 2는 파일 읽기와 패킷 준비와 전송 요청이 한 줄에서 서로 기다리던 시간을 줄이려 했습니다.

⚙️ 이전 병목은 네트워크 바깥에도 있었습니다

💡 병렬화가 바꾼 것은 “한 번에 더 많이 보내기”가 아니라, 파일 읽기와 패킷 준비와 전송 요청이 서로를 덜 기다리게 만든 것입니다.

대용량 전송에서 가장 먼저 의심하는 병목은 네트워크입니다. 하지만 PonsWarp 초반의 병목은 네트워크만이 아니었습니다. 파일을 slice하고, ArrayBuffer로 읽고, 헤더를 붙이고, worker에서 main thread로 넘기고, 다시 DataChannel에 실어 보내는 과정 전체가 하나의 리듬을 만들었습니다.

6e635f3에서 Push 방식과 AIMD가 들어오면서 sender는 요청을 기다리기만 하는 구조에서 벗어났습니다. 그래도 worker가 청크를 만드는 시간이 전송 요청과 같은 줄에 놓이면, 빠른 링크에서는 DataChannel이 비어 있는 시간이 생깁니다. 사용자는 내부 작업을 보지 못하지만, 진행률이 미세하게 끊기거나 속도 표시가 출렁이는 것으로 그 공백을 느낍니다.

949d921의 역사 파일 workers/file-sender.worker.v2.ts는 이 문제를 직접 겨냥합니다. 파일 상단 주석부터 Phase 2, 파이프라인 병렬화, 이중 버퍼링, 청크 풀링을 선언합니다. CHUNK_SIZE는 128KB, BUFFER_SIZE는 버퍼당 4MB, POOL_SIZE는 64, PREFETCH_BATCH는 8이었습니다.

🔁 이중 버퍼는 대기 시간을 숨기는 장치였습니다

Phase 2의 핵심은 전송 요청이 들어온 뒤 청크를 만드는 것에서 다음 요청 전에 청크 일부를 준비해 두는 것으로 이동한 데 있습니다.

Phase 2의 핵심은 DoubleBuffer입니다. 활성 버퍼에서는 전송 요청에 응답할 청크를 꺼내고, 비활성 버퍼에는 prefetchBatch()가 다음 청크를 채웁니다. 활성 버퍼가 비면 swap()으로 역할을 바꿉니다. 구조만 보면 단순하지만 제품 체감에는 의미가 큽니다.

기존 경로에서는 processBatch가 청크를 만들기 위해 파일 읽기를 기다려야 했습니다. Phase 2에서는 triggerPrefetch()가 비동기 Promise로 프리페치를 시작하고 즉시 반환합니다. 그러면 다음 process-batch 요청은 이미 채워진 active buffer에서 청크를 동기적으로 꺼낼 가능성이 생깁니다.

PonsWarp pipeline stages and queue boundaries
파이프라인은 sender 내부 대기 시간을 줄이지만, 단계 사이 큐 경계를 새 제품 문제로 만듭니다.

📦 청크 풀링은 GC와 싸우는 방식이었습니다

949d921에는 ChunkPool도 들어옵니다. acquire()로 기존 Uint8Array를 꺼내 쓰고, release()로 다시 pool에 넣습니다. 대용량 파일을 128KB 단위로 계속 자르면 객체 생성과 해제가 반복됩니다. 브라우저는 결국 GC를 해야 하고, GC pause는 전송 중 “앱이 멈춘 것 같은” 체감으로 나타납니다.

다만 이 구현은 완벽한 zero-copy가 아니었습니다. Phase 2에서도 실제 반환 시점에는 결과 ArrayBuffer를 새로 만들고 데이터를 복사합니다. 이후 현재 코드가 pons-core-wasmZeroCopyPacketPool과 WASM 메모리 경로로 내려간 것은, 이 시기의 청크 풀링이 방향은 맞았지만 경계가 아직 브라우저 객체 모델 안에 있었기 때문으로 읽을 수 있습니다.

🧱 병렬 단계가 생기자 큐 경계도 제품 문제가 됐습니다

파이프라인 병렬화는 단계 사이에 큐를 만듭니다. 비활성 버퍼, 활성 버퍼, DataChannel bufferedAmount, receiver worker, writer queue가 모두 서로 다른 속도로 움직입니다. 어느 한 구간이 빠르면 다음 구간을 압박하고, 어느 한 구간이 느리면 앞 구간이 쌓입니다.

sender가 덜 놀게 만드는 순간, receiver와 브라우저 큐를 더 쉽게 압박한다는 새로운 문제가 생겼습니다.

현재 src/utils/constants.ts의 주석은 이 후속 판단을 솔직하게 남깁니다. DataChannel bufferedAmount는 sender 로컬 큐일 뿐 receiver 저장 완료를 의미하지 않는다고 적고, 과도한 8~16MB 파이프라인이 실제 브라우저에서 메모리와 GC 압박, 진행 정지를 만들 수 있다고 경고합니다. 현재 값도 MAX_BUFFERED_AMOUNTHIGH_WATER_MARK를 4MB로 제한하고, BATCH_SIZE_MAX를 1로 묶어 둡니다.

📐 시스템 관점에서는 전송 엔진이 더 분해됐습니다

PonsWarp의 4층 구조로 보면 이 변화는 브라우저 전송 엔진 층에서 일어났습니다. 위에는 React UI와 전송 상태 표시가 있고, 중간에는 swarmManager와 sender worker가 있으며, 아래에는 WebRTC DataChannel과 receiver 저장 계층이 있습니다. Phase 2는 sender worker 내부를 다시 파일 읽기, 패킷 생성, 버퍼링, batch 응답으로 쪼갰습니다.

현재 코드에도 그 흔적은 남아 있습니다. src/workers/file-sender.worker.ts에는 DoubleBuffer, triggerPrefetch(), prefetchBatch(), processBatch()가 남아 있고, AdaptiveConfigprefetchBatch 값을 받습니다. 다만 src/utils/constants.ts에서는 PREFETCH_BUFFER_SIZE가 0이고 BATCH_SIZE_MAX가 1입니다. 구조는 남았지만 기본 정책은 훨씬 보수적으로 돌아선 것입니다.

PonsWarp pipeline parallelism tradeoffs
Phase 2는 속도 체감을 살렸지만, 이후 bounded queue와 backpressure가 필요해진 이유도 함께 만들었습니다.

⚖️ 제품 관점에서는 빠름보다 예측 가능성이 중요해졌습니다

사용자에게 파이프라인 병렬화는 prefetchBatch가 생겼다는 뜻이 아닙니다. 사용자가 보는 것은 전송 버튼을 누른 뒤 진행률이 살아 있는지, 속도가 갑자기 0처럼 보이지 않는지, 완료 표시가 실제 저장 완료와 어긋나지 않는지입니다.

Phase 2는 앞의 두 문제에는 도움을 줬습니다. sender가 청크 준비 때문에 덜 굶으면 진행률과 속도 표시가 더 자연스럽게 움직입니다. 그러나 마지막 문제, 즉 실제 수신 저장 완료와의 일치는 별개의 문제였습니다. sender 쪽 파이프라인이 잘 돈다고 해서 receiver disk write가 끝난 것은 아닙니다.

🧠 지금 돌아보는 판단

949d921은 PonsWarp가 전송을 단일 루프가 아니라 생산자와 소비자가 있는 시스템으로 보기 시작한 커밋입니다. 파일 읽기는 생산자이고, DataChannel 전송은 소비자이며, receiver 저장 계층은 또 다른 소비자입니다. 생산자를 빠르게 만드는 것만으로는 제품이 안정되지 않습니다. 소비자의 속도와 실패 신호를 같이 봐야 합니다.

PonsWarp에서 파이프라인 병렬화가 실제로 바꾼 것은 처리량 숫자 하나가 아닙니다. 전송 엔진을 기다림의 연쇄에서 큐와 피드백의 시스템으로 바꾼 것입니다. 그 순간부터 성능 최적화는 “더 밀어 넣기”가 아니라 “어디까지 미리 준비하고, 어디서 멈출 것인가”의 문제가 됐습니다.

📚 읽은 코드
PonsWarp commit 949d921 — Phase 2 파이프라인 병렬화, 이중 버퍼링, 청크 풀링
PonsWarp/workers/file-sender.worker.v2.ts at 949d921 — DoubleBuffer, ChunkPool, triggerPrefetch, prefetchBatch, processBatch
PonsWarp commit 3b58612 — Phase 1 성능 최적화 흐름
PonsWarp commit 541dd0c — Phase 3 멀티 채널 및 네트워크 적응형 제어
PonsWarp/src/workers/file-sender.worker.ts:40-110,551-577,974-1023 — 현재 sender worker의 double buffer/prefetch/process batch 구조
PonsWarp/src/utils/constants.ts:18-47 — 안정성 우선 DataChannel queue, batch, prefetch 보수화
PonsWarp/src/utils/transferFlowControl.ts — 준비된 조건에서만 worker 청크를 더 요청하는 흐름 제어
LIST