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