use std::borrow::Cow;
use std::cell::OnceCell;
use std::io;
use std::sync::Arc;
use std::time::Duration;
#[cfg(not(target_arch = "wasm32"))]
use std::time::Instant;
use portable_atomic::{AtomicU64, AtomicU8, Ordering};
#[cfg(target_arch = "wasm32")]
use web_time::Instant;
use crate::draw_target::{LineType, ProgressDrawTarget};
use crate::style::ProgressStyle;
pub(crate) struct BarState {
pub(crate) draw_target: ProgressDrawTarget,
pub(crate) on_finish: ProgressFinish,
pub(crate) style: ProgressStyle,
pub(crate) state: ProgressState,
pub(crate) tab_width: usize,
}
impl BarState {
pub(crate) fn new(
len: Option<u64>,
draw_target: ProgressDrawTarget,
pos: Arc<AtomicPosition>,
) -> Self {
Self {
draw_target,
on_finish: ProgressFinish::default(),
style: ProgressStyle::default_bar(),
state: ProgressState::new(len, pos),
tab_width: DEFAULT_TAB_WIDTH,
}
}
pub(crate) fn finish_using_style(&mut self, now: Instant, finish: ProgressFinish) {
self.state.status = Status::DoneVisible;
match finish {
ProgressFinish::AndLeave => {
if let Some(len) = self.state.len {
self.state.pos.set(len);
}
}
ProgressFinish::WithMessage(msg) => {
if let Some(len) = self.state.len {
self.state.pos.set(len);
}
self.state.message = TabExpandedString::new(msg, self.tab_width);
}
ProgressFinish::AndClear => {
if let Some(len) = self.state.len {
self.state.pos.set(len);
}
self.state.status = Status::DoneHidden;
}
ProgressFinish::Abandon => {}
ProgressFinish::AbandonWithMessage(msg) => {
self.state.message = TabExpandedString::new(msg, self.tab_width);
}
}
let _ = self.draw(true, now);
}
pub(crate) fn reset(&mut self, now: Instant, mode: Reset) {
self.state.est.reset(now);
if let Reset::Elapsed | Reset::All = mode {
self.state.started = now;
}
if let Reset::All = mode {
self.state.pos.reset(now);
self.state.status = Status::InProgress;
for tracker in self.style.format_map.values_mut() {
tracker.reset(&self.state, now);
}
let _ = self.draw(false, now);
}
}
pub(crate) fn update(&mut self, now: Instant, f: impl FnOnce(&mut ProgressState), tick: bool) {
f(&mut self.state);
if tick {
self.tick(now);
}
}
pub(crate) fn unset_length(&mut self, now: Instant) {
self.state.len = None;
self.update_estimate_and_draw(now);
}
pub(crate) fn set_length(&mut self, now: Instant, len: u64) {
self.state.len = Some(len);
self.update_estimate_and_draw(now);
}
pub(crate) fn inc_length(&mut self, now: Instant, delta: u64) {
if let Some(len) = self.state.len {
self.state.len = Some(len.saturating_add(delta));
}
self.update_estimate_and_draw(now);
}
pub(crate) fn dec_length(&mut self, now: Instant, delta: u64) {
if let Some(len) = self.state.len {
self.state.len = Some(len.saturating_sub(delta));
}
self.update_estimate_and_draw(now);
}
pub(crate) fn set_tab_width(&mut self, tab_width: usize) {
self.tab_width = tab_width;
self.state.message.set_tab_width(tab_width);
self.state.prefix.set_tab_width(tab_width);
self.style.set_tab_width(tab_width);
}
pub(crate) fn set_style(&mut self, style: ProgressStyle) {
self.style = style;
self.style.set_tab_width(self.tab_width);
}
pub(crate) fn tick(&mut self, now: Instant) {
self.state.tick = self.state.tick.saturating_add(1);
self.update_estimate_and_draw(now);
}
pub(crate) fn update_estimate_and_draw(&mut self, now: Instant) {
let pos = self.state.pos.pos.load(Ordering::Relaxed);
self.state.est.record(pos, now);
for tracker in self.style.format_map.values_mut() {
tracker.tick(&self.state, now);
}
let _ = self.draw(false, now);
}
pub(crate) fn println(&mut self, now: Instant, msg: &str) {
let width = self.draw_target.width();
let mut drawable = match self.draw_target.drawable(true, now) {
Some(drawable) => drawable,
None => return,
};
let mut draw_state = drawable.state();
let lines: Vec<LineType> = msg.lines().map(|l| LineType::Text(Into::into(l))).collect();
if lines.is_empty() {
draw_state.lines.push(LineType::Empty);
} else {
draw_state.lines.extend(lines);
}
if let Some(width) = width {
if !matches!(self.state.status, Status::DoneHidden) {
self.style
.format_state(&self.state, &mut draw_state.lines, width);
}
}
drop(draw_state);
let _ = drawable.draw();
}
pub(crate) fn suspend<F: FnOnce() -> R, R>(&mut self, now: Instant, f: F) -> R {
if let Some((state, _)) = self.draw_target.remote() {
return state.write().unwrap().suspend(f, now);
}
if let Some(drawable) = self.draw_target.drawable(true, now) {
let _ = drawable.clear();
}
let ret = f();
let _ = self.draw(true, Instant::now());
ret
}
pub(crate) fn draw(&mut self, mut force_draw: bool, now: Instant) -> io::Result<()> {
force_draw |= self.state.is_finished();
let mut drawable = match self.draw_target.drawable(force_draw, now) {
Some(drawable) => drawable,
None => return Ok(()),
};
let width = drawable.width();
let mut draw_state = drawable.state();
if let Some(width) = width {
if !matches!(self.state.status, Status::DoneHidden) {
self.style
.format_state(&self.state, &mut draw_state.lines, width);
}
}
drop(draw_state);
drawable.draw()
}
}
impl Drop for BarState {
fn drop(&mut self) {
if self.state.is_finished() {
self.draw_target.mark_zombie();
return;
}
self.finish_using_style(Instant::now(), self.on_finish.clone());
self.draw_target.mark_zombie();
}
}
pub(crate) enum Reset {
Eta,
Elapsed,
All,
}
#[non_exhaustive]
pub struct ProgressState {
pos: Arc<AtomicPosition>,
len: Option<u64>,
pub(crate) tick: u64,
pub(crate) started: Instant,
status: Status,
est: Estimator,
pub(crate) message: TabExpandedString,
pub(crate) prefix: TabExpandedString,
}
impl ProgressState {
pub(crate) fn new(len: Option<u64>, pos: Arc<AtomicPosition>) -> Self {
let now = Instant::now();
Self {
pos,
len,
tick: 0,
status: Status::InProgress,
started: now,
est: Estimator::new(now),
message: TabExpandedString::NoTabs("".into()),
prefix: TabExpandedString::NoTabs("".into()),
}
}
pub fn is_finished(&self) -> bool {
match self.status {
Status::InProgress => false,
Status::DoneVisible => true,
Status::DoneHidden => true,
}
}
pub fn fraction(&self) -> f32 {
let pos = self.pos.pos.load(Ordering::Relaxed);
let pct = match (pos, self.len) {
(_, None) => 0.0,
(_, Some(0)) => 1.0,
(0, _) => 0.0,
(pos, Some(len)) => pos as f32 / len as f32,
};
pct.clamp(0.0, 1.0)
}
pub fn eta(&self) -> Duration {
if self.is_finished() {
return Duration::new(0, 0);
}
let len = match self.len {
Some(len) => len,
None => return Duration::new(0, 0),
};
let pos = self.pos.pos.load(Ordering::Relaxed);
let sps = self.est.steps_per_second(Instant::now());
if sps == 0.0 {
return Duration::new(0, 0);
}
secs_to_duration(len.saturating_sub(pos) as f64 / sps)
}
pub fn duration(&self) -> Duration {
if self.len.is_none() || self.is_finished() {
return Duration::new(0, 0);
}
self.started.elapsed().saturating_add(self.eta())
}
pub fn per_sec(&self) -> f64 {
if let Status::InProgress = self.status {
self.est.steps_per_second(Instant::now())
} else {
self.pos() as f64 / self.started.elapsed().as_secs_f64()
}
}
pub fn elapsed(&self) -> Duration {
self.started.elapsed()
}
pub fn pos(&self) -> u64 {
self.pos.pos.load(Ordering::Relaxed)
}
pub fn set_pos(&mut self, pos: u64) {
self.pos.set(pos);
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> Option<u64> {
self.len
}
pub fn set_len(&mut self, len: u64) {
self.len = Some(len);
}
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub(crate) enum TabExpandedString {
NoTabs(Cow<'static, str>),
WithTabs {
original: Cow<'static, str>,
expanded: OnceCell<String>,
tab_width: usize,
},
}
impl TabExpandedString {
pub(crate) fn new(s: Cow<'static, str>, tab_width: usize) -> Self {
if !s.contains('\t') {
Self::NoTabs(s)
} else {
Self::WithTabs {
original: s,
tab_width,
expanded: OnceCell::new(),
}
}
}
pub(crate) fn expanded(&self) -> &str {
match &self {
Self::NoTabs(s) => {
debug_assert!(!s.contains('\t'));
s
}
Self::WithTabs {
original,
tab_width,
expanded,
} => expanded.get_or_init(|| original.replace('\t', &" ".repeat(*tab_width))),
}
}
pub(crate) fn set_tab_width(&mut self, new_tab_width: usize) {
if let Self::WithTabs {
expanded,
tab_width,
..
} = self
{
if *tab_width != new_tab_width {
*tab_width = new_tab_width;
expanded.take();
}
}
}
}
#[derive(Debug)]
pub(crate) struct Estimator {
smoothed_steps_per_sec: f64,
double_smoothed_steps_per_sec: f64,
prev_steps: u64,
prev_time: Instant,
start_time: Instant,
}
impl Estimator {
fn new(now: Instant) -> Self {
Self {
smoothed_steps_per_sec: 0.0,
double_smoothed_steps_per_sec: 0.0,
prev_steps: 0,
prev_time: now,
start_time: now,
}
}
fn record(&mut self, new_steps: u64, now: Instant) {
if new_steps <= self.prev_steps || now <= self.prev_time {
if new_steps < self.prev_steps {
self.prev_steps = new_steps;
self.reset(now);
}
return;
}
let delta_steps = new_steps - self.prev_steps;
let delta_t = duration_to_secs(now - self.prev_time);
let new_steps_per_second = delta_steps as f64 / delta_t;
let weight = estimator_weight(delta_t);
self.smoothed_steps_per_sec =
self.smoothed_steps_per_sec * weight + new_steps_per_second * (1.0 - weight);
let delta_t_start = duration_to_secs(now - self.start_time);
let total_weight = 1.0 - estimator_weight(delta_t_start);
let normalized_smoothed_steps_per_sec = self.smoothed_steps_per_sec / total_weight;
self.double_smoothed_steps_per_sec = self.double_smoothed_steps_per_sec * weight
+ normalized_smoothed_steps_per_sec * (1.0 - weight);
self.prev_steps = new_steps;
self.prev_time = now;
}
pub(crate) fn reset(&mut self, now: Instant) {
self.smoothed_steps_per_sec = 0.0;
self.double_smoothed_steps_per_sec = 0.0;
self.prev_time = now;
self.start_time = now;
}
fn steps_per_second(&self, now: Instant) -> f64 {
let delta_t = duration_to_secs(now - self.prev_time);
let reweight = estimator_weight(delta_t);
let delta_t_start = duration_to_secs(now - self.start_time);
let total_weight = 1.0 - estimator_weight(delta_t_start);
let sps = self.smoothed_steps_per_sec * reweight / total_weight;
let dsps = self.double_smoothed_steps_per_sec * reweight + sps * (1.0 - reweight);
dsps / total_weight
}
}
pub(crate) struct AtomicPosition {
pub(crate) pos: AtomicU64,
capacity: AtomicU8,
prev: AtomicU64,
start: Instant,
}
impl AtomicPosition {
pub(crate) fn new() -> Self {
Self {
pos: AtomicU64::new(0),
capacity: AtomicU8::new(MAX_BURST),
prev: AtomicU64::new(0),
start: Instant::now(),
}
}
pub(crate) fn allow(&self, now: Instant) -> bool {
if now < self.start {
return false;
}
let mut capacity = self.capacity.load(Ordering::Acquire);
let prev = self.prev.load(Ordering::Acquire);
let elapsed = (now - self.start).as_nanos() as u64;
let diff = elapsed.saturating_sub(prev);
if capacity == 0 && diff < INTERVAL {
return false;
}
let (new, remainder) = ((diff / INTERVAL), (diff % INTERVAL));
capacity = Ord::min(MAX_BURST as u128, (capacity as u128) + (new as u128) - 1) as u8;
self.capacity.store(capacity, Ordering::Release);
self.prev.store(elapsed - remainder, Ordering::Release);
true
}
fn reset(&self, now: Instant) {
self.set(0);
let elapsed = (now.saturating_duration_since(self.start)).as_nanos() as u64;
self.prev.store(elapsed, Ordering::Release);
}
pub(crate) fn inc(&self, delta: u64) {
self.pos.fetch_add(delta, Ordering::SeqCst);
}
pub(crate) fn dec(&self, delta: u64) {
self.pos.fetch_sub(delta, Ordering::SeqCst);
}
pub(crate) fn set(&self, pos: u64) {
self.pos.store(pos, Ordering::Release);
}
}
const INTERVAL: u64 = 1_000_000;
const MAX_BURST: u8 = 10;
#[derive(Clone, Debug)]
pub enum ProgressFinish {
AndLeave,
WithMessage(Cow<'static, str>),
AndClear,
Abandon,
AbandonWithMessage(Cow<'static, str>),
}
impl Default for ProgressFinish {
fn default() -> Self {
Self::AndClear
}
}
fn estimator_weight(age: f64) -> f64 {
const EXPONENTIAL_WEIGHTING_SECONDS: f64 = 15.0;
0.1_f64.powf(age / EXPONENTIAL_WEIGHTING_SECONDS)
}
fn duration_to_secs(d: Duration) -> f64 {
d.as_secs() as f64 + f64::from(d.subsec_nanos()) / 1_000_000_000f64
}
fn secs_to_duration(s: f64) -> Duration {
let secs = s.trunc() as u64;
let nanos = (s.fract() * 1_000_000_000f64) as u32;
Duration::new(secs, nanos)
}
#[derive(Debug)]
pub(crate) enum Status {
InProgress,
DoneVisible,
DoneHidden,
}
pub(crate) const DEFAULT_TAB_WIDTH: usize = 8;
#[cfg(test)]
mod tests {
use super::*;
use crate::ProgressBar;
#[allow(clippy::uninlined_format_args)]
#[test]
fn test_steps_per_second() {
let test_rate = |items_per_second| {
let mut now = Instant::now();
let mut est = Estimator::new(now);
let mut pos = 0;
for _ in 0..20 {
pos += items_per_second;
now += Duration::from_secs(1);
est.record(pos, now);
}
let avg_steps_per_second = est.steps_per_second(now);
assert!(avg_steps_per_second > 0.0);
assert!(avg_steps_per_second.is_finite());
let absolute_error = (avg_steps_per_second - items_per_second as f64).abs();
let relative_error = absolute_error / items_per_second as f64;
assert!(
relative_error < 1.0 / 1e9,
"Expected rate: {}, actual: {}, relative error: {}",
items_per_second,
avg_steps_per_second,
relative_error
);
};
test_rate(1);
test_rate(1_000);
test_rate(1_000_000);
test_rate(1_000_000_000);
test_rate(1_000_000_001);
test_rate(100_000_000_000);
test_rate(1_000_000_000_000);
test_rate(100_000_000_000_000);
test_rate(1_000_000_000_000_000);
}
#[test]
fn test_double_exponential_ave() {
let mut now = Instant::now();
let mut est = Estimator::new(now);
let mut pos = 0;
let weight = 15;
for _ in 0..weight {
pos += 1;
now += Duration::from_secs(1);
est.record(pos, now);
}
now += Duration::from_secs(weight);
let single_target = 0.09 / 0.99;
let double_target = (0.9 * single_target + 0.09) / 0.99;
assert_eq!(est.steps_per_second(now), double_target);
}
#[test]
fn test_estimator_rewind_position() {
let mut now = Instant::now();
let mut est = Estimator::new(now);
now += Duration::from_secs(1);
est.record(1, now);
now += Duration::from_secs(1);
est.record(0, now);
now += Duration::from_secs(1);
est.record(1, now);
assert_eq!(est.steps_per_second(now), 1.0);
let pb = ProgressBar::hidden();
pb.set_length(10);
pb.set_position(1);
pb.tick();
pb.set_position(0);
}
#[test]
fn test_reset_eta() {
let mut now = Instant::now();
let mut est = Estimator::new(now);
now += Duration::from_secs(1);
est.record(2, now);
est.reset(now);
now += Duration::from_secs(1);
est.record(3, now);
assert_eq!(est.steps_per_second(now), 1.0);
}
#[test]
fn test_duration_stuff() {
let duration = Duration::new(42, 100_000_000);
let secs = duration_to_secs(duration);
assert_eq!(secs_to_duration(secs), duration);
}
#[test]
fn test_atomic_position_large_time_difference() {
let atomic_position = AtomicPosition::new();
let later = atomic_position.start + Duration::from_nanos(INTERVAL * u64::from(u8::MAX));
atomic_position.allow(later);
}
#[test]
fn test_atomic_position_reset() {
const ELAPSE_TIME: Duration = Duration::from_millis(20);
let mut pos = AtomicPosition::new();
pos.reset(pos.start + ELAPSE_TIME);
assert_eq!(*pos.pos.get_mut(), 0);
assert_eq!(*pos.prev.get_mut(), ELAPSE_TIME.as_nanos() as u64);
}
}