Skip to main content

lzma_rust2/enc/
lzma2_writer_mt.rs

1use std::{
2    io::{self, Write},
3    sync::{
4        Arc, Mutex,
5        atomic::{AtomicBool, AtomicU32, Ordering},
6        mpsc::SyncSender,
7    },
8};
9
10use super::Lzma2Writer;
11use crate::{
12    AutoFinish, AutoFinisher, ByteWriter, Lzma2Options, error_invalid_input, set_error,
13    work_pool::{WorkPool, WorkPoolConfig},
14    work_queue::WorkerHandle,
15};
16
17/// A work unit for a worker thread.
18#[derive(Debug, Clone)]
19struct WorkUnit {
20    data: Vec<u8>,
21    options: Lzma2Options,
22}
23
24/// A multi-threaded LZMA2 compressor.
25pub struct Lzma2WriterMt<W: Write> {
26    inner: W,
27    options: Lzma2Options,
28    chunk_size: usize,
29    current_work_unit: Vec<u8>,
30    work_pool: WorkPool<WorkUnit, Vec<u8>>,
31}
32
33impl<W: Write> Lzma2WriterMt<W> {
34    /// Creates a new multi-threaded LZMA2 writer.
35    ///
36    /// - `inner`: The writer to write compressed data to.
37    /// - `options`: The LZMA2 options used for compressing. Chunk size must be set when using the
38    ///   multi-threaded encoder. If you need just one chunk, then use the single-threaded encoder.
39    /// - `num_workers`: The maximum number of worker threads for compression.
40    ///   Currently capped at 256 Threads.
41    pub fn new(inner: W, options: Lzma2Options, num_workers: u32) -> crate::Result<Self> {
42        let chunk_size = match options.chunk_size {
43            None => return Err(error_invalid_input("chunk size must be set")),
44            Some(chunk_size) => chunk_size.get().max(options.lzma_options.dict_size as u64),
45        };
46
47        let chunk_size = usize::try_from(chunk_size)
48            .map_err(|_| error_invalid_input("chunk size bigger than usize"))?;
49
50        // We don't know how many work units we'll have ahead of time.
51        let num_work = u64::MAX;
52
53        Ok(Self {
54            inner,
55            options,
56            chunk_size,
57            current_work_unit: Vec::with_capacity(chunk_size),
58            work_pool: WorkPool::new(
59                WorkPoolConfig::new(num_workers, num_work),
60                worker_thread_logic,
61            ),
62        })
63    }
64
65    /// Sends the current work unit to the workers.
66    fn send_work_unit(&mut self) -> io::Result<()> {
67        if self.current_work_unit.is_empty() {
68            return Ok(());
69        }
70
71        self.drain_available_results()?;
72
73        let work_data = core::mem::take(&mut self.current_work_unit);
74        let mut single_chunk_options = self.options.clone();
75        single_chunk_options.chunk_size = None;
76        single_chunk_options.lzma_options.preset_dict = None;
77
78        let mut work_data_opt = Some(work_data);
79
80        self.work_pool.dispatch_next_work(&mut |_seq| {
81            let data = work_data_opt.take().ok_or_else(|| {
82                io::Error::new(io::ErrorKind::InvalidInput, "work already provided")
83            })?;
84            Ok(WorkUnit {
85                data,
86                options: single_chunk_options.clone(),
87            })
88        })?;
89
90        self.drain_available_results()?;
91
92        Ok(())
93    }
94
95    /// Drains all currently available results from the work pool and writes them.
96    fn drain_available_results(&mut self) -> io::Result<()> {
97        while let Some(compressed_data) = self.work_pool.try_get_result()? {
98            self.inner.write_all(&compressed_data)?;
99        }
100        Ok(())
101    }
102
103    /// Returns a wrapper around `self` that will finish the stream on drop.
104    pub fn auto_finish(self) -> AutoFinisher<Self> {
105        AutoFinisher(Some(self))
106    }
107
108    /// Consume the Lzma2WriterMt and return the inner writer.
109    pub fn into_inner(self) -> W {
110        self.inner
111    }
112
113    /// Finishes the compression and returns the underlying writer.
114    pub fn finish(mut self) -> io::Result<W> {
115        if !self.current_work_unit.is_empty() {
116            self.send_work_unit()?;
117        }
118
119        // If no data was provided to compress, write an empty LZMA2 stream.
120        if self.work_pool.next_index_to_dispatch() == 0 {
121            self.inner.write_u8(0x00)?;
122            self.inner.flush()?;
123
124            return Ok(self.inner);
125        }
126
127        // Mark the WorkPool as finished so it knows no more work is coming.
128        self.work_pool.finish();
129
130        // Wait for all remaining work to complete.
131        while let Some(compressed_data) = self.work_pool.get_result(|_| {
132            Err(io::Error::new(
133                io::ErrorKind::InvalidInput,
134                "no more work to dispatch",
135            ))
136        })? {
137            self.inner.write_all(&compressed_data)?;
138        }
139
140        self.inner.write_u8(0x00)?;
141        self.inner.flush()?;
142
143        Ok(self.inner)
144    }
145}
146
147/// The logic for a single worker thread.
148fn worker_thread_logic(
149    worker_handle: WorkerHandle<(u64, WorkUnit)>,
150    result_tx: SyncSender<(u64, Vec<u8>)>,
151    shutdown_flag: Arc<AtomicBool>,
152    error_store: Arc<Mutex<Option<io::Error>>>,
153    active_workers: Arc<AtomicU32>,
154) {
155    while !shutdown_flag.load(Ordering::Acquire) {
156        let (index, work_unit) = match worker_handle.steal() {
157            Some(work) => {
158                active_workers.fetch_add(1, Ordering::Release);
159                work
160            }
161            None => {
162                // No more work available and queue is closed.
163                break;
164            }
165        };
166
167        let mut compressed_buffer = Vec::new();
168
169        let mut writer = Lzma2Writer::new(&mut compressed_buffer, work_unit.options);
170
171        let result = match writer.write_all(&work_unit.data) {
172            Ok(_) => match writer.flush() {
173                Ok(_) => compressed_buffer,
174                Err(error) => {
175                    active_workers.fetch_sub(1, Ordering::Release);
176                    set_error(error, &error_store, &shutdown_flag);
177                    return;
178                }
179            },
180            Err(error) => {
181                active_workers.fetch_sub(1, Ordering::Release);
182                set_error(error, &error_store, &shutdown_flag);
183                return;
184            }
185        };
186
187        if result_tx.send((index, result)).is_err() {
188            active_workers.fetch_sub(1, Ordering::Release);
189            return;
190        }
191
192        active_workers.fetch_sub(1, Ordering::Release);
193    }
194}
195
196impl<W: Write> Write for Lzma2WriterMt<W> {
197    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
198        if buf.is_empty() {
199            return Ok(0);
200        }
201
202        let mut total_written = 0;
203        let mut remaining_buf = buf;
204
205        while !remaining_buf.is_empty() {
206            let chunk_remaining = self.chunk_size.saturating_sub(self.current_work_unit.len());
207            let to_write = remaining_buf.len().min(chunk_remaining);
208
209            if to_write > 0 {
210                self.current_work_unit
211                    .extend_from_slice(&remaining_buf[..to_write]);
212                total_written += to_write;
213                remaining_buf = &remaining_buf[to_write..];
214            }
215
216            if self.current_work_unit.len() >= self.chunk_size {
217                self.send_work_unit()?;
218            }
219
220            self.drain_available_results()?;
221        }
222
223        Ok(total_written)
224    }
225
226    fn flush(&mut self) -> io::Result<()> {
227        if !self.current_work_unit.is_empty() {
228            self.send_work_unit()?;
229        }
230
231        // Wait for all pending work to complete and write the results.
232        while let Some(compressed_data) = self.work_pool.try_get_result()? {
233            self.inner.write_all(&compressed_data)?;
234        }
235
236        self.inner.flush()
237    }
238}
239
240impl<W: Write> AutoFinish for Lzma2WriterMt<W> {
241    fn finish_ignore_error(self) {
242        let _ = self.finish();
243    }
244}