use super::stream::{Event, Scanner, Source};
use super::{decode_block, Error, ErrorKind, Kernels, Scratch};
use std::collections::BTreeMap;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::sync::mpsc::{channel, sync_channel, Receiver, SyncSender};
use std::sync::{Arc, Condvar, Mutex};
use std::thread::JoinHandle;
pub(crate) const INPUT_QUEUE: usize = 2;
pub(crate) const COMPACT: usize = 4 << 20;
macro_rules! mark {
($kind:ident) => {
#[cfg(feature = "trace")]
crate::trace::mark(crate::trace::Kind::$kind, crate::trace::NONE);
};
($kind:ident, $place:expr) => {
#[cfg(feature = "trace")]
crate::trace::mark(crate::trace::Kind::$kind, $place);
};
($kind:ident, $place:expr, $len:expr, $cap:expr) => {
#[cfg(feature = "trace")]
crate::trace::mark_sized(crate::trace::Kind::$kind, $place, $len as u64, $cap as u64);
};
}
type Job = (u64, u64, super::block::Symbols);
struct State {
ready: BTreeMap<u64, Result<Vec<u8>, Error>>,
issued: u64,
taken: u64,
end: Option<u64>,
cancelled: bool,
free_out: Vec<Vec<u8>>,
free_syms: Vec<Vec<u16>>,
}
struct Shared {
state: Mutex<State>,
cv: Condvar,
slots: u64,
}
impl Shared {
fn lock(&self) -> std::sync::MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(|e| e.into_inner())
}
}
#[derive(Debug)]
pub struct Stopped;
pub struct StreamDecoder {
input: Mutex<Option<SyncSender<Vec<u8>>>>,
shared: Arc<Shared>,
handles: Mutex<Vec<JoinHandle<()>>>,
reader: Mutex<Reader>,
threads: usize,
}
#[derive(Default)]
struct Reader {
cur: Vec<u8>,
at: usize,
failed: Option<Error>,
done: bool,
}
impl StreamDecoder {
pub fn new(threads: usize, slots: usize, k: Kernels) -> StreamDecoder {
let threads = threads.max(1);
let plan = Plan::for_threads(threads);
Self::planned(plan, window(plan, threads, slots), k)
}
pub fn planned(plan: Plan, slots: usize, k: Kernels) -> StreamDecoder {
use super::bwt::{Cooperative, Segmented};
let mut dec = match plan {
Plan::Workers(n) => Self::with_inverse::<Segmented>(n, slots, k),
Plan::Teams { teams, k: 2 } => Self::with_inverse::<Cooperative<2>>(teams, slots, k),
Plan::Teams { teams, k: 3 } => Self::with_inverse::<Cooperative<3>>(teams, slots, k),
Plan::Teams { teams, k: 4 } => Self::with_inverse::<Cooperative<4>>(teams, slots, k),
Plan::Teams { teams, k: t } => {
Self::with_inverse::<Segmented>(teams.max(1) * t.max(1), slots, k)
}
};
dec.threads = plan.threads();
dec
}
pub fn with_inverse<I: super::bwt::InverseBwt + 'static>(
threads: usize,
slots: usize,
k: Kernels,
) -> StreamDecoder {
let threads = threads.max(1);
let slots = slots.max(threads) as u64;
let shared = Arc::new(Shared {
state: Mutex::new(State {
ready: BTreeMap::new(),
issued: 0,
taken: 0,
end: None,
cancelled: false,
free_out: Vec::new(),
free_syms: Vec::new(),
}),
cv: Condvar::new(),
slots,
});
let (in_tx, in_rx) = sync_channel::<Vec<u8>>(INPUT_QUEUE);
let (job_tx, job_rx) = channel::<Job>();
let job_rx = Arc::new(Mutex::new(job_rx));
let mut handles = Vec::with_capacity(threads + 1);
for i in 0..threads {
let (shared, job_rx) = (shared.clone(), job_rx.clone());
handles.push(
std::thread::Builder::new()
.name(format!("mibz2-{i}"))
.spawn(move || worker::<I>(&shared, &job_rx, k))
.expect("spawn a bz2 worker"),
);
}
let sh = shared.clone();
handles.push(
std::thread::Builder::new()
.name("mibz2-scan".into())
.spawn(move || scanner(&sh, in_rx, job_tx))
.expect("spawn the bz2 scanner"),
);
StreamDecoder {
input: Mutex::new(Some(in_tx)),
shared,
handles: Mutex::new(handles),
reader: Mutex::default(),
threads,
}
}
pub fn threads(&self) -> usize {
self.threads
}
pub fn write(&self, data: Vec<u8>) -> Result<(), Stopped> {
let tx = self
.input
.lock()
.unwrap_or_else(|e| e.into_inner())
.clone()
.ok_or(Stopped)?;
if data.is_empty() {
return Ok(());
}
tx.send(data).map_err(|_| Stopped)
}
pub fn close(&self) {
self.input.lock().unwrap_or_else(|e| e.into_inner()).take();
}
pub fn cancel(&self) {
self.close();
let mut st = self.shared.lock();
st.cancelled = true;
st.ready.clear();
drop(st);
self.shared.cv.notify_all();
}
pub fn read(&self, buf: &mut [u8]) -> Result<usize, Error> {
let mut r = self.reader.lock().unwrap_or_else(|e| e.into_inner());
if !r.done && r.failed.is_none() && self.shared.lock().cancelled {
r.failed = Some(Error::new(ErrorKind::Cancelled));
}
loop {
if let Some(e) = &r.failed {
return Err(e.clone());
}
if r.at < r.cur.len() {
let n = buf.len().min(r.cur.len() - r.at);
buf[..n].copy_from_slice(&r.cur[r.at..r.at + n]);
r.at += n;
return Ok(n);
}
if r.done || buf.is_empty() {
return Ok(0);
}
let mut st = self.shared.lock();
let spent = std::mem::take(&mut r.cur);
if spent.capacity() != 0 && st.free_out.len() < self.shared.slots as usize {
st.free_out.push(spent);
}
r.at = 0;
mark!(ReadWait);
loop {
if st.cancelled {
r.failed = Some(Error::new(ErrorKind::Cancelled));
break;
}
let taken = st.taken;
if let Some(res) = st.ready.remove(&taken) {
st.taken += 1;
self.shared.cv.notify_all();
mark!(Taken, taken);
match res {
Ok(v) => r.cur = v,
Err(e) => r.failed = Some(e),
}
break;
}
if st.end == Some(st.taken) {
r.done = true;
break;
}
st = self.shared.cv.wait(st).unwrap_or_else(|e| e.into_inner());
}
}
}
pub fn join(&self) {
self.cancel();
let handles = std::mem::take(&mut *self.handles.lock().unwrap_or_else(|e| e.into_inner()));
for h in handles {
let _ = h.join();
}
}
}
impl Drop for StreamDecoder {
fn drop(&mut self) {
self.join();
}
}
struct ChannelSource<'a> {
rx: Receiver<Vec<u8>>,
buf: Vec<u8>,
base: u64,
released: u64,
eof: bool,
shared: &'a Shared,
}
impl Source for ChannelSource<'_> {
fn view(&mut self, from: u64, want: usize) -> Result<(&[u8], bool), ErrorKind> {
while !self.eof && self.base + (self.buf.len() as u64) < from + want as u64 {
mark!(InputWait);
match self.rx.recv() {
Ok(chunk) => {
mark!(InputGot);
let drop_n = (self.released - self.base) as usize;
if drop_n >= COMPACT {
self.buf.drain(..drop_n);
self.base = self.released;
}
self.buf.extend_from_slice(&chunk);
mark!(
Window,
crate::trace::NONE,
self.base + self.buf.len() as u64 - self.released,
self.buf.capacity()
);
}
Err(_) => {
mark!(InputEnd);
self.eof = true;
}
}
if self.shared.lock().cancelled {
return Err(ErrorKind::Cancelled);
}
}
let i = (from - self.base) as usize;
Ok((self.buf.get(i..).unwrap_or(&[]), self.eof))
}
fn release(&mut self, upto: u64) {
self.released = self.released.max(upto);
}
}
fn scanner(shared: &Shared, rx: Receiver<Vec<u8>>, jobs: std::sync::mpsc::Sender<Job>) {
let src = ChannelSource {
rx,
buf: Vec::new(),
base: 0,
released: 0,
eof: false,
shared,
};
let mut sc = Scanner::new(src);
let finish = |res: Option<Error>| {
let mut st = shared.lock();
let at = st.issued;
match res {
Some(e) if e.kind != ErrorKind::Cancelled => {
st.ready.insert(at, Err(e));
st.issued += 1;
st.end = Some(at + 1);
}
_ => st.end = Some(at),
}
drop(st);
shared.cv.notify_all();
};
loop {
mark!(SlotWait);
let syms = {
let mut st = shared.lock();
while !st.cancelled && st.issued - st.taken >= shared.slots {
st = shared.cv.wait(st).unwrap_or_else(|e| e.into_inner());
}
if st.cancelled {
drop(st);
return finish(None);
}
st.free_syms.pop().unwrap_or_default()
};
mark!(SlotGot);
let next = catch_unwind(AssertUnwindSafe(|| sc.next(syms)))
.unwrap_or_else(|p| Err(Error::new(ErrorKind::Internal(panic_message(p)))));
match next {
Ok(Event::Block {
index,
bit,
symbols,
..
}) => {
let mut st = shared.lock();
let place = st.issued;
st.issued += 1;
drop(st);
mark!(
Issued,
place,
std::mem::size_of_val(&symbols.syms[..]),
symbols.syms.capacity() * std::mem::size_of::<u16>()
);
debug_assert_eq!(place, index);
if jobs.send((place, bit, symbols)).is_err() {
return finish(None);
}
mark!(Enqueued, place);
}
Ok(Event::Done) => return finish(None),
Err(e) => return finish(Some(e)),
}
}
}
fn worker<I: super::bwt::InverseBwt>(shared: &Shared, jobs: &Mutex<Receiver<Job>>, k: Kernels) {
let mut scratch = Scratch::<I>::default();
loop {
mark!(JobLock);
let job = {
let rx = jobs.lock().unwrap_or_else(|e| e.into_inner());
mark!(JobRecv);
rx.recv()
};
let Ok((place, bit, symbols)) = job else {
#[cfg(feature = "trace")]
if crate::trace::level() > 0 {
crate::trace::buffers(&scratch.buffers());
}
return;
};
mark!(JobGot, place);
let mut out = {
let mut st = shared.lock();
if st.cancelled {
continue; }
st.free_out.pop().unwrap_or_default()
};
out.clear();
mark!(DecodeStart, place);
let res = catch_unwind(AssertUnwindSafe(|| {
decode_block(&symbols, k, &mut scratch, &mut out)
}));
mark!(DecodeEnd, place, out.len(), out.capacity());
let res = match res {
Ok(Ok(_)) => Ok(out),
Ok(Err(kind)) => Err(kind),
Err(p) => {
scratch = Scratch::default();
Err(ErrorKind::Internal(panic_message(p)))
}
}
.map_err(|kind| Error {
kind,
block: Some(place),
bit: Some(bit),
});
let mut st = shared.lock();
if !st.cancelled {
st.ready.insert(place, res);
}
if st.free_syms.len() < shared.slots as usize {
st.free_syms.push(symbols.syms);
}
drop(st);
mark!(Ready, place);
shared.cv.notify_all();
}
}
fn panic_message(p: Box<dyn std::any::Any + Send>) -> String {
p.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| p.downcast_ref::<String>().cloned())
.unwrap_or_else(|| "a panic".into())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Plan {
Workers(usize),
Teams { teams: usize, k: usize },
}
pub const COOP_MIN_CORES: usize = 6;
const FULL_TABLE: f64 = 3.6e6;
impl Plan {
pub fn for_threads(threads: usize) -> Plan {
plan_with(threads.max(1), physical_cores(), l3_bytes())
}
pub fn workers(&self) -> usize {
match *self {
Plan::Workers(n) => n.max(1),
Plan::Teams { teams, .. } => teams.max(1),
}
}
pub fn threads(&self) -> usize {
match *self {
Plan::Workers(n) => n.max(1),
Plan::Teams { teams, k } => teams.max(1) * k.max(1),
}
}
}
fn window(plan: Plan, threads: usize, slots: usize) -> usize {
match plan {
Plan::Workers(_) => slots,
Plan::Teams { teams, .. } => (slots.saturating_mul(teams) / threads.max(1)).max(teams),
}
}
pub(crate) fn plan_with(threads: usize, phys: Option<usize>, l3: Option<u64>) -> Plan {
let Some(phys) = phys else {
return Plan::Workers(threads);
};
let cores = threads.min(phys);
if cores < COOP_MIN_CORES {
return Plan::Workers(threads);
}
let cap = l3.map_or(4, |b| {
((0.7 * b as f64) / FULL_TABLE).round().max(1.0) as usize
});
Plan::Teams {
teams: (cores / 2).min(cap).max(1),
k: 2,
}
}
fn physical_cores() -> Option<usize> {
#[cfg(target_os = "linux")]
{
let status = std::fs::read_to_string("/proc/self/status").ok()?;
let allowed = status
.lines()
.find_map(|l| l.strip_prefix("Cpus_allowed_list:"))?
.trim()
.to_string();
let mut cores = std::collections::BTreeSet::new();
for part in allowed.split(',') {
let (a, b) = part.split_once('-').unwrap_or((part, part));
let (a, b): (usize, usize) = (a.parse().ok()?, b.parse().ok()?);
for c in a..=b {
let t = format!("/sys/devices/system/cpu/cpu{c}/topology");
let Ok(core) = std::fs::read_to_string(format!("{t}/core_id")) else {
continue; };
let pkg = std::fs::read_to_string(format!("{t}/physical_package_id")).ok()?;
cores.insert((pkg.trim().to_string(), core.trim().to_string()));
}
}
(!cores.is_empty()).then_some(cores.len())
}
#[cfg(not(target_os = "linux"))]
None
}
fn l3_bytes() -> Option<u64> {
#[cfg(target_os = "linux")]
{
let s = std::fs::read_to_string("/sys/devices/system/cpu/cpu0/cache/index3/size").ok()?;
let s = s.trim();
let (num, mult) = match s.as_bytes().last()? {
b'K' => (&s[..s.len() - 1], 1u64 << 10),
b'M' => (&s[..s.len() - 1], 1 << 20),
_ => (s, 1),
};
num.parse::<u64>().ok().map(|n| n * mult)
}
#[cfg(not(target_os = "linux"))]
None
}
#[cfg(test)]
mod plan_tests {
use super::*;
#[test]
fn the_policy() {
let i9 = (Some(10), Some(20 << 20));
assert_eq!(plan_with(4, i9.0, i9.1), Plan::Workers(4));
assert_eq!(plan_with(5, i9.0, i9.1), Plan::Workers(5));
assert_eq!(plan_with(8, None, i9.1), Plan::Workers(8));
assert_eq!(plan_with(20, Some(4), i9.1), Plan::Workers(20));
assert_eq!(plan_with(6, i9.0, i9.1), Plan::Teams { teams: 3, k: 2 });
assert_eq!(plan_with(8, i9.0, i9.1), Plan::Teams { teams: 4, k: 2 });
assert_eq!(plan_with(10, i9.0, i9.1), Plan::Teams { teams: 4, k: 2 });
assert_eq!(plan_with(20, i9.0, i9.1), Plan::Teams { teams: 4, k: 2 });
assert_eq!(
plan_with(16, Some(16), Some(32 << 20)),
Plan::Teams { teams: 7, k: 2 }
);
assert_eq!(
plan_with(12, Some(12), None),
Plan::Teams { teams: 4, k: 2 }
);
let p = Plan::Teams { teams: 4, k: 2 };
assert_eq!((p.workers(), p.threads()), (4, 8));
}
#[test]
fn the_window() {
let t4 = Plan::Teams { teams: 4, k: 2 };
assert_eq!(window(Plan::Workers(4), 4, 8), 8);
assert_eq!(window(t4, 20, 40), 8);
assert_eq!(window(t4, 8, 16), 8);
assert_eq!(window(t4, 20, 1), 4);
assert_eq!(window(t4, 20, usize::MAX), usize::MAX / 20);
}
#[test]
fn the_default_is_the_plan() {
for t in [1, 4, 8] {
let dec = StreamDecoder::new(t, 2 * t, Kernels::detect());
assert_eq!(dec.threads(), Plan::for_threads(t).threads());
dec.close();
let e = dec.read(&mut [0; 16]).unwrap_err();
assert_eq!(e.kind, ErrorKind::Empty);
}
}
#[test]
fn the_policy_here() {
if cfg!(target_os = "linux") {
let p = physical_cores().expect("Linux's topology");
let logical = std::thread::available_parallelism().unwrap().get();
assert!(p >= 1 && p <= logical, "{p} cores of {logical} CPUs");
}
for t in [1, 2, 4, 6, 8, 64] {
let p = Plan::for_threads(t);
assert!(p.threads() <= t.max(1), "{t}: {p:?}");
}
}
}