From 3ba7b591f5d7a0e991955993c4eee4904da04aae Mon Sep 17 00:00:00 2001 From: Aravinth Manivannan Date: Wed, 1 Nov 2023 18:37:40 +0530 Subject: [PATCH] feat: API to retrieve percentile for benches --- src/api/v1/mod.rs | 2 + src/api/v1/routes.rs | 3 + src/api/v1/stats.rs | 256 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 261 insertions(+) create mode 100644 src/api/v1/stats.rs diff --git a/src/api/v1/mod.rs b/src/api/v1/mod.rs index d32d80b..fd6a6cf 100644 --- a/src/api/v1/mod.rs +++ b/src/api/v1/mod.rs @@ -12,6 +12,7 @@ pub mod bench; pub mod mcaptcha; mod meta; pub mod routes; +pub mod stats; pub use routes::ROUTES; pub fn services(cfg: &mut ServiceConfig) { @@ -19,6 +20,7 @@ pub fn services(cfg: &mut ServiceConfig) { bench::services(cfg); admin::services(cfg); mcaptcha::services(cfg); + stats::services(cfg); } pub fn get_random(len: usize) -> String { diff --git a/src/api/v1/routes.rs b/src/api/v1/routes.rs index faefd01..6a484d2 100644 --- a/src/api/v1/routes.rs +++ b/src/api/v1/routes.rs @@ -9,6 +9,7 @@ use super::admin::routes::Admin; use super::bench::routes::Benches; use super::mcaptcha::routes::Mcaptcha; use super::meta::routes::Meta; +use super::stats::routes::Stats; pub const ROUTES: Routes = Routes::new(); @@ -18,6 +19,7 @@ pub struct Routes { pub meta: Meta, pub benches: Benches, pub mcaptcha: Mcaptcha, + pub stats: Stats, } impl Routes { @@ -27,6 +29,7 @@ impl Routes { meta: Meta::new(), benches: Benches::new(), mcaptcha: Mcaptcha::new(), + stats: Stats::new(), } } } diff --git a/src/api/v1/stats.rs b/src/api/v1/stats.rs new file mode 100644 index 0000000..50600eb --- /dev/null +++ b/src/api/v1/stats.rs @@ -0,0 +1,256 @@ +// Copyright (C) 2021 Aravinth Manivannan +// SPDX-FileCopyrightText: 2023 Aravinth Manivannan +// +// SPDX-License-Identifier: AGPL-3.0-or-later +use actix_web::{web, HttpResponse, Responder}; +use derive_builder::Builder; +use serde::{Deserialize, Serialize}; + +use crate::errors::*; +use crate::AppData; + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +pub struct BuildDetails { + pub version: &'static str, + pub git_commit_hash: &'static str, +} + +pub mod routes { + use serde::{Deserialize, Serialize}; + + #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] + pub struct Stats { + pub percentile_benches: &'static str, + } + + impl Stats { + pub const fn new() -> Self { + Self { + percentile_benches: "/api/v1/stats/benches/percentile", + } + } + } +} + +/// Get difficulty factor with max time limit for percentile of stats +#[actix_web_codegen_const_routes::post( + path = "crate::V1_API_ROUTES.stats.percentile_benches" +)] +async fn percentile_benches( + data: AppData, + payload: web::Json, +) -> ServiceResult { + struct Count { + count: Option, + } + + let count = sqlx::query_as!( + Count, + "SELECT COUNT(difficulty) FROM survey_benches WHERE duration <= $1;", + payload.time as f32 + ) + .fetch_one(&data.db) + .await?; + + if count.count.is_none() { + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: None, + })); + } + + let count = count.count.unwrap(); + + if count < 2 { + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: None, + })); + } + + let location = ((count - 1) as f64 * (payload.percentile / 100.00)) + 1.00; + let fraction = location - location.floor(); + + async fn get_data_at_location( + data: &crate::Data, + time: u32, + location: i64, + ) -> ServiceResult> { + struct Difficulty { + difficulty: Option, + } + + match sqlx::query_as!( + Difficulty, + "SELECT + difficulty + FROM + survey_benches + WHERE + duration <= $1 + ORDER BY difficulty ASC LIMIT 1 OFFSET $2;", + time as f32, + location as i64 - 1, + ) + .fetch_one(&data.db) + .await + { + Ok(res) => Ok(Some(res.difficulty.unwrap() as u32)), + Err(sqlx::Error::RowNotFound) => Ok(None), + Err(e) => Err(e.into()), + } + } + + if fraction > 0.00 { + if let (Some(base), Some(ceiling)) = ( + get_data_at_location(&data, payload.time, location.floor() as i64).await?, + get_data_at_location(&data, payload.time, location.floor() as i64 + 1) + .await?, + ) { + let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32; + + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: Some(res), + })); + } + } else { + if let Some(base) = + get_data_at_location(&data, payload.time, location.floor() as i64).await? + { + let res = base as u32; + + return Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: Some(res), + })); + } + }; + Ok(HttpResponse::Ok().json(PercentileResp { + difficulty_factor: None, + })) +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct PercentileReq { + time: u32, + percentile: f64, +} + +#[derive(Clone, Debug, Deserialize, Builder, Serialize)] +/// Health check return datatype +pub struct PercentileResp { + difficulty_factor: Option, +} + +pub fn services(cfg: &mut web::ServiceConfig) { + cfg.service(percentile_benches); +} + +#[cfg(test)] +mod tests { + use actix_web::{http::StatusCode, test, App}; + + use super::*; + use crate::api::v1::services; + use crate::tests::get_test_data; + use crate::*; + + #[actix_rt::test] + async fn stats_bench_work() { + use crate::tests::*; + + const NAME: &str = "benchstatsuesr"; + const EMAIL: &str = "benchstatsuesr@testadminuser.com"; + const PASSWORD: &str = "longpassword2"; + + const DEVICE_USER_PROVIDED: &str = "foo"; + const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2"; + const THREADS: i32 = 4; + + { + let data = get_test_data().await; + delete_user(NAME, &data).await; + } + + let (data, _creds, signin_resp) = + register_and_signin(NAME, EMAIL, PASSWORD).await; + let cookies = get_cookie!(signin_resp); + let app = get_app!(data).await; + + let survey = get_survey_user(data.clone()).await; + let survey_cookie = get_cookie!(survey); + let campaign = create_new_campaign(NAME, data.clone(), cookies.clone()).await; + let campaign_config = + get_campaign_config(&campaign, data.clone(), survey_cookie.clone()).await; + + assert_eq!(DIFFICULTIES.to_vec(), campaign_config.difficulties); + + let submit_payload = crate::api::v1::bench::Submission { + device_user_provided: DEVICE_USER_PROVIDED.into(), + device_software_recognised: DEVICE_SOFTWARE_RECOGNISED.into(), + threads: THREADS, + benches: BENCHES.clone(), + submission_type: crate::api::v1::bench::SubmissionType::Wasm, + }; + + submit_bench(&submit_payload, &campaign, survey_cookie, data.clone()).await; + + let msg = PercentileReq { + time: 1, + percentile: 99.00, + }; + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert!(resp.difficulty_factor.is_none()); + + let msg = PercentileReq { + time: 1, + percentile: 100.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert!(resp.difficulty_factor.is_none()); + + let msg = PercentileReq { + time: 2, + percentile: 100.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert_eq!(resp.difficulty_factor.unwrap(), 2); + + let msg = PercentileReq { + time: 5, + percentile: 90.00, + }; + + let resp = test::call_service( + &app, + post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(), + ) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let resp: PercentileResp = test::read_body_json(resp).await; + + assert_eq!(resp.difficulty_factor.unwrap(), 4); + delete_user(NAME, &data).await; + } +}