use std::io::Write;
use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use cbvault_format::cbh::{Annotations, Batch, Entities, Headers, Wide};
use cbvault_format::error::Result;
use cbvault_format::file::DbFile;
use cbvault_format::game::RecordKind;
use rayon::prelude::*;
use super::PgnWriter;
pub const DEFAULT_BATCH: u32 = 8192;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ExportStats {
pub records: u64,
pub games: u64,
pub texts: u64,
pub bytes: u64,
pub peak_writer: u64,
pub failures: u64,
}
pub fn export_parallel(
base: &Path,
out: &mut impl Write,
threads: usize,
batch_size: u32,
failure_limit: usize,
) -> Result<ExportStats> {
export_range(base, out, threads, batch_size, failure_limit, 0)
}
pub fn export_range(
base: &Path,
out: &mut impl Write,
threads: usize,
batch_size: u32,
failure_limit: usize,
last: u32,
) -> Result<ExportStats> {
export_span(base, out, threads, batch_size, failure_limit, 1, last)
}
pub fn export_range_from(
base: &Path,
out: &mut impl Write,
threads: usize,
batch_size: u32,
failure_limit: usize,
first: u32,
last: u32,
) -> Result<ExportStats> {
export_span(base, out, threads, batch_size, failure_limit, first, last)
}
pub fn export_span(
base: &Path,
out: &mut impl Write,
threads: usize,
batch_size: u32,
failure_limit: usize,
first: u32,
last: u32,
) -> Result<ExportStats> {
let headers = Headers::open(base)?;
let entities = Entities::open(base)?;
let annotations = Annotations::open(base)?;
let wide = Wide::open(base).ok();
let cbg_path = base.with_extension("cbg");
let cbg = DbFile::open(cbg_path)?;
let total = headers.records();
let batch = if batch_size == 0 { DEFAULT_BATCH } else { batch_size };
let first = first.max(1);
let last = if last == 0 { total } else { last.min(total) };
let mut chunks: Vec<(u32, u32)> = Vec::new();
let mut id = first;
while id <= last {
let end = id.saturating_add(batch - 1).min(last);
chunks.push((id, end));
id = end + 1;
}
let failures = Mutex::new(Vec::new());
let mut stats = ExportStats::default();
let sink = &mut *out;
let buffers: Mutex<Vec<Vec<u8>>> = Mutex::new(Vec::new());
let high_water = AtomicUsize::new(0);
let render = |wave: &[(u32, u32)]| -> Vec<(u32, Vec<u8>, ExportStats)> {
wave.par_iter()
.map(|&(first, last)| {
let mut bytes = buffers.lock().unwrap_or_else(|e| e.into_inner()).pop().unwrap_or_default();
bytes.clear();
let hint = high_water.load(Ordering::Relaxed);
if bytes.capacity() < hint {
bytes.reserve(hint - bytes.len());
}
let s = render_chunk(
&headers,
&entities,
&annotations,
wide.as_ref(),
&cbg,
first,
last,
&mut bytes,
&failures,
failure_limit,
);
(first, bytes, s)
})
.collect()
};
let mut write = |rendered: Vec<(u32, Vec<u8>, ExportStats)>, stats: &mut ExportStats| {
for (_, bytes, s) in rendered {
let _ = (*sink).write_all(&bytes);
stats.records += s.records;
stats.games += s.games;
stats.texts += s.texts;
stats.bytes += s.bytes;
stats.failures += s.failures;
high_water.fetch_max(bytes.len(), Ordering::Relaxed);
if bytes.capacity() > 0 {
buffers.lock().unwrap_or_else(|e| e.into_inner()).push(bytes);
}
}
};
if threads <= 1 {
for wave in chunks.chunks(1) {
write(render(wave), &mut stats);
}
} else {
let pool = rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.build()
.map_err(|e| cbvault_format::error::Error::corrupt(base, 0, format!("thread pool creation failed: {e}")))?;
for wave in chunks.chunks(threads) {
let rendered = pool.install(|| render(wave));
write(rendered, &mut stats);
}
}
let _ = out.flush();
Ok(stats)
}
#[allow(clippy::too_many_arguments)]
fn render_chunk(
headers: &Headers,
entities: &Entities,
annotations: &Annotations,
wide: Option<&Wide>,
cbg: &DbFile,
first: u32,
last: u32,
bytes: &mut Vec<u8>,
failures: &Mutex<Vec<String>>,
failure_limit: usize,
) -> ExportStats {
let mut stats = ExportStats::default();
let mut writer = PgnWriter::new();
let mut peak = 0usize;
let (mut move_scratch, mut ann_scratch) = (Vec::new(), Vec::new());
let batch = match Batch::open(headers, cbg, wide, first, last) {
Ok(b) => b,
Err(e) => {
record(failures, failure_limit, format!("batch open {first}..={last}: {e}"));
stats.failures += 1;
return stats;
}
};
for header in batch.iter_records() {
stats.records += 1;
if header.is_deleted() {
continue;
}
match header.kind() {
RecordKind::Game => {}
RecordKind::Text => {
stats.texts += 1;
continue;
}
_ => continue,
}
let id = header.id();
let game = match batch.moves_of_ref(&header, &mut move_scratch) {
Ok(g) => g,
Err(e) => {
record(failures, failure_limit, format!("game {id}: {e}"));
stats.failures += 1;
continue;
}
};
let anns = match annotations.of_ref(&header, wide, &mut ann_scratch) {
Ok(a) => a,
Err(e) => {
record(failures, failure_limit, format!("game {id} annotations: {e}"));
stats.failures += 1;
let _ = writer.write_game(bytes, &header, entities, &game, None);
stats.games += 1;
continue;
}
};
let _ = writer.write_game(bytes, &header, entities, &game, Some(&anns));
stats.games += 1;
peak = peak.max(writer.capacity());
}
stats.bytes = bytes.len() as u64;
stats.peak_writer = peak as u64;
stats
}
fn record(failures: &Mutex<Vec<String>>, limit: usize, message: String) {
let mut list = failures.lock().unwrap_or_else(|e| e.into_inner());
if list.len() < limit {
list.push(message);
}
}