feat: API to retrieve percentile for benches #20
3 changed files with 261 additions and 0 deletions
|
@ -12,6 +12,7 @@ pub mod bench;
|
||||||
pub mod mcaptcha;
|
pub mod mcaptcha;
|
||||||
mod meta;
|
mod meta;
|
||||||
pub mod routes;
|
pub mod routes;
|
||||||
|
pub mod stats;
|
||||||
pub use routes::ROUTES;
|
pub use routes::ROUTES;
|
||||||
|
|
||||||
pub fn services(cfg: &mut ServiceConfig) {
|
pub fn services(cfg: &mut ServiceConfig) {
|
||||||
|
@ -19,6 +20,7 @@ pub fn services(cfg: &mut ServiceConfig) {
|
||||||
bench::services(cfg);
|
bench::services(cfg);
|
||||||
admin::services(cfg);
|
admin::services(cfg);
|
||||||
mcaptcha::services(cfg);
|
mcaptcha::services(cfg);
|
||||||
|
stats::services(cfg);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_random(len: usize) -> String {
|
pub fn get_random(len: usize) -> String {
|
||||||
|
|
|
@ -9,6 +9,7 @@ use super::admin::routes::Admin;
|
||||||
use super::bench::routes::Benches;
|
use super::bench::routes::Benches;
|
||||||
use super::mcaptcha::routes::Mcaptcha;
|
use super::mcaptcha::routes::Mcaptcha;
|
||||||
use super::meta::routes::Meta;
|
use super::meta::routes::Meta;
|
||||||
|
use super::stats::routes::Stats;
|
||||||
|
|
||||||
pub const ROUTES: Routes = Routes::new();
|
pub const ROUTES: Routes = Routes::new();
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ pub struct Routes {
|
||||||
pub meta: Meta,
|
pub meta: Meta,
|
||||||
pub benches: Benches,
|
pub benches: Benches,
|
||||||
pub mcaptcha: Mcaptcha,
|
pub mcaptcha: Mcaptcha,
|
||||||
|
pub stats: Stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Routes {
|
impl Routes {
|
||||||
|
@ -27,6 +29,7 @@ impl Routes {
|
||||||
meta: Meta::new(),
|
meta: Meta::new(),
|
||||||
benches: Benches::new(),
|
benches: Benches::new(),
|
||||||
mcaptcha: Mcaptcha::new(),
|
mcaptcha: Mcaptcha::new(),
|
||||||
|
stats: Stats::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
256
src/api/v1/stats.rs
Normal file
256
src/api/v1/stats.rs
Normal file
|
@ -0,0 +1,256 @@
|
||||||
|
// Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
// SPDX-FileCopyrightText: 2023 Aravinth Manivannan <realaravinth@batsense.net>
|
||||||
|
//
|
||||||
|
// 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<PercentileReq>,
|
||||||
|
) -> ServiceResult<impl Responder> {
|
||||||
|
struct Count {
|
||||||
|
count: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Option<u32>> {
|
||||||
|
struct Difficulty {
|
||||||
|
difficulty: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue