@@ -29,6 +29,18 @@ pub struct DaemonConfig {
2929 pub extract_threads : usize ,
3030}
3131
32+ impl DaemonConfig {
33+ /// Number of parallel extract workers. Each worker pulls wheels from the
34+ /// channel and extracts independently, preventing small wheels from queuing
35+ /// behind large ones. Each worker gets `extract_threads / workers` rayon threads.
36+ pub fn extract_workers ( & self ) -> usize {
37+ // 4 workers is a good default: allows 4 wheels to extract simultaneously,
38+ // each with ~6-7 threads on a 26-core machine.
39+ // Minimum 1, cap at extract_threads (no point having more workers than threads).
40+ 4 . min ( self . extract_threads )
41+ }
42+ }
43+
3244impl Default for DaemonConfig {
3345 fn default ( ) -> Self {
3446 Self {
@@ -121,10 +133,11 @@ impl DaemonEngine {
121133 . pool_max_idle_per_host ( config. parallel_downloads )
122134 . build ( ) ?;
123135
124- // Channel: downloaded wheels flow from download workers → extract worker.
125- // Small capacity (4) provides backpressure — if extraction is slow,
126- // downloads pause rather than filling disk with temp files.
127- let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < DownloadedWheel > ( 4 ) ;
136+ // Channel: downloaded wheels flow from download workers → extract workers.
137+ // Capacity = 2 * extract_workers provides enough buffering for workers to
138+ // stay busy while providing backpressure to avoid filling disk with temp files.
139+ let num_workers = config. extract_workers ( ) ;
140+ let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < DownloadedWheel > ( num_workers * 2 ) ;
128141
129142 let tmp_dir = tempfile:: tempdir ( ) . context ( "failed to create temp dir" ) ?;
130143 let tmp_path = tmp_dir. path ( ) . to_path_buf ( ) ;
@@ -191,76 +204,95 @@ impl DaemonEngine {
191204 drop ( tx) ;
192205
193206 // === Extract stage ===
194- // Single blocking loop: receives downloaded wheels, extracts each immediately.
195- // Extraction uses all extract_threads for parallelism within a single wheel.
196- let site_packages = config. site_packages . clone ( ) ;
197- let ext_threads = config. extract_threads ;
198- let stats = self . stats . clone ( ) ;
199- let completion = self . completion . clone ( ) ;
200- let queue = self . queue . clone ( ) ;
201- let total_wheels = self . total_wheels ;
202-
203- let extract_handle = tokio:: task:: spawn_blocking ( move || {
204- let rx = rx;
205- // blocking_recv in a loop — channel closes when all downloads finish
206- let mut rx = rx;
207- while let Some ( downloaded) = rx. blocking_recv ( ) {
208- let dist = downloaded. spec . distribution . clone ( ) ;
209- let extract_start = Instant :: now ( ) ;
210-
211- let result = extract:: extract_wheel_atomic (
212- & downloaded. path ,
213- & site_packages,
214- & dist,
215- ext_threads,
216- true ,
217- & stats,
218- ) ;
219-
220- let ( lock, cvar) = & * completion;
221-
222- match result {
223- Ok ( ( ) ) => {
224- let elapsed = extract_start. elapsed ( ) ;
225- tracing:: info!(
226- "[{dist}] extracted in {:.1}s" ,
227- elapsed. as_secs_f64( )
228- ) ;
229-
230- {
231- let mut q = queue. lock ( ) . unwrap ( ) ;
232- q. mark_done ( & dist) ;
207+ // Multiple extract workers pull from the same channel, extracting different
208+ // wheels in parallel. Each worker gets a share of the total extract threads.
209+ // This prevents small wheels from queuing behind large ones.
210+ let num_extract_workers = config. extract_workers ( ) ;
211+ let threads_per_worker = ( config. extract_threads / num_extract_workers) . max ( 1 ) ;
212+ let rx = Arc :: new ( tokio:: sync:: Mutex :: new ( rx) ) ;
213+
214+ let mut extract_handles = Vec :: new ( ) ;
215+ for worker_id in 0 ..num_extract_workers {
216+ let site_packages = config. site_packages . clone ( ) ;
217+ let stats = self . stats . clone ( ) ;
218+ let completion = self . completion . clone ( ) ;
219+ let queue = self . queue . clone ( ) ;
220+ let total_wheels = self . total_wheels ;
221+ let rx = rx. clone ( ) ;
222+
223+ let handle = tokio:: task:: spawn_blocking ( move || {
224+ loop {
225+ // Lock channel briefly to receive next wheel
226+ let downloaded = {
227+ let mut rx = rx. blocking_lock ( ) ;
228+ rx. blocking_recv ( )
229+ } ;
230+ let downloaded = match downloaded {
231+ Some ( d) => d,
232+ None => break , // channel closed
233+ } ;
234+
235+ let dist = downloaded. spec . distribution . clone ( ) ;
236+ let extract_start = Instant :: now ( ) ;
237+
238+ tracing:: debug!( "[{dist}] extract worker {worker_id} starting" ) ;
239+
240+ let result = extract:: extract_wheel_atomic (
241+ & downloaded. path ,
242+ & site_packages,
243+ & dist,
244+ threads_per_worker,
245+ true ,
246+ & stats,
247+ ) ;
248+
249+ let ( lock, cvar) = & * completion;
250+
251+ match result {
252+ Ok ( ( ) ) => {
253+ let elapsed = extract_start. elapsed ( ) ;
254+ tracing:: info!(
255+ "[{dist}] extracted in {:.1}s (worker {worker_id})" ,
256+ elapsed. as_secs_f64( )
257+ ) ;
258+
259+ {
260+ let mut q = queue. lock ( ) . unwrap ( ) ;
261+ q. mark_done ( & dist) ;
262+ }
263+
264+ let mut state = lock. lock ( ) . unwrap ( ) ;
265+ state. done . insert ( dist) ;
266+ if state. done . len ( ) + state. failed . len ( ) >= total_wheels {
267+ state. all_finished = true ;
268+ }
269+ cvar. notify_all ( ) ;
233270 }
234-
235- let mut state = lock. lock ( ) . unwrap ( ) ;
236- state. done . insert ( dist) ;
237- if state. done . len ( ) + state. failed . len ( ) >= total_wheels {
238- state. all_finished = true ;
271+ Err ( e) => {
272+ let err_msg = format ! ( "{e:#}" ) ;
273+ tracing:: error!( "[{dist}] extraction failed: {err_msg}" ) ;
274+
275+ {
276+ let mut q = queue. lock ( ) . unwrap ( ) ;
277+ q. mark_failed ( & dist) ;
278+ }
279+
280+ let mut state = lock. lock ( ) . unwrap ( ) ;
281+ state. failed . insert ( dist, err_msg) ;
282+ if state. done . len ( ) + state. failed . len ( ) >= total_wheels {
283+ state. all_finished = true ;
284+ }
285+ cvar. notify_all ( ) ;
239286 }
240- cvar. notify_all ( ) ;
241287 }
242- Err ( e) => {
243- let err_msg = format ! ( "{e:#}" ) ;
244- tracing:: error!( "[{dist}] extraction failed: {err_msg}" ) ;
245-
246- {
247- let mut q = queue. lock ( ) . unwrap ( ) ;
248- q. mark_failed ( & dist) ;
249- }
250288
251- let mut state = lock. lock ( ) . unwrap ( ) ;
252- state. failed . insert ( dist, err_msg) ;
253- if state. done . len ( ) + state. failed . len ( ) >= total_wheels {
254- state. all_finished = true ;
255- }
256- cvar. notify_all ( ) ;
257- }
289+ // Clean up temp file
290+ let _ = std:: fs:: remove_file ( & downloaded. path ) ;
258291 }
292+ } ) ;
259293
260- // Clean up temp file
261- let _ = std:: fs:: remove_file ( & downloaded. path ) ;
262- }
263- } ) ;
294+ extract_handles. push ( handle) ;
295+ }
264296
265297 // === Wait for download failures ===
266298 // Collect download errors and mark them as failed
@@ -277,8 +309,10 @@ impl DaemonEngine {
277309 }
278310 }
279311
280- // Wait for extract worker to finish
281- extract_handle. await ?;
312+ // Wait for all extract workers to finish
313+ for handle in extract_handles {
314+ handle. await ?;
315+ }
282316
283317 // Mark all finished
284318 {
0 commit comments