use std::path::PathBuf;
use std::sync::{Arc, mpsc::Sender};
use polars::datatypes::AnyValue;
use polars::prelude::LazyFrame;
use crate::table::DataTableState;
use crate::{AppEvent, logging};
#[derive(Debug, Clone, Copy)]
pub(crate) struct InflightCollect {
pub(crate) began: std::time::Instant,
pub(crate) files: Option<usize>,
pub(crate) dataset: u64,
pub(crate) columns: u64,
pub(crate) start: usize,
pub(crate) end: usize,
}
impl InflightCollect {
pub(crate) fn columns_of(state: &DataTableState) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
state.get_column_order().hash(&mut hasher);
hasher.finish()
}
#[cfg(test)]
pub(crate) fn for_tests(start: usize, end: usize) -> Self {
Self {
began: std::time::Instant::now(),
files: None,
dataset: 0,
columns: 0,
start,
end,
}
}
pub(crate) fn covers(&self, state: &DataTableState) -> bool {
let bound = state.num_rows_if_valid().unwrap_or(usize::MAX);
let view_end = (state.start_row() + state.visible_rows).min(bound);
let (mut start, mut end) = (self.start, self.end);
let (held_start, held_end) = (state.buffered_start(), state.buffered_end());
if state.stitches_buffer() && (start == held_end || end == held_start) {
start = start.min(held_start);
end = end.max(held_end);
}
self.dataset == state.len_generation()
&& self.columns == Self::columns_of(state)
&& start <= state.start_row()
&& view_end <= end
}
}
pub(crate) struct LenCount {
pub(crate) len_generation: u64,
pub(crate) count_dir: Option<PathBuf>,
pub(crate) files: Option<crate::table::FileCounter>,
pub(crate) counter: Option<crate::formats::pushdown::Counter>,
pub(crate) lf: LazyFrame,
pub(crate) streaming: bool,
pub(crate) meter: Arc<crate::loading::measurements::Meter>,
pub(crate) progress: Arc<crate::formats::schema_union::FooterProgress>,
}
pub(crate) struct Counted {
pub(crate) rows: usize,
file_row_groups: Option<Vec<Vec<usize>>>,
}
impl From<usize> for Counted {
fn from(rows: usize) -> Self {
Counted {
rows,
file_row_groups: None,
}
}
}
impl LenCount {
pub(crate) fn for_state(state: &DataTableState) -> Self {
Self {
len_generation: state.len_generation(),
count_dir: state.parquet_count_dir(),
files: state.remote_files_counter(),
counter: state.source_counter(),
meter: state.measurements().clone(),
lf: state.lf_clone(),
streaming: state.polars_streaming_enabled(),
progress: Arc::new(if state.is_remote_source() {
crate::formats::schema_union::FooterProgress::counting()
} else {
crate::formats::schema_union::FooterProgress::default()
}),
}
}
pub(crate) fn reads_footers(&self) -> bool {
self.files.is_some() || self.count_dir.is_some()
}
pub(crate) fn run(&self) -> Result<Counted, ()> {
let kind = if self.reads_footers() {
"footers"
} else {
"scan"
};
let began = std::time::Instant::now();
log::debug!(target: "datui", "row count {} ({kind}): started", self.len_generation);
let counted = self.count();
match &counted {
Ok(counted) => log::debug!(
target: "datui",
"row count {} ({kind}): {} rows in {:.1?}",
self.len_generation,
counted.rows,
began.elapsed()
),
Err(()) => log::debug!(
target: "datui",
"row count {} ({kind}): failed after {:.1?}",
self.len_generation,
began.elapsed()
),
}
counted
}
fn count(&self) -> Result<Counted, ()> {
if let Some(counter) = &self.counter {
return counter()
.map(Counted::from)
.map_err(|e| log::warn!(target: "datui", "row count failed: {e}"));
}
if let Some(count) = &self.files {
match count(&self.progress) {
Ok(groups) => {
return Ok(Counted {
rows: groups.iter().flatten().sum(),
file_row_groups: Some(groups),
});
}
Err(_) if self.progress.is_cancelled() => return Err(()),
Err(_) => {}
}
}
match &self.count_dir {
Some(dir) => crate::formats::dataset_files::LocalFiles::new(dir)
.count_rows(&self.meter, &self.progress)
.map(Counted::from)
.map_err(|e| log::warn!(target: "datui", "row count failed: {e:#}")),
None => {
match crate::analysis::statistics::collect_lazy(
crate::table::row_count_lf(&self.lf),
self.streaming,
) {
Ok(df) => Ok(match df.get(0) {
Some(col) => match col.first() {
Some(AnyValue::UInt64(n)) => *n as usize,
_ => 0,
},
None => 0,
}
.into()),
Err(e) => {
log::warn!(target: "datui", "row count failed: {e}");
Err(())
}
}
}
}
}
pub(crate) fn after_collect(
&self,
start: usize,
returned: usize,
requested: usize,
) -> Result<Counted, ()> {
if returned < requested && (start == 0 || returned > 0) {
Ok((start + returned).into())
} else {
self.run()
}
}
fn send(&self, counted: Result<Counted, ()>, tx: &Sender<AppEvent>) {
let _ = tx.send(match counted {
Ok(counted) => AppEvent::BackgroundLenReady {
len_generation: self.len_generation,
num_rows: counted.rows,
file_row_groups: counted.file_row_groups,
},
Err(()) => AppEvent::BackgroundLenFailed {
len_generation: self.len_generation,
},
});
}
}
pub(crate) struct OwedCount {
job: Option<LenCount>,
tx: Sender<AppEvent>,
}
impl OwedCount {
pub(crate) fn new(job: LenCount, tx: Sender<AppEvent>) -> Self {
Self { job: Some(job), tx }
}
pub(crate) fn answer(mut self, count: impl FnOnce(&LenCount) -> Result<Counted, ()>) {
if let Some(job) = self.job.take() {
let counted = logging::catch_panic(|| count(&job)).unwrap_or(Err(()));
job.send(counted, &self.tx);
}
}
}
impl Drop for OwedCount {
fn drop(&mut self) {
if let Some(job) = self.job.take() {
job.send(Err(()), &self.tx);
}
}
}
pub(crate) struct OwedAnswer {
pub(crate) tx: Sender<AppEvent>,
pub(crate) instead: Option<AppEvent>,
#[cfg(test)]
pub(crate) dies: bool,
}
impl OwedAnswer {
pub(crate) fn run(mut self, work: impl FnOnce()) {
#[cfg(test)]
if self.dies {
panic!("worker died");
}
work();
self.instead = None;
}
}
impl Drop for OwedAnswer {
fn drop(&mut self) {
if let Some(instead) = self.instead.take() {
let _ = self.tx.send(instead);
}
}
}
pub(crate) fn release(held: impl Send + 'static) {
*RELEASING.0.lock().unwrap_or_else(|e| e.into_inner()) += 1;
let done = Released;
let _ = std::thread::Builder::new()
.name("datui-release".into())
.spawn(move || {
let _done = done;
drop(held);
trim_allocator();
});
}
static RELEASING: (std::sync::Mutex<usize>, std::sync::Condvar) =
(std::sync::Mutex::new(0), std::sync::Condvar::new());
struct Released;
impl Drop for Released {
fn drop(&mut self) {
let (count, ended) = &RELEASING;
*count.lock().unwrap_or_else(|e| e.into_inner()) -= 1;
ended.notify_all();
}
}
pub(crate) fn releases_settled(within: std::time::Duration) -> bool {
let (count, ended) = &RELEASING;
let count = count.lock().unwrap_or_else(|e| e.into_inner());
let (count, _) = ended
.wait_timeout_while(count, within, |n| *n > 0)
.unwrap_or_else(|e| e.into_inner());
*count == 0
}
fn trim_allocator() {
#[cfg(all(target_os = "linux", target_env = "gnu"))]
unsafe {
libc::malloc_trim(0);
}
}
#[derive(Clone, Default)]
pub(crate) struct CacheWrites(Arc<(std::sync::Mutex<usize>, std::sync::Condvar)>);
impl CacheWrites {
const SETTLE: std::time::Duration = std::time::Duration::from_secs(5);
pub(crate) fn spawn(&self, write: impl FnOnce() + Send + 'static) {
*self.0.0.lock().unwrap_or_else(|e| e.into_inner()) += 1;
let done = WriteDone(self.clone());
std::thread::spawn(move || {
let _done = done;
write();
});
}
pub(crate) fn settle(&self) {
let (count, ended) = &*self.0;
let count = count.lock().unwrap_or_else(|e| e.into_inner());
let _ = ended.wait_timeout_while(count, Self::SETTLE, |n| *n > 0);
}
}
impl std::fmt::Debug for CacheWrites {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("CacheWrites")
}
}
struct WriteDone(CacheWrites);
impl Drop for WriteDone {
fn drop(&mut self) {
let (count, ended) = &*(self.0).0;
*count.lock().unwrap_or_else(|e| e.into_inner()) -= 1;
ended.notify_all();
}
}
#[cfg(test)]
mod cache_writes_tests {
use super::CacheWrites;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, mpsc};
#[test]
fn settle_waits_for_a_write_in_flight() {
let writes = CacheWrites::default();
let (go, wait) = mpsc::channel::<()>();
let written = Arc::new(AtomicBool::new(false));
let flag = written.clone();
writes.spawn(move || {
let _ = wait.recv();
flag.store(true, Ordering::SeqCst);
});
let settled = {
let writes = writes.clone();
std::thread::spawn(move || writes.settle())
};
std::thread::sleep(std::time::Duration::from_millis(50));
assert!(!settled.is_finished(), "settled while the write was held");
go.send(()).unwrap();
settled.join().unwrap();
assert!(written.load(Ordering::SeqCst));
}
#[test]
fn a_write_that_panics_still_ends() {
let writes = CacheWrites::default();
writes.spawn(|| panic!("write died"));
let started = std::time::Instant::now();
writes.settle();
assert!(started.elapsed() < CacheWrites::SETTLE);
}
}
#[cfg(test)]
mod release_tests {
struct Held(std::sync::mpsc::Sender<()>);
impl Drop for Held {
fn drop(&mut self) {
let _ = self.0.send(());
}
}
#[test]
fn a_released_value_is_dropped() {
let (tx, rx) = std::sync::mpsc::channel();
super::release(Held(tx));
rx.recv_timeout(std::time::Duration::from_secs(60))
.expect("dropped");
}
}