use std::mem;
use anyhow::Result;
use chrono::{DateTime, Utc};
use sysinfo::System;
use crate::Resolution;
const MAX_BATCH_POINTS: usize = 1 << 20;
pub struct TargetTimesIterator {
current: DateTime<Utc>,
end: DateTime<Utc>,
resolution: Resolution,
system: System,
}
impl TargetTimesIterator {
#[must_use]
pub fn new(start: DateTime<Utc>, end: DateTime<Utc>, resolution: Resolution) -> Self {
Self { current: start, end, resolution, system: crate::helpers::memory_only_system() }
}
pub fn estimate_len(&self) -> Result<usize> {
let duration = self.end.signed_duration_since(self.current);
let total_seconds = duration.num_seconds();
let interval_seconds = match self.resolution {
Resolution::Nanoseconds => return Ok(usize::try_from(duration.num_nanoseconds().unwrap_or(0))?),
Resolution::Microseconds => return Ok(usize::try_from(duration.num_microseconds().unwrap_or(0))?),
Resolution::Milliseconds => return Ok(usize::try_from(duration.num_milliseconds())?),
Resolution::Seconds => 1,
Resolution::Minutes => 60,
Resolution::Hours => 3600,
Resolution::Days => 86400,
Resolution::Weeks => 604_800,
Resolution::Months => 2_629_746, Resolution::Years => 31_556_952, };
Ok(usize::try_from(total_seconds / interval_seconds + 1)?)
}
fn remaining_points(&self) -> u64 {
let duration = self.end.signed_duration_since(self.current);
let interval_seconds = match self.resolution {
#[allow(clippy::cast_sign_loss)] Resolution::Nanoseconds => return duration.num_nanoseconds().unwrap_or(i64::MAX).max(0) as u64 + 1,
#[allow(clippy::cast_sign_loss)]
Resolution::Microseconds => return duration.num_microseconds().unwrap_or(i64::MAX).max(0) as u64 + 1,
#[allow(clippy::cast_sign_loss)]
Resolution::Milliseconds => return duration.num_milliseconds().max(0) as u64 + 1,
Resolution::Seconds => 1,
Resolution::Minutes => 60,
Resolution::Hours => 3600,
Resolution::Days => 86400,
Resolution::Weeks => 604_800,
Resolution::Months => 2_629_746,
Resolution::Years => 31_556_952,
};
#[allow(clippy::cast_sign_loss)] let total_seconds = duration.num_seconds().max(0) as u64;
total_seconds / interval_seconds + 1
}
fn next_impl(&mut self) -> Option<Vec<DateTime<Utc>>> {
if self.current > self.end {
return None;
}
self.system.refresh_memory();
let available_memory = self.system.free_memory().max(self.system.available_memory()); let m_t = self.system.total_memory();
#[allow(clippy::cast_precision_loss, clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let memory_threshold = (m_t as f64 * 0.8) as u64;
let point_size = mem::size_of::<DateTime<Utc>>() as u64;
let batch_size = {
let max_memory = available_memory.min(memory_threshold);
let memory_points = (max_memory / point_size / 2).max(100);
let bounded = memory_points.min(self.remaining_points()).min(MAX_BATCH_POINTS as u64);
usize::try_from(bounded).unwrap_or(MAX_BATCH_POINTS).max(1)
};
let mut batch = Vec::with_capacity(batch_size);
while batch.len() < batch_size && self.current <= self.end {
batch.push(self.current);
self.current += self.resolution.to_step();
}
if batch.is_empty() { None } else { Some(batch) }
}
}
impl Iterator for TargetTimesIterator {
type Item = Vec<DateTime<Utc>>;
fn next(&mut self) -> Option<Self::Item> {
self.next_impl()
}
}
#[must_use]
pub fn generate_target_times(start: DateTime<Utc>, end: DateTime<Utc>, resolution: Resolution) -> Vec<DateTime<Utc>> {
let mut times = Vec::new();
let mut current = start;
while current <= end {
times.push(current);
current += resolution.to_step();
}
times
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
fn utc(secs: i64) -> DateTime<Utc> {
Utc.timestamp_opt(secs, 0).single().expect("valid timestamp")
}
#[test]
fn batch_capacity_tracks_the_data_not_free_ram() {
let mut iter = TargetTimesIterator::new(utc(0), utc(10), Resolution::Seconds);
let batch = iter.next().expect("first batch is non-empty");
assert_eq!(batch.len(), 11, "11 inclusive one-second steps from 0..=10");
assert!(batch.capacity() <= MAX_BATCH_POINTS, "capacity {} must never exceed the absolute cap", batch.capacity());
assert!(batch.capacity() <= 64, "capacity {} must track the ~11 remaining points, not free RAM", batch.capacity());
}
#[test]
fn iterator_reproduces_the_reference_grid() {
let (start, end, res) = (utc(1_000), utc(1_600), Resolution::Seconds);
let batched: Vec<DateTime<Utc>> = TargetTimesIterator::new(start, end, res).flatten().collect();
assert_eq!(batched, generate_target_times(start, end, res));
}
#[test]
fn remaining_points_matches_estimate_len() {
let iter = TargetTimesIterator::new(utc(0), utc(3600), Resolution::Minutes);
assert_eq!(iter.remaining_points(), iter.estimate_len().expect("estimates") as u64);
}
#[test]
fn reversed_range_yields_no_timestamps() {
let mut iter = TargetTimesIterator::new(utc(100), utc(0), Resolution::Seconds);
assert!(iter.next().is_none(), "a reversed range produces nothing");
assert_eq!(TargetTimesIterator::new(utc(100), utc(0), Resolution::Seconds).remaining_points(), 1);
}
}