use serde::{Deserialize, Serialize};
mod facts;
pub use facts::{
JobTitle, Milestone, Outcome, OutcomeKind, Phase, PhaseKind, RunComparison, SampleUsage,
job_title,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum JobState {
Queued,
Running,
Completed,
Lost,
Elsewhere,
}
impl JobState {
pub fn is_final(self) -> bool {
matches!(
self,
JobState::Completed | JobState::Lost | JobState::Elsewhere
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobSummary {
pub id: u64,
pub repository: String,
pub run_id: u64,
pub run_attempt: u32,
pub workflow: Option<String>,
pub name: String,
pub branch: Option<String>,
pub url: Option<String>,
pub labels: Vec<String>,
pub state: JobState,
pub conclusion: Option<String>,
pub runner: Option<JobRunner>,
pub timeline: Timeline,
pub metrics: JobMetrics,
pub log: LogSummary,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Job {
#[serde(flatten)]
pub summary: JobSummary,
pub steps: Vec<Step>,
#[serde(default)]
pub record: Option<super::RecordNote>,
#[serde(default)]
pub admission: Option<super::Admission>,
}
impl Job {
pub fn chart_lines(&self, profile: Option<&super::ProfileStatus>) -> ChartLines {
let limits = self.limits(profile);
let admission = self.admission.as_ref();
let name = profile.map_or("profile", |profile| profile.name.as_str());
ChartLines {
memory: chart_lines(
limits.memory,
admission.map(|admission| admission.reserved),
|value| crate::config::ByteSize(value).to_string(),
name,
),
cpu: chart_lines(
limits.cpu,
admission
.and_then(|admission| admission.cpu)
.map(|cpu| cpu.reserved),
super::cpus,
name,
),
}
}
pub fn limits(&self, profile: Option<&super::ProfileStatus>) -> Limits {
let runner = self.summary.runner.as_ref();
let limit = |cap: Option<u64>, now: Option<u64>| match (cap, now) {
(Some(value), _) => Some(Limit {
value,
source: LimitSource::Cap,
}),
(None, Some(value)) => Some(Limit {
value,
source: LimitSource::ProfileNow,
}),
(None, None) => None,
};
Limits {
memory: limit(
runner.and_then(|runner| runner.memory_cap),
profile.map(|profile| profile.memory),
),
cpu: limit(
runner.and_then(|runner| runner.cpu_cap),
profile.map(|profile| u64::from(profile.cpus) * 1000),
),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limits {
pub memory: Option<Limit>,
pub cpu: Option<Limit>,
}
impl Limits {
pub fn line(&self) -> Option<String> {
let memory = self.memory.map(|limit| {
(
limit.source,
crate::config::ByteSize(limit.value).to_string(),
)
});
let cpu = self
.cpu
.map(|limit| (limit.source, super::cpus(limit.value)));
let clause = |source: LimitSource, figures: &str| match source {
LimitSource::Cap => format!("capped at {figures}"),
LimitSource::ProfileNow => format!("profile now {figures}"),
};
match (memory, cpu) {
(Some((memory_source, memory)), Some((cpu_source, cpu)))
if memory_source == cpu_source =>
{
Some(clause(memory_source, &format!("{memory} and {cpu}")))
}
(Some((memory_source, memory)), Some((cpu_source, cpu))) => Some(format!(
"{}; {}",
clause(memory_source, &memory),
clause(cpu_source, &cpu)
)),
(Some((source, figure)), None) | (None, Some((source, figure))) => {
Some(clause(source, &figure))
}
(None, None) => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Limit {
pub value: u64,
pub source: LimitSource,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LimitSource {
Cap,
ProfileNow,
}
fn chart_lines(
limit: Option<Limit>,
reserved: Option<u64>,
value: fn(u64) -> String,
profile: &str,
) -> Vec<ChartLine> {
let line = |figure, kind, label: &str| ChartLine {
value: figure,
kind,
label: format!("{label} {}", value(figure)),
};
let mut lines = vec![];
match limit {
Some(Limit {
value: cap,
source: LimitSource::Cap,
}) if reserved == Some(cap) => {
return vec![line(cap, ChartLineKind::CapAndReserved, "cap and reserved")];
}
Some(Limit {
value: cap,
source: LimitSource::Cap,
}) => lines.push(line(cap, ChartLineKind::Cap, "cap")),
Some(Limit {
value: now,
source: LimitSource::ProfileNow,
}) => lines.push(line(
now,
ChartLineKind::ProfileNow,
&format!("{profile} now"),
)),
None => {}
}
if let Some(reserved) = reserved {
lines.push(line(reserved, ChartLineKind::Reserved, "reserved"));
}
lines
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChartLines {
pub memory: Vec<ChartLine>,
pub cpu: Vec<ChartLine>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChartLine {
pub value: u64,
pub kind: ChartLineKind,
pub label: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ChartLineKind {
Cap,
ProfileNow,
Reserved,
CapAndReserved,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobRunner {
pub name: String,
pub profile: String,
pub slot: usize,
pub memory_cap: Option<u64>,
pub cpu_cap: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Timeline {
pub queued_at: u64,
pub container_started_at: Option<u64>,
pub started_at: Option<u64>,
pub finished_at: Option<u64>,
pub cleaned_at: Option<u64>,
}
impl Timeline {
pub fn runner_from(&self) -> Option<u64> {
self.container_started_at
.map(|started| started.max(self.queued_at))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobMetrics {
pub cpu_time: Option<u64>,
pub cpu_peak: Option<u64>,
pub cpu_average: Option<u64>,
pub memory_peak: Option<u64>,
pub memory_average: Option<u64>,
pub network_received: Option<u64>,
pub network_sent: Option<u64>,
pub mirror_bytes: u64,
pub build_cache: BuildCacheCounts,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sock_throttled: Option<u64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildCacheCounts {
pub hits: u64,
pub misses: u64,
pub read_bytes: u64,
pub written_bytes: u64,
pub refused: u64,
pub writer: bool,
}
impl BuildCacheCounts {
pub fn lookups(&self) -> u64 {
self.hits + self.misses
}
pub fn hit_percent(&self) -> Option<u64> {
let lookups = self.lookups();
(lookups > 0).then(|| self.hits * 100 / lookups)
}
pub fn add(&mut self, other: &BuildCacheCounts) {
self.hits += other.hits;
self.misses += other.misses;
self.read_bytes += other.read_bytes;
self.written_bytes += other.written_bytes;
self.refused += other.refused;
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogSummary {
pub lines: u64,
pub bytes: u64,
pub truncated: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Step {
pub number: u32,
pub name: String,
pub status: String,
pub conclusion: Option<String>,
pub started_at: Option<u64>,
pub completed_at: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobQuery {
pub repository: Option<String>,
pub state: Option<JobState>,
pub conclusion: Option<String>,
pub workflow: Option<String>,
pub name: Option<String>,
pub since: Option<u64>,
pub until: Option<u64>,
pub before: Option<u64>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobPage {
pub jobs: Vec<JobSummary>,
pub next: Option<u64>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct LogQuery {
pub from: Option<u64>,
#[serde(default)]
pub follow: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Sample {
pub at: u64,
pub cpu: u64,
pub memory: u64,
pub network_received: u64,
pub network_sent: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SampleSeries {
pub job: u64,
pub interval: u64,
pub samples: Vec<Sample>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostSample {
pub at: u64,
pub cpu: u64,
pub memory: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostMemory {
pub at: u64,
pub total: u64,
pub available: u64,
pub slice: u64,
pub some: Option<Pressure>,
pub full: Option<Pressure>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub holders: Vec<Holder>,
}
impl HostMemory {
pub fn outside(&self) -> u64 {
self.total
.saturating_sub(self.available)
.saturating_sub(self.slice)
}
pub fn outside_line(&self) -> String {
let size = |bytes: u64| crate::config::ByteSize(bytes).to_string();
let line = format!("programs outside CI hold {}", size(self.outside()));
let named: Vec<String> = self
.holders
.iter()
.map(|holder| format!("{} {}", holder.name, size(holder.memory)))
.collect();
match named.as_slice() {
[] => line,
[one] => format!("{line}, most {one}"),
[rest @ .., last] => format!("{line}, most {} and {last}", rest.join(", ")),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Holder {
pub name: String,
pub memory: u64,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Pressure {
pub avg10: u32,
pub avg60: u32,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostSampleQuery {
pub since: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostSamples {
pub interval: u64,
pub samples: Vec<HostSample>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Bucket {
Hour,
#[default]
Day,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ActivityQuery {
pub since: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Activity {
pub since: u64,
pub until: u64,
pub started: Vec<Started>,
pub finished: u64,
pub failed: u64,
pub runner_failed: u64,
}
impl Activity {
pub fn wait_p95(&self) -> Option<u64> {
let mut waits: Vec<u64> = self.started.iter().map(|start| start.waited).collect();
waits.sort_unstable();
let rank = (waits.len() * 95).div_ceil(100);
waits.get(rank.checked_sub(1)?).copied()
}
pub fn wait_hours(&self) -> [u64; 24] {
const HOUR: u64 = 3600;
let since = self.until.saturating_sub(24 * HOUR);
let mut hours = [0u64; 24];
for start in self
.started
.iter()
.filter(|start| (since..=self.until).contains(&start.at))
{
let back =
usize::try_from((self.until - start.at) / HOUR).map_or(23, |back| back.min(23));
hours[23 - back] = hours[23 - back].max(start.waited);
}
hours
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Started {
pub at: u64,
pub waited: u64,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatsQuery {
pub repository: Option<String>,
pub since: Option<u64>,
pub until: Option<u64>,
#[serde(default)]
pub bucket: Bucket,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Stats {
pub bucket: Bucket,
pub rows: Vec<StatsRow>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct StatsRow {
pub start: u64,
pub repository: String,
pub jobs: u64,
pub succeeded: u64,
pub failed: u64,
pub cancelled: u64,
pub queue_seconds: u64,
pub run_seconds: u64,
pub cpu_time: u64,
pub memory_peak: u64,
pub network_received: u64,
pub mirror_bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", tag = "kind")]
pub enum CacheKind {
Snapshot {
profile: String,
},
Slot {
profile: String,
slot: usize,
},
Pnpm,
Maven,
Mirror,
Build,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheVolume {
pub name: String,
#[serde(flatten)]
pub kind: CacheKind,
pub bytes: u64,
pub limit: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Caches {
pub measured_at: Option<u64>,
pub volumes: Vec<CacheVolume>,
pub build: Option<BuildCacheStats>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BuildCacheStats {
pub bytes: u64,
pub entries: u64,
pub limit: u64,
pub week: BuildCacheCounts,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheHistoryQuery {
pub since: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheSize {
pub at: u64,
pub name: String,
pub bytes: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CacheHistory {
pub sizes: Vec<CacheSize>,
}