// 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/analytics/percentile", } } } } pub async fn percentile_bench_runner( data: &AppData, req: &PercentileReq, ) -> ServiceResult { let count = data.db.stats_get_num_logs_under_time(req.time).await?; if count == 0 { return Ok(PercentileResp { difficulty_factor: None, }); } if count < 2 { return Ok(PercentileResp { difficulty_factor: None, }); } let location = ((count - 1) as f64 * (req.percentile / 100.00)) + 1.00; let fraction = location - location.floor(); if fraction > 0.00 { if let (Some(base), Some(ceiling)) = ( data.db .stats_get_entry_at_location_for_time_limit_asc( req.time, location.floor() as u32, ) .await?, data.db .stats_get_entry_at_location_for_time_limit_asc( req.time, location.floor() as u32 + 1, ) .await?, ) { let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32; return Ok(PercentileResp { difficulty_factor: Some(res), }); } } else { if let Some(base) = data .db .stats_get_entry_at_location_for_time_limit_asc( req.time, location.floor() as u32, ) .await? { let res = base as u32; return Ok(PercentileResp { difficulty_factor: Some(res), }); } }; Ok(PercentileResp { difficulty_factor: None, }) } /// Get difficulty factor with max time limit for percentile of stats #[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")] async fn percentile_benches( data: AppData, payload: web::Json, ) -> ServiceResult { Ok(HttpResponse::Ok().json(percentile_bench_runner(&data, &payload).await?)) } #[derive(Clone, Debug, Deserialize, Builder, Serialize)] /// Health check return datatype pub struct PercentileReq { pub time: u32, pub percentile: f64, } #[derive(Clone, Debug, Deserialize, Builder, Serialize)] /// Health check return datatype pub struct PercentileResp { pub 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::*; #[actix_rt::test] async fn stats_bench_work_pg() { let data = crate::tests::pg::get_data().await; stats_bench_work(data).await; } #[actix_rt::test] async fn stats_bench_work_maria() { let data = crate::tests::maria::get_data().await; stats_bench_work(data).await; } async fn stats_bench_work(data: ArcData) { 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 = &data; { delete_user(&data, NAME).await; } register_and_signin(data, NAME, EMAIL, PASSWORD).await; // create captcha let (_, _signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; let app = get_app!(data).await; let page = 1; let tmp_id = uuid::Uuid::new_v4(); let download_rotue = V1_API_ROUTES .survey .get_download_route(&tmp_id.to_string(), page); let download_req = test::call_service( &app, test::TestRequest::get().uri(&download_rotue).to_request(), ) .await; assert_eq!(download_req.status(), StatusCode::NOT_FOUND); data.db .analytics_create_psuedo_id_if_not_exists(&key.key) .await .unwrap(); let psuedo_id = data .db .analytics_get_psuedo_id_from_capmaign_id(&key.key) .await .unwrap(); for i in 1..6 { println!("[{i}] Saving analytics"); let analytics = db_core::CreatePerformanceAnalytics { time: i, difficulty_factor: i, worker_type: "wasm".into(), }; data.db.analysis_save(&key.key, &analytics).await.unwrap(); } 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(&data, NAME).await; } }