Skip to main content

runnerd_core/api/
stats.rs

1//! What the job history's statistics mean as every client shows them: a window's jobs summed, by day or whole,
2//! with their averages, success share and sampled peak. Plain derivations of [`Stats`]; the dashboard's history
3//! page only draws them.
4
5use super::{Stats, StatsRow};
6
7/// Finished jobs summed over a time, every repository of the query together, from `Stats::daily`.
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub struct StatsSummary {
10    /// Its start.
11    pub start: u64,
12    /// Jobs completed or lost.
13    pub jobs: u64,
14    /// Of those, concluded `success`.
15    pub succeeded: u64,
16    /// Of those, failed, timed out or lost.
17    pub failed: u64,
18    /// Of those, cancelled.
19    pub cancelled: u64,
20    /// Seconds from queued to started, summed.
21    pub queue_seconds: u64,
22    /// Seconds from started to finished, summed.
23    pub run_seconds: u64,
24    /// The highest sampled memory peak of a job; `None` when no job had a sample.
25    pub memory_peak: Option<u64>,
26}
27
28impl StatsSummary {
29    fn add(&mut self, row: &StatsRow) {
30        self.jobs += row.jobs;
31        self.succeeded += row.succeeded;
32        self.failed += row.failed;
33        self.cancelled += row.cancelled;
34        self.queue_seconds += row.queue_seconds;
35        self.run_seconds += row.run_seconds;
36        self.memory_peak = self
37            .memory_peak
38            .max((row.memory_peak > 0).then_some(row.memory_peak));
39    }
40
41    /// `summaries` together, starting at the first's start.
42    pub fn of(summaries: &[StatsSummary]) -> StatsSummary {
43        summaries.iter().fold(
44            StatsSummary {
45                start: summaries.first().map_or(0, |summary| summary.start),
46                ..StatsSummary::default()
47            },
48            |total, summary| StatsSummary {
49                start: total.start,
50                jobs: total.jobs + summary.jobs,
51                succeeded: total.succeeded + summary.succeeded,
52                failed: total.failed + summary.failed,
53                cancelled: total.cancelled + summary.cancelled,
54                queue_seconds: total.queue_seconds + summary.queue_seconds,
55                run_seconds: total.run_seconds + summary.run_seconds,
56                memory_peak: total.memory_peak.max(summary.memory_peak),
57            },
58        )
59    }
60
61    /// Jobs that finished otherwise: neither succeeded, failed nor cancelled, as a skipped one.
62    pub fn other(&self) -> u64 {
63        self.jobs
64            .saturating_sub(self.succeeded + self.failed + self.cancelled)
65    }
66
67    /// The share of its jobs that succeeded, in whole percent rounded down; `None` without jobs.
68    pub fn success_percent(&self) -> Option<u64> {
69        (self.succeeded * 100).checked_div(self.jobs)
70    }
71
72    /// Its jobs' average run, in seconds; `None` without jobs.
73    pub fn average_run(&self) -> Option<u64> {
74        self.run_seconds.checked_div(self.jobs)
75    }
76
77    /// Its jobs' average wait from queued to started, in seconds; `None` without jobs.
78    pub fn average_wait(&self) -> Option<u64> {
79        self.queue_seconds.checked_div(self.jobs)
80    }
81}
82
83impl Stats {
84    /// `days` days from `first`, the start of a UTC day, each summing every row whose bucket starts on it: a day
85    /// without jobs is there, empty. Rows of other buckets than days, or outside the days, are left out.
86    pub fn daily(&self, first: u64, days: u64) -> Vec<StatsSummary> {
87        let mut summaries: Vec<StatsSummary> = (0..days)
88            .map(|day| StatsSummary {
89                start: first + day * 86_400,
90                ..StatsSummary::default()
91            })
92            .collect();
93        for row in &self.rows {
94            if let Some(summary) = summaries
95                .iter_mut()
96                .find(|summary| summary.start == row.start)
97            {
98                summary.add(row);
99            }
100        }
101        summaries
102    }
103}
104
105#[cfg(test)]
106mod tests;