diff --git a/bin/core/src/requests/read/build.rs b/bin/core/src/requests/read/build.rs index 37ce19514..a9f5e33f3 100644 --- a/bin/core/src/requests/read/build.rs +++ b/bin/core/src/requests/read/build.rs @@ -1,13 +1,17 @@ +use std::collections::HashMap; + use anyhow::Context; +use async_timing_util::unix_timestamp_ms; use async_trait::async_trait; +use futures::TryStreamExt; use monitor_types::{ entities::{ build::{Build, BuildActionState}, - PermissionLevel, + Operation, PermissionLevel, update::UpdateStatus, }, requests::read::*, }; -use mungos::mongodb::bson::doc; +use mungos::mongodb::{bson::doc, options::FindOptions}; use resolver_api::Resolve; use crate::{auth::RequestUser, resource::Resource, state::State}; @@ -87,3 +91,130 @@ impl Resolve for State { Ok(res) } } + +const ONE_DAY_MS: i64 = 86400000; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildMonthlyStats { page }: GetBuildMonthlyStats, + _: RequestUser, + ) -> anyhow::Result { + let curr_ts = unix_timestamp_ms() as i64; + let next_day = curr_ts - curr_ts % ONE_DAY_MS + ONE_DAY_MS; + + let close_ts = next_day - page as i64 * 30 * ONE_DAY_MS; + let open_ts = close_ts - 30 * ONE_DAY_MS; + + let mut build_updates = self + .db + .updates + .collection + .find( + doc! { + "start_ts": { + "$gte": open_ts, + "$lt": close_ts + }, + "operation": Operation::RunBuild.to_string(), + }, + None, + ) + .await + .context("failed to get updates cursor")?; + + let mut days = HashMap::::with_capacity(32); + + let mut curr = open_ts; + + while curr < close_ts { + let stats = BuildStatsDay { + ts: curr as f64, + ..Default::default() + }; + days.insert(curr, stats); + curr += ONE_DAY_MS; + } + + while let Some(update) = build_updates.try_next().await? { + if let Some(end_ts) = update.end_ts { + let day = update.start_ts - update.start_ts % ONE_DAY_MS; + let mut entry = days.entry(day).or_default(); + entry.count += 1.0; + entry.time += ms_to_hour(end_ts - update.start_ts); + } + } + + Ok(GetBuildMonthlyStatsResponse::new( + days.into_values().collect(), + )) + } +} + +const MS_TO_HOUR_DIVISOR: f64 = 1000.0 * 60.0 * 60.0; +fn ms_to_hour(duration: i64) -> f64 { + duration as f64 / MS_TO_HOUR_DIVISOR +} + +const NUM_VERSIONS_PER_PAGE: u64 = 10; + +#[async_trait] +impl Resolve for State { + async fn resolve( + &self, + GetBuildVersions { + id, + page, + major, + minor, + patch, + }: GetBuildVersions, + user: RequestUser, + ) -> anyhow::Result> { + let _: Build = self + .get_resource_check_permissions(&id, &user, PermissionLevel::Read) + .await?; + + let mut filter = doc! { + "target": { + "type": "Build", + "id": id + }, + "operation": Operation::RunBuild.to_string(), + "status": UpdateStatus::Complete.to_string(), + "success": true + }; + if let Some(major) = major { + filter.insert("version.major", major); + } + if let Some(minor) = minor { + filter.insert("version.minor", minor); + } + if let Some(patch) = patch { + filter.insert("version.patch", patch); + } + let versions = self + .db + .updates + .get_some( + filter, + FindOptions::builder() + .sort(doc! { "_id": -1 }) + .limit(NUM_VERSIONS_PER_PAGE as i64) + .skip(page as u64 * NUM_VERSIONS_PER_PAGE) + .build(), + ) + .await + .context("failed to pull versions from mongo")? + .into_iter() + .map(|u| (u.version, u.start_ts)) + .filter(|(v, _)| !v.is_none()) + .map(|(version, ts)| BuildVersionResponseItem { + version, + ts, + }) + .collect(); + Ok(versions) + } +} diff --git a/bin/core/src/requests/read/mod.rs b/bin/core/src/requests/read/mod.rs index 882726aee..736fbe130 100644 --- a/bin/core/src/requests/read/mod.rs +++ b/bin/core/src/requests/read/mod.rs @@ -67,6 +67,8 @@ enum ReadRequest { GetBuild(GetBuild), ListBuilds(ListBuilds), GetBuildActionState(GetBuildActionState), + GetBuildMonthlyStats(GetBuildMonthlyStats), + GetBuildVersions(GetBuildVersions), // ==== REPO ==== GetReposSummary(GetReposSummary), diff --git a/client/ts/src/responses.ts b/client/ts/src/responses.ts index dc67d133b..b0ba33bad 100644 --- a/client/ts/src/responses.ts +++ b/client/ts/src/responses.ts @@ -42,6 +42,8 @@ export type ReadResponses = { GetBuild: Types.Build; ListBuilds: Types.BuildListItem[]; GetBuildActionState: Types.BuildActionState; + GetBuildMonthlyStats: Types.GetBuildMonthlyStatsResponse; + GetBuildVersions: Types.BuildVersionResponseItem[]; // ==== REPO ==== GetReposSummary: Types.GetReposSummaryResponse; diff --git a/client/ts/src/types.ts b/client/ts/src/types.ts index 5f4d9334a..04699c0a5 100644 --- a/client/ts/src/types.ts +++ b/client/ts/src/types.ts @@ -767,6 +767,35 @@ export interface GetBuildsSummaryResponse { total: number; } +export interface GetBuildMonthlyStats { + page?: number; +} + +export interface BuildStatsDay { + time: number; + count: number; + ts: number; +} + +export interface GetBuildMonthlyStatsResponse { + total_time: number; + total_count: number; + days: BuildStatsDay[]; +} + +export interface GetBuildVersions { + id: string; + page?: number; + major?: number; + minor?: number; + patch?: number; +} + +export interface BuildVersionResponseItem { + version: Version; + ts: I64; +} + export interface GetBuilder { id: string; } @@ -1231,6 +1260,8 @@ export type ReadRequest = | { type: "GetBuild", params: GetBuild } | { type: "ListBuilds", params: ListBuilds } | { type: "GetBuildActionState", params: GetBuildActionState } + | { type: "GetBuildMonthlyStats", params: GetBuildMonthlyStats } + | { type: "GetBuildVersions", params: GetBuildVersions } | { type: "GetReposSummary", params: GetReposSummary } | { type: "GetRepo", params: GetRepo } | { type: "ListRepos", params: ListRepos } diff --git a/lib/types/src/requests/read/build.rs b/lib/types/src/requests/read/build.rs index 55cee7700..9da7947db 100644 --- a/lib/types/src/requests/read/build.rs +++ b/lib/types/src/requests/read/build.rs @@ -1,3 +1,5 @@ +use std::cmp::Ordering; + use resolver_api::derive::Request; use serde::{Deserialize, Serialize}; use typeshare::typeshare; @@ -59,3 +61,73 @@ pub struct GetBuildsSummary {} pub struct GetBuildsSummaryResponse { pub total: u32, } + +// + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Request)] +#[response(GetBuildMonthlyStatsResponse)] +pub struct GetBuildMonthlyStats { + #[serde(default)] + pub page: u32, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct GetBuildMonthlyStatsResponse { + pub total_time: f64, // in hours + pub total_count: f64, // number of builds + pub days: Vec, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildStatsDay { + pub time: f64, + pub count: f64, + pub ts: f64, +} + +impl GetBuildMonthlyStatsResponse { + pub fn new(mut days: Vec) -> GetBuildMonthlyStatsResponse { + days.sort_by(|a, b| { + if a.ts < b.ts { + Ordering::Less + } else { + Ordering::Greater + } + }); + let mut total_time = 0.0; + let mut total_count = 0.0; + for day in &days { + total_time += day.time; + total_count += day.count; + } + GetBuildMonthlyStatsResponse { + total_time, + total_count, + days, + } + } +} + +// + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Request)] +#[response(Vec)] +pub struct GetBuildVersions { + pub id: String, + #[serde(default)] + pub page: u32, + pub major: Option, + pub minor: Option, + pub patch: Option, +} + +#[typeshare] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct BuildVersionResponseItem { + pub version: Version, + pub ts: I64, +} \ No newline at end of file