Skip to main content

trg/
sched.rs

1//! Bounded worker pool. Concurrency lives at the archive boundary and nowhere
2//! else, because gzip has no index: member N requires inflating 1..N-1.
3//!
4//! # Ordering
5//!
6//! Output stays in argument order by default: these archives are days, and
7//! argument order is chronological order. That is achieved by holding a
8//! completed archive's buffer until every earlier archive has been emitted —
9//! **not** by joining all workers first. A finished job whose predecessors are
10//! already out is written and freed immediately, so nothing waits on the
11//! slowest archive to be handed to `emit`.
12//!
13//! # What actually bounds memory
14//!
15//! Three separate things, and it is worth being precise about which does what,
16//! because `spill_bytes` alone does far less than its name suggests:
17//!
18//! 1. **Buffer recycling.** Output buffers are owned by this module and pooled.
19//!    A worker takes one, fills it, and the collector `clear()`s it after
20//!    emitting and returns it to the pool. Allocating a fresh buffer per
21//!    archive instead makes peak RSS climb with the *archive count*: each one
22//!    is grown by a realloc chain to that archive's whole match volume and then
23//!    freed, and the allocator does not hand the same span back for the next
24//!    one. Measured at `-j1` on six archives of 120k matching lines: 84 MB
25//!    per-job vs. 22 MB pooled.
26//!
27//!    At `workers == 1` exactly one buffer exists for the whole run, which is
28//!    the case that matters most since `-j1` is the default. In the pool path
29//!    the *count* of live buffers is not capped, and deliberately so: holding
30//!    `k` results in order requires `k` buffers, so a hard cap would be a cap
31//!    on the ordering window. It is not needed either — an unwritten
32//!    `termcolor::Buffer` is an empty `Vec` that has allocated nothing, and the
33//!    ones that did allocate are bounded in *bytes* by `spill_bytes` below.
34//!    What the pool guarantees is that a buffer's allocation is reused rather
35//!    than rebuilt, which is what the realloc chain cost.
36//! 2. **Per-worker state.** `make_worker` is called once per worker thread, not
37//!    once per job, for the same reason — see [`run`].
38//! 3. **`spill_bytes`.** This bounds only the buffers *parked* in `slots`
39//!    waiting for their turn in the order. It does **not** bound peak RSS: the
40//!    `workers` buffers currently being filled, and the batch the collector has
41//!    drained but not yet emitted, are outside the count. Treat it as "how far
42//!    out of order are we willing to hold results", not as a memory ceiling. A
43//!    real run peaked at 286 MB under a 64 MB cap.
44//!
45//! When parked buffers do exceed `spill_bytes`, they are flushed out of order
46//! with a note on stderr. Holding results in order is worth a lot, but not
47//! unboundedly much when a single slow archive is pinning the rest.
48//!
49//! # Design
50//!
51//! One `Mutex` guards the slot vector, the buffer pool, and a little
52//! bookkeeping; one `Condvar` wakes the collector. Workers only ever *deposit*:
53//! pull the next index with a `fetch_add`, do the work outside the lock, then
54//! take the lock just long enough to move the finished [`Done`] into its slot
55//! and notify. Nothing a worker does can block on another worker.
56//!
57//! The calling thread is the collector. It owns `head` and `emitted`, neither
58//! of which is shared, and it calls `emit` *outside* the lock so a slow stdout
59//! — a pipe into `head(1)`, say — never stalls a worker mid-archive.
60//!
61//! Emission order is correct because a slot is written exactly once (position
62//! `p` is handed to exactly one worker by the atomic counter) and read exactly
63//! once (only the collector takes, and only from a slot it then marks emitted).
64//! In sorted mode the collector walks `order`, so it emits the job with the
65//! next-lowest [`Job::index`] only once every lower index has gone out. The one
66//! deliberate exception is the spill above.
67//!
68//! # Panics and early stop
69//!
70//! A panicking job must not be able to hang the process — during an incident a
71//! hang looks like a slow search and gets waited on, which is strictly worse
72//! than a crash. Four independent mechanisms guarantee it cannot, one per place
73//! a thread can leave unexpectedly:
74//!
75//! - [`catch_unwind`](std::panic::catch_unwind) around the call to `work`,
76//!   which turns a panic into an ordinary error on that archive, and a second
77//!   one around `make_worker`, which is likewise the caller's closure.
78//! - a `Completion` drop guard that advances the finished count and wakes the
79//!   collector even if a worker unwinds anyway, once it has claimed a job.
80//! - a `WorkerLife` drop guard covering the rest of the thread's life —
81//!   everything before and between jobs, where `Completion` does not exist yet.
82//!   When the last worker goes with jobs still unclaimed, the collector stops
83//!   waiting and reports those archives as unread, so the run is a 2.
84//! - a `Teardown` drop guard on the collector, releasing workers parked on
85//!   backpressure when it leaves, including by unwinding out of `emit`.
86//!
87//! Each guards a different exit; none of them is redundant with the others, and
88//! the module has hit this failure shape three times, so a new path out of a
89//! thread needs a guard rather than an argument that it cannot be taken.
90//!
91//! `emit` returns `io::Result`, and the first error stops the run: no further
92//! job is started and [`run`] returns the error. That is what makes
93//! `trg ... | head -5` stop searching instead of grinding through a whole
94//! month of archives writing into a closed pipe.
95
96use std::collections::VecDeque;
97use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
98use std::sync::{Condvar, Mutex};
99
100pub struct Job {
101    pub index: usize,
102    pub path: std::path::PathBuf,
103}
104
105pub struct Done {
106    pub index: usize,
107    pub buffer: termcolor::Buffer,
108    pub outcome: crate::archive::Outcome,
109}
110
111/// How the pool runs, as opposed to what it runs.
112pub struct Config {
113    /// Archives searched concurrently. `1` takes a dedicated sequential path.
114    pub workers: usize,
115    /// Emit in [`Job::index`] order rather than completion order.
116    pub sorted: bool,
117    /// How many bytes of *parked* output may wait for their turn before the
118    /// order is abandoned. Not a memory ceiling — see the module docs.
119    pub spill_bytes: usize,
120}
121
122/// Everything the workers and the collector share. The lock is held only for
123/// the moves in and out of `slots` and `pool`.
124struct Shared {
125    /// One slot per job position. `Some` means finished and not yet emitted.
126    slots: Vec<Option<Done>>,
127    /// Positions in completion order, for `--no-sort`.
128    ready: VecDeque<usize>,
129    /// Output buffers not currently in use, kept so their allocations are
130    /// reused instead of being rebuilt per archive.
131    pool: Vec<termcolor::Buffer>,
132    /// Bytes currently parked in `slots`, waiting for their turn in the order.
133    held: usize,
134    /// Bytes the collector has drained out of `slots` but not yet emitted and
135    /// recycled. Counted separately and added to `held` for backpressure,
136    /// because draining moves bytes out of `slots` without freeing anything —
137    /// missing this is what let live buffers track the job count.
138    batch_bytes: usize,
139    /// Jobs finished so far, so the collector knows when to stop waiting.
140    finished: usize,
141    /// Worker threads still running. Decremented by [`WorkerLife`] on every
142    /// exit path, normal or unwinding, so that "nobody is left to finish the
143    /// outstanding jobs" is a state the collector can observe rather than a
144    /// wait it never wakes from.
145    live_workers: usize,
146}
147
148/// Advances the finished count and wakes the collector on drop, so that a
149/// worker which unwinds still counts as having completed its job.
150///
151/// Without this, a panicking worker would leave `finished` short of `n`
152/// forever: the collector would wait on a notification that never comes, and
153/// `thread::scope` would never return. Belt and braces alongside the
154/// `catch_unwind` in [`run_job`], which already stops a panic in `work` itself;
155/// this covers everything else on the path.
156struct Completion<'a> {
157    state: &'a Mutex<Shared>,
158    wake: &'a Condvar,
159    armed: bool,
160}
161
162impl Drop for Completion<'_> {
163    fn drop(&mut self) {
164        if !self.armed {
165            return;
166        }
167        // Never `unwrap` here: panicking inside a `Drop` that is itself running
168        // during an unwind aborts the process. A poisoned lock is handled by
169        // waking the collector anyway, which will then fail on its own lock and
170        // propagate a panic — noisy, but not a hang.
171        if let Ok(mut st) = self.state.lock() {
172            st.finished += 1;
173        }
174        self.wake.notify_all();
175    }
176}
177
178/// Decrements the live-worker count and wakes the collector on drop.
179///
180/// [`Completion`] covers a worker that unwinds *after* it has claimed a job.
181/// This covers the rest of the thread's life, which [`Completion`] cannot: it
182/// is constructed as the thread's first act, before `make_worker()` and before
183/// the first `state.lock()`, so a panic in either — or in `space.wait()` on a
184/// poisoned lock — still tells the collector that this worker is gone.
185///
186/// Without it the collector waits at `wake.wait` for a notification no living
187/// thread will send, and `thread::scope` never returns: a hang, which during an
188/// incident looks like a slow search and gets waited on. This is the third
189/// instance of that shape in this module, after the panicking worker and the
190/// collector unwinding out of `emit`, so it is guarded structurally rather than
191/// by arguing that the current caller does not panic — `run` is public API and
192/// `make_worker` is the caller's closure.
193struct WorkerLife<'a> {
194    state: &'a Mutex<Shared>,
195    wake: &'a Condvar,
196}
197
198impl Drop for WorkerLife<'_> {
199    fn drop(&mut self) {
200        // Never `unwrap`: same discipline as `Completion` — this `Drop` runs
201        // during an unwind, and panicking there aborts the process.
202        if let Ok(mut st) = self.state.lock() {
203            st.live_workers -= 1;
204        }
205        self.wake.notify_all();
206    }
207}
208
209/// Releases every worker parked on backpressure when the collector leaves,
210/// *including* when it leaves by unwinding out of `emit`.
211///
212/// The exact mirror of [`Completion`] at the other end of the pipe. Without it,
213/// a panic in `emit` skips the teardown, workers stay parked on `space`, and
214/// `thread::scope` blocks forever joining them — a hang, which is the one
215/// outcome worse than a crash. Fixing that on the worker side and not the
216/// collector side would not be defensible.
217struct Teardown<'a> {
218    state: &'a Mutex<Shared>,
219    space: &'a Condvar,
220    wake: &'a Condvar,
221    stop: &'a AtomicBool,
222}
223
224impl Drop for Teardown<'_> {
225    fn drop(&mut self) {
226        // Never `unwrap` here: this `Drop` runs during an unwind, and a panic
227        // inside it would abort the process. Same discipline as `Completion`.
228        if let Ok(mut st) = self.state.lock() {
229            // The `Done`s still in the collector's batch are dropped without
230            // passing through `recycle`, so their bytes would be counted
231            // against the backpressure predicate forever. Zeroing here makes
232            // the predicate correct rather than merely short-circuited, so the
233            // wakeup below does not depend on `stop` alone.
234            st.batch_bytes = 0;
235            // Stored while the lock is held so the mutex's release/acquire edge
236            // carries it too; the `Release` ordering covers the lock-free check
237            // at the top of the worker loop.
238            self.stop.store(true, Ordering::Release);
239        } else {
240            self.stop.store(true, Ordering::Release);
241        }
242        self.space.notify_all();
243        self.wake.notify_all();
244    }
245}
246
247/// Call `work`, converting a panic into an error on that archive.
248///
249/// The buffer is cleared on panic: a job that died mid-write may have left a
250/// partial line behind, and half a log line in the middle of ordered output is
251/// worse than none.
252///
253/// This is a no-op under `panic = "abort"`, which the release profile sets. It
254/// is still worth having: an abort is a crash, and a crash is a diagnosable
255/// outcome. The failure mode being prevented is the *hang*, and the
256/// `Completion` guard prevents that on both profiles.
257fn run_job<C, F>(
258    job: &Job,
259    ctx: &mut C,
260    buf: &mut termcolor::Buffer,
261    work: &F,
262) -> crate::archive::Outcome
263where
264    F: Fn(&Job, &mut C, &mut termcolor::Buffer) -> crate::archive::Outcome,
265{
266    // `AssertUnwindSafe` is honest here rather than a silencer: everything
267    // reachable is either this job's own scratch state, which is discarded on
268    // the panic path, or the shared slot vector, which this job has not touched
269    // yet — it deposits only after `work` returns.
270    let called = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| work(job, ctx, buf)));
271    match called {
272        Ok(o) => o,
273        Err(_) => {
274            buf.clear();
275            let mut o = crate::archive::Outcome::default();
276            o.errors.push(format!(
277                "{}: panicked while searching; skipped",
278                job.path.display()
279            ));
280            o
281        }
282    }
283}
284
285/// Run `work` over `jobs`, emitting each result through `emit`.
286///
287/// `make_worker` is called **once per worker thread**, not once per job. That
288/// is the correct granularity for anything a job needs but cannot share:
289/// `grep_searcher::Searcher` is not `Sync`, so it cannot be hoisted out of the
290/// pool entirely, but building one per *archive* is a real cost — its internal
291/// line buffer grows toward the configured heap limit and is thrown away every
292/// archive. Measured on 60 small archives: 288 MB per-job vs. 30 MB per-worker.
293/// At `workers == 1` this means exactly one is ever built.
294///
295/// `emit` borrows its [`Done`] rather than taking it, so the buffer can be
296/// recycled afterwards. The first `Err` it returns stops the run: no further
297/// job is started, and that error is returned.
298pub fn run<C, F, E>(
299    jobs: Vec<Job>,
300    cfg: Config,
301    make_buffer: impl Fn() -> termcolor::Buffer + Send + Sync,
302    make_worker: impl Fn() -> C + Send + Sync,
303    work: F,
304    mut emit: E,
305) -> std::io::Result<()>
306where
307    F: Fn(&Job, &mut C, &mut termcolor::Buffer) -> crate::archive::Outcome + Send + Sync,
308    E: FnMut(&Done) -> std::io::Result<()>,
309{
310    let Config { workers, sorted, spill_bytes } = cfg;
311    let n = jobs.len();
312    if n == 0 {
313        return Ok(());
314    }
315
316    // The sequential path holds nothing and needs no lock, which is already the
317    // bounded-memory ideal. Kept separate rather than folded into the pool:
318    // `-j1` is the production default and deserves no threads at all. One
319    // worker context and one buffer for the whole run.
320    if workers <= 1 {
321        let mut ctx = make_worker();
322        let mut buf = make_buffer();
323        for job in &jobs {
324            buf.clear();
325            let outcome = run_job(job, &mut ctx, &mut buf, &work);
326            let done = Done { index: job.index, buffer: buf, outcome };
327            let r = emit(&done);
328            buf = done.buffer;
329            r?;
330            // One pathological archive should not leave its whole match volume
331            // reserved for the rest of the run.
332            if buf.as_slice().len() > spill_bytes {
333                buf = make_buffer();
334            }
335        }
336        return Ok(());
337    }
338
339    // Emission order. Sorting by `Job::index` rather than by slot position
340    // means a caller that hands us indices which are not `0..n` still gets the
341    // order it asked for, and that `Done::index` is the one ordering key
342    // everywhere — including the spill loop. Stable, so equal indices keep
343    // argument order. For `main`, where index == position, this is the identity.
344    let mut order: Vec<usize> = (0..n).collect();
345    if sorted {
346        order.sort_by_key(|&p| jobs[p].index);
347    }
348
349    let live = workers.min(n);
350
351    let next = AtomicUsize::new(0);
352    let stop = AtomicBool::new(false);
353    let state = Mutex::new(Shared {
354        slots: (0..n).map(|_| None).collect(),
355        ready: VecDeque::new(),
356        pool: Vec::new(),
357        held: 0,
358        batch_bytes: 0,
359        finished: 0,
360        // Seeded before a single thread is spawned, so it only ever counts
361        // down: the collector cannot mistake "not started yet" for "all gone".
362        live_workers: live,
363    });
364    let wake = Condvar::new();
365    // Workers wait here for the collector to free output bytes. A second
366    // condvar on the *same* mutex, so a worker's check-and-wait is atomic
367    // against the collector's decrement and no wakeup can be missed.
368    let space = Condvar::new();
369
370    let mut write_err: Option<std::io::Error> = None;
371
372    std::thread::scope(|scope| {
373        // Armed before a single worker is spawned, so a panic from
374        // `scope.spawn` itself — the OS refusing a thread — still runs
375        // teardown instead of leaving spawned workers parked on
376        // backpressure forever. The normal exit and the unwinding exit
377        // run identical teardown either way, since this lives until the
378        // end of the same scope regardless of where it's declared.
379        let _teardown = Teardown { state: &state, space: &space, wake: &wake, stop: &stop };
380
381        for _ in 0..live {
382            let (next, stop, state, wake) = (&next, &stop, &state, &wake);
383            let space = &space;
384            let (jobs, work, make_buffer, make_worker) =
385                (&jobs, &work, &make_buffer, &make_worker);
386            scope.spawn(move || {
387                // First act of the thread, so every later line is covered.
388                let _life = WorkerLife { state, wake };
389                // `make_worker` is the caller's closure and may panic. Catching
390                // it here keeps the panic from propagating out of the scope —
391                // which would turn a per-worker fault into a process-level one
392                // — and lets the surviving workers claim the whole job list.
393                // If none survive, `_life` above is what stops the collector
394                // waiting for them.
395                let mut ctx = match std::panic::catch_unwind(
396                    std::panic::AssertUnwindSafe(&make_worker),
397                ) {
398                    Ok(c) => c,
399                    Err(_) => return,
400                };
401                loop {
402                    // Producer-side backpressure. Waiting *before* claiming a
403                    // job means a blocked worker holds nothing at all — no
404                    // buffer, no slot, no job — which is what makes the wait
405                    // deadlock-free. See the module docs.
406                    {
407                        let mut st = state.lock().unwrap();
408                        while !stop.load(Ordering::Acquire)
409                            && st.held + st.batch_bytes > spill_bytes
410                        {
411                            st = space.wait(st).unwrap();
412                        }
413                    }
414
415                    // Checked before claiming work, so a write failure or a
416                    // closed pipe stops the sweep at the next archive boundary
417                    // instead of at the end of the month.
418                    if stop.load(Ordering::Acquire) {
419                        return;
420                    }
421                    let p = next.fetch_add(1, Ordering::Relaxed);
422                    if p >= n {
423                        return;
424                    }
425
426                    let mut guard = Completion { state, wake, armed: true };
427
428                    let mut buf = {
429                        let mut st = state.lock().unwrap();
430                        st.pool.pop()
431                    }
432                    .unwrap_or_else(make_buffer);
433
434                    let outcome = run_job(&jobs[p], &mut ctx, &mut buf, work);
435                    let bytes = buf.as_slice().len();
436                    let done = Done { index: jobs[p].index, buffer: buf, outcome };
437
438                    {
439                        let mut st = state.lock().unwrap();
440                        st.slots[p] = Some(done);
441                        st.ready.push_back(p);
442                        st.held += bytes;
443                        st.finished += 1;
444                        guard.armed = false;
445                    }
446                    wake.notify_all();
447                }
448            });
449        }
450
451        // The collector. Runs on the calling thread so `emit` needs to be
452        // neither `Send` nor `Sync`, and so ordering decisions live in exactly
453        // one place. `head` walks `order`, not raw positions.
454
455        let mut head = 0usize;
456        let mut emitted = vec![false; n];
457        let mut emitted_count = 0usize;
458        let mut batch: Vec<Done> = Vec::new();
459
460        'collect: while emitted_count < n {
461            let mut spill_notices = 0usize;
462            {
463                let mut st = state.lock().unwrap();
464                loop {
465                    if sorted {
466                        // Drain the in-order prefix: everything from `head`
467                        // onwards, in index order, that has landed.
468                        while head < n {
469                            if emitted[head] {
470                                head += 1;
471                                continue;
472                            }
473                            match st.slots[order[head]].take() {
474                                Some(d) => {
475                                    let len = d.buffer.as_slice().len();
476                                    st.held -= len;
477                                    st.batch_bytes += len;
478                                    emitted[head] = true;
479                                    head += 1;
480                                    batch.push(d);
481                                }
482                                None => break,
483                            }
484                        }
485                        // Checked every pass, not only when blocked, so a fast
486                        // producer cannot outrun the cap while we are emitting.
487                        if st.held > spill_bytes {
488                            // Counted here, printed after the lock is dropped.
489                            // Writing to stderr under the state mutex stalls
490                            // every worker behind a slow terminal, and a failed
491                            // stderr write panics *inside* the lock: poisoned
492                            // under unwinding, an abort under `panic = "abort"`.
493                            // The lock is for moves in and out of `slots` and
494                            // `pool`, and nothing else.
495                            spill_notices += 1;
496                            flush_parked(&mut st, &order, &mut emitted, head, &mut batch);
497                        }
498                    } else {
499                        while let Some(p) = st.ready.pop_front() {
500                            if let Some(d) = st.slots[p].take() {
501                                let len = d.buffer.as_slice().len();
502                                st.held -= len;
503                                st.batch_bytes += len;
504                                // `order` is the identity here, so position and
505                                // order-index coincide. Marked in this mode too
506                                // so `emitted` means "taken" on both paths —
507                                // the stall sweep below needs to know which
508                                // positions are still outstanding.
509                                emitted[p] = true;
510                                batch.push(d);
511                            }
512                        }
513                    }
514
515                    if !batch.is_empty() {
516                        break;
517                    }
518                    // Not a spurious-wakeup hazard: `finished == n` implies
519                    // every unemitted slot is `Some`, so the drain above would
520                    // have filled `batch`. Reaching here means work is still
521                    // outstanding and some worker will notify — the `Completion`
522                    // guard makes that true even for a worker that unwinds.
523                    debug_assert!(st.finished < n, "collector stalled with all work finished");
524                    if st.finished >= n {
525                        break;
526                    }
527                    // Jobs outstanding and not one worker left to claim them.
528                    // Only reachable when a worker died on a path neither
529                    // `run_job`'s `catch_unwind` nor `Completion` covers — a
530                    // poisoned lock, say. Waiting here is the hang; instead,
531                    // take whatever landed and report the rest as unread, which
532                    // `main` turns into a 2. Never a silent short read.
533                    if st.live_workers == 0 {
534                        for (h, flag) in emitted.iter_mut().enumerate() {
535                            if *flag {
536                                continue;
537                            }
538                            *flag = true;
539                            let p = order[h];
540                            match st.slots[p].take() {
541                                Some(d) => {
542                                    let len = d.buffer.as_slice().len();
543                                    st.held -= len;
544                                    st.batch_bytes += len;
545                                    batch.push(d);
546                                }
547                                // `make_buffer` under the lock is safe *here*
548                                // and nowhere else: there are no workers left
549                                // to stall. The buffer stays empty, so it costs
550                                // `recycle` nothing.
551                                None => batch.push(Done {
552                                    index: jobs[p].index,
553                                    buffer: make_buffer(),
554                                    outcome: unclaimed(&jobs[p]),
555                                }),
556                            }
557                        }
558                        break;
559                    }
560                    st = wake.wait(st).unwrap();
561                }
562            }
563
564            for _ in 0..spill_notices {
565                eprintln!(
566                    "trg: held output exceeded {spill_bytes} bytes; flushing out of order"
567                );
568            }
569
570            // Unreachable per the invariant above; a `break` rather than a spin
571            // so a bug degrades into short output instead of a hung process.
572            if batch.is_empty() {
573                break;
574            }
575
576            // Emit outside the lock: stdout may block, workers must not.
577            for d in batch.drain(..) {
578                emitted_count += 1;
579                let r = emit(&d);
580                recycle(&state, &space, d.buffer, spill_bytes);
581                if let Err(e) = r {
582                    stop.store(true, Ordering::Release);
583                    write_err = Some(e);
584                    break 'collect;
585                }
586            }
587        }
588
589        // Anything still parked is dropped on the way out of the scope; only
590        // reachable on the early-stop path, where it is output nobody wants.
591        // Waking the blocked producers is `Teardown`'s job, on this path and on
592        // the unwinding one alike.
593    });
594
595    match write_err {
596        Some(e) => Err(e),
597        None => Ok(()),
598    }
599}
600
601/// The outcome for a job no worker ever claimed. An `errors` entry is the only
602/// partial-run signal `main` reads, so this is what makes the stalled run a 2
603/// rather than a confident "no matches, everything read".
604fn unclaimed(job: &Job) -> crate::archive::Outcome {
605    let mut o = crate::archive::Outcome::default();
606    o.errors.push(format!(
607        "{}: every worker exited before this archive was searched; not read",
608        job.path.display()
609    ));
610    o
611}
612
613/// Return a buffer to the pool so its allocation is reused, unless it grew past
614/// the spill cap — one pathological archive should not reserve its whole match
615/// volume for the rest of the run. `termcolor::Buffer` exposes no `capacity`,
616/// so the length it reached stands in for it.
617///
618/// This is the only place output bytes are actually freed, so it is also where
619/// backpressure is released. The pool itself needs no size cap: the number of
620/// buffers in existence is now bounded by the backpressure invariant, and
621/// capping it would only throw away the allocations the pool exists to keep.
622fn recycle(
623    state: &Mutex<Shared>,
624    space: &Condvar,
625    mut buffer: termcolor::Buffer,
626    spill_bytes: usize,
627) {
628    let len = buffer.as_slice().len();
629    let oversized = len > spill_bytes;
630    buffer.clear();
631    if let Ok(mut st) = state.lock() {
632        st.batch_bytes -= len;
633        if !oversized {
634            st.pool.push(buffer);
635        }
636    }
637    space.notify_all();
638}
639
640/// Emit everything that has landed but is out of turn, in index order. The
641/// escape valve behind both the byte cap and buffer starvation.
642fn flush_parked(
643    st: &mut Shared,
644    order: &[usize],
645    emitted: &mut [bool],
646    head: usize,
647    batch: &mut Vec<Done>,
648) {
649    for (h, flag) in emitted.iter_mut().enumerate().skip(head) {
650        if let Some(d) = st.slots[order[h]].take() {
651            // Note this does not relieve backpressure: the bytes move from
652            // `held` to `batch_bytes`, and only `recycle` frees them.
653            let len = d.buffer.as_slice().len();
654            st.held -= len;
655            st.batch_bytes += len;
656            *flag = true;
657            batch.push(d);
658        }
659    }
660}