1use actix_web::{web, HttpResponse, Responder};
6use derive_builder::Builder;
7use serde::{Deserialize, Serialize};
8
9use crate::errors::*;
10use crate::AppData;
11
12#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
13pub struct BuildDetails {
14 pub version: &'static str,
15 pub git_commit_hash: &'static str,
16}
17
18pub mod routes {
19 use serde::{Deserialize, Serialize};
20
21 #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
22 pub struct Stats {
23 pub percentile_benches: &'static str,
24 }
25
26 impl Stats {
27 pub const fn new() -> Self {
28 Self {
29 percentile_benches: "/api/v1/stats/analytics/percentile",
30 }
31 }
32 }
33}
34
35pub async fn percentile_bench_runner(
36 data: &AppData,
37 req: &PercentileReq,
38) -> ServiceResult<PercentileResp> {
39 let count = data.db.stats_get_num_logs_under_time(req.time).await?;
40
41 if count == 0 {
42 return Ok(PercentileResp {
43 difficulty_factor: None,
44 });
45 }
46
47 if count < 2 {
48 return Ok(PercentileResp {
49 difficulty_factor: None,
50 });
51 }
52
53 let location = ((count - 1) as f64 * (req.percentile / 100.00)) + 1.00;
54 let fraction = location - location.floor();
55
56 if fraction > 0.00 {
57 if let (Some(base), Some(ceiling)) = (
58 data.db
59 .stats_get_entry_at_location_for_time_limit_asc(
60 req.time,
61 location.floor() as u32,
62 )
63 .await?,
64 data.db
65 .stats_get_entry_at_location_for_time_limit_asc(
66 req.time,
67 location.floor() as u32 + 1,
68 )
69 .await?,
70 ) {
71 let res = base as u32 + ((ceiling - base) as f64 * fraction).floor() as u32;
72
73 return Ok(PercentileResp {
74 difficulty_factor: Some(res),
75 });
76 }
77 } else {
78 if let Some(base) = data
79 .db
80 .stats_get_entry_at_location_for_time_limit_asc(
81 req.time,
82 location.floor() as u32,
83 )
84 .await?
85 {
86 let res = base as u32;
87
88 return Ok(PercentileResp {
89 difficulty_factor: Some(res),
90 });
91 }
92 };
93 Ok(PercentileResp {
94 difficulty_factor: None,
95 })
96}
97
98#[my_codegen::post(path = "crate::V1_API_ROUTES.stats.percentile_benches")]
100async fn percentile_benches(
101 data: AppData,
102 payload: web::Json<PercentileReq>,
103) -> ServiceResult<impl Responder> {
104 Ok(HttpResponse::Ok().json(percentile_bench_runner(&data, &payload).await?))
105}
106
107#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
108pub struct PercentileReq {
110 pub time: u32,
111 pub percentile: f64,
112}
113
114#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
115pub struct PercentileResp {
117 pub difficulty_factor: Option<u32>,
118}
119
120pub fn services(cfg: &mut web::ServiceConfig) {
121 cfg.service(percentile_benches);
122}
123
124#[cfg(test)]
125mod tests {
126 use actix_web::{http::StatusCode, test, App};
127
128 use super::*;
129 use crate::api::v1::services;
130 use crate::*;
131
132 #[actix_rt::test]
133 async fn stats_bench_work_pg() {
134 let data = crate::tests::pg::get_data().await;
135 stats_bench_work(data).await;
136 }
137
138 #[actix_rt::test]
139 async fn stats_bench_work_maria() {
140 let data = crate::tests::maria::get_data().await;
141 stats_bench_work(data).await;
142 }
143
144 async fn stats_bench_work(data: ArcData) {
145 use crate::tests::*;
146
147 const NAME: &str = "benchstatsuesr";
148 const EMAIL: &str = "benchstatsuesr@testadminuser.com";
149 const PASSWORD: &str = "longpassword2";
150
151 const DEVICE_USER_PROVIDED: &str = "foo";
152 const DEVICE_SOFTWARE_RECOGNISED: &str = "Foobar.v2";
153 const THREADS: i32 = 4;
154
155 let data = &data;
156 {
157 delete_user(&data, NAME).await;
158 }
159
160 register_and_signin(data, NAME, EMAIL, PASSWORD).await;
161 let (_, _signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await;
163 let app = get_app!(data).await;
164
165 let page = 1;
166 let tmp_id = uuid::Uuid::new_v4();
167 let download_rotue = V1_API_ROUTES
168 .survey
169 .get_download_route(&tmp_id.to_string(), page);
170
171 let download_req = test::call_service(
172 &app,
173 test::TestRequest::get().uri(&download_rotue).to_request(),
174 )
175 .await;
176 assert_eq!(download_req.status(), StatusCode::NOT_FOUND);
177
178 data.db
179 .analytics_create_psuedo_id_if_not_exists(&key.key)
180 .await
181 .unwrap();
182
183 let psuedo_id = data
184 .db
185 .analytics_get_psuedo_id_from_capmaign_id(&key.key)
186 .await
187 .unwrap();
188
189 for i in 1..6 {
190 println!("[{i}] Saving analytics");
191 let analytics = db_core::CreatePerformanceAnalytics {
192 time: i,
193 difficulty_factor: i,
194 worker_type: "wasm".into(),
195 };
196 data.db.analysis_save(&key.key, &analytics).await.unwrap();
197 }
198
199 let msg = PercentileReq {
200 time: 1,
201 percentile: 99.00,
202 };
203 let resp = test::call_service(
204 &app,
205 post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
206 )
207 .await;
208 assert_eq!(resp.status(), StatusCode::OK);
209 let resp: PercentileResp = test::read_body_json(resp).await;
210
211 assert!(resp.difficulty_factor.is_none());
212
213 let msg = PercentileReq {
214 time: 1,
215 percentile: 100.00,
216 };
217
218 let resp = test::call_service(
219 &app,
220 post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
221 )
222 .await;
223 assert_eq!(resp.status(), StatusCode::OK);
224 let resp: PercentileResp = test::read_body_json(resp).await;
225
226 assert!(resp.difficulty_factor.is_none());
227
228 let msg = PercentileReq {
229 time: 2,
230 percentile: 100.00,
231 };
232
233 let resp = test::call_service(
234 &app,
235 post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
236 )
237 .await;
238 assert_eq!(resp.status(), StatusCode::OK);
239 let resp: PercentileResp = test::read_body_json(resp).await;
240
241 assert_eq!(resp.difficulty_factor.unwrap(), 2);
242
243 let msg = PercentileReq {
244 time: 5,
245 percentile: 90.00,
246 };
247
248 let resp = test::call_service(
249 &app,
250 post_request!(&msg, V1_API_ROUTES.stats.percentile_benches).to_request(),
251 )
252 .await;
253 assert_eq!(resp.status(), StatusCode::OK);
254 let resp: PercentileResp = test::read_body_json(resp).await;
255
256 assert_eq!(resp.difficulty_factor.unwrap(), 4);
257 delete_user(&data, NAME).await;
258 }
259}