Skip to content

Commit 5c41598

Browse files
cursoragentlovasoa
andcommitted
Refactor Server-Timing header generation and add tests
This commit refactors the Server-Timing header generation to be more efficient and adds comprehensive tests for its behavior in development and production environments. It also includes a new test SQL file for server timing verification. Co-authored-by: contact <[email protected]>
1 parent faf19a6 commit 5c41598

File tree

6 files changed

+124
-93
lines changed

6 files changed

+124
-93
lines changed

src/render.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ impl HeaderContext {
371371
fn add_server_timing_header(&mut self) {
372372
let header_value = self.request_context.server_timing.as_header_value();
373373
if !header_value.is_empty() {
374-
self.response.insert_header(("Server-Timing", header_value));
374+
self.response
375+
.insert_header(("Server-Timing", header_value.to_string()));
375376
}
376377
}
377378

src/webserver/http.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ async fn render_sql(
182182
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
183183
log::debug!("Received a request with the following parameters: {req_param:?}");
184184

185-
req_param.server_timing.borrow_mut().record("parse");
185+
req_param.server_timing.borrow_mut().record("parse_req");
186186

187187
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
188188
let source_path: PathBuf = sql_file.source_path.clone();

src/webserver/server_timing.rs

Lines changed: 18 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -3,32 +3,33 @@ use std::time::Instant;
33
#[derive(Debug, Clone)]
44
pub struct ServerTiming {
55
enabled: bool,
6-
start: Instant,
7-
events: Vec<TimingEvent>,
8-
}
9-
10-
#[derive(Debug, Clone)]
11-
struct TimingEvent {
12-
name: &'static str,
13-
duration_ms: f64,
6+
previous_event: Instant,
7+
header: String,
148
}
159

1610
impl ServerTiming {
1711
#[must_use]
1812
pub fn new(enabled: bool) -> Self {
1913
Self {
2014
enabled,
21-
start: Instant::now(),
22-
events: Vec::new(),
15+
previous_event: Instant::now(),
16+
header: String::new(),
2317
}
2418
}
2519

2620
pub fn record(&mut self, name: &'static str) {
2721
if !self.enabled {
2822
return;
2923
}
30-
let duration_ms = self.start.elapsed().as_secs_f64() * 1000.0;
31-
self.events.push(TimingEvent { name, duration_ms });
24+
let now = Instant::now();
25+
let duration_ms = (now - self.previous_event).as_secs_f64() * 1000.0;
26+
self.previous_event = now;
27+
28+
if !self.header.is_empty() {
29+
self.header.push_str(", ");
30+
}
31+
use std::fmt::Write;
32+
write!(&mut self.header, "{};dur={:.2}", name, duration_ms).unwrap();
3233
}
3334

3435
#[must_use]
@@ -37,15 +38,12 @@ impl ServerTiming {
3738
}
3839

3940
#[must_use]
40-
pub fn as_header_value(&self) -> String {
41-
if !self.enabled {
42-
return String::new();
41+
pub fn as_header_value(&self) -> &str {
42+
if self.enabled {
43+
&self.header
44+
} else {
45+
""
4346
}
44-
self.events
45-
.iter()
46-
.map(|event| format!("{};dur={:.2}", event.name, event.duration_ms))
47-
.collect::<Vec<_>>()
48-
.join(", ")
4947
}
5048
}
5149

@@ -54,74 +52,3 @@ impl Default for ServerTiming {
5452
Self::new(false)
5553
}
5654
}
57-
58-
#[cfg(test)]
59-
mod tests {
60-
use super::*;
61-
use std::thread;
62-
use std::time::Duration;
63-
64-
#[test]
65-
fn test_disabled_timing() {
66-
let mut timing = ServerTiming::new(false);
67-
assert!(!timing.is_enabled());
68-
timing.record("event1");
69-
timing.record("event2");
70-
assert_eq!(timing.as_header_value(), "");
71-
}
72-
73-
#[test]
74-
fn test_enabled_timing() {
75-
let mut timing = ServerTiming::new(true);
76-
assert!(timing.is_enabled());
77-
timing.record("event1");
78-
thread::sleep(Duration::from_millis(10));
79-
timing.record("event2");
80-
let header = timing.as_header_value();
81-
assert!(header.contains("event1;dur="));
82-
assert!(header.contains("event2;dur="));
83-
assert!(header.contains(", "));
84-
}
85-
86-
#[test]
87-
fn test_timing_values_increase() {
88-
let mut timing = ServerTiming::new(true);
89-
timing.record("first");
90-
thread::sleep(Duration::from_millis(5));
91-
timing.record("second");
92-
assert_eq!(timing.events.len(), 2);
93-
assert!(timing.events[1].duration_ms > timing.events[0].duration_ms);
94-
}
95-
96-
#[test]
97-
fn test_default_is_disabled() {
98-
let timing = ServerTiming::default();
99-
assert!(!timing.is_enabled());
100-
}
101-
102-
#[test]
103-
fn test_header_format() {
104-
let mut timing = ServerTiming::new(true);
105-
timing.events.push(TimingEvent {
106-
name: "test",
107-
duration_ms: 123.456,
108-
});
109-
let header = timing.as_header_value();
110-
assert_eq!(header, "test;dur=123.46");
111-
}
112-
113-
#[test]
114-
fn test_multiple_events_format() {
115-
let mut timing = ServerTiming::new(true);
116-
timing.events.push(TimingEvent {
117-
name: "first",
118-
duration_ms: 10.5,
119-
});
120-
timing.events.push(TimingEvent {
121-
name: "second",
122-
duration_ms: 25.75,
123-
});
124-
let header = timing.as_header_value();
125-
assert_eq!(header, "first;dur=10.50, second;dur=25.75");
126-
}
127-
}

tests/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod core;
55
mod data_formats;
66
mod errors;
77
mod requests;
8+
mod server_timing;
89
pub mod sql_test_files;
910
mod transactions;
1011
mod uploads;

tests/server_timing/mod.rs

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use actix_web::{http::StatusCode, test};
2+
use sqlpage::webserver::http::main_handler;
3+
4+
use crate::common::{get_request_to, make_app_data_from_config, test_config};
5+
6+
#[actix_web::test]
7+
async fn test_server_timing_disabled_in_production() -> actix_web::Result<()> {
8+
let mut config = test_config();
9+
config.environment = sqlpage::app_config::DevOrProd::Production;
10+
let app_data = make_app_data_from_config(config).await;
11+
12+
let req =
13+
crate::common::get_request_to_with_data("/tests/server_timing/simple_query.sql", app_data)
14+
.await?
15+
.to_srv_request();
16+
let resp = main_handler(req).await?;
17+
18+
assert_eq!(resp.status(), StatusCode::OK);
19+
assert!(
20+
resp.headers().get("Server-Timing").is_none(),
21+
"Server-Timing header should not be present in production mode"
22+
);
23+
Ok(())
24+
}
25+
26+
#[actix_web::test]
27+
async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> {
28+
let mut config = test_config();
29+
config.environment = sqlpage::app_config::DevOrProd::Development;
30+
let app_data = make_app_data_from_config(config).await;
31+
32+
let req =
33+
crate::common::get_request_to_with_data("/tests/server_timing/simple_query.sql", app_data)
34+
.await?
35+
.to_srv_request();
36+
let resp = main_handler(req).await?;
37+
38+
assert_eq!(resp.status(), StatusCode::OK);
39+
let server_timing_header = resp
40+
.headers()
41+
.get("Server-Timing")
42+
.expect("Server-Timing header should be present in development mode");
43+
let header_value = server_timing_header.to_str().unwrap();
44+
45+
assert!(
46+
header_value.contains("request;dur="),
47+
"Should contain request timing: {header_value}"
48+
);
49+
assert!(
50+
header_value.contains("sql_file;dur="),
51+
"Should contain sql_file timing: {header_value}"
52+
);
53+
assert!(
54+
header_value.contains("parse_req;dur="),
55+
"Should contain parse_req timing: {header_value}"
56+
);
57+
assert!(
58+
header_value.contains("db_conn;dur="),
59+
"Should contain db_conn timing: {header_value}"
60+
);
61+
assert!(
62+
header_value.contains("row;dur="),
63+
"Should contain row timing: {header_value}"
64+
);
65+
66+
Ok(())
67+
}
68+
69+
#[actix_web::test]
70+
async fn test_server_timing_format() -> actix_web::Result<()> {
71+
let req = get_request_to("/tests/server_timing/simple_query.sql")
72+
.await?
73+
.to_srv_request();
74+
let resp = main_handler(req).await?;
75+
76+
assert_eq!(resp.status(), StatusCode::OK);
77+
let server_timing_header = resp.headers().get("Server-Timing").unwrap();
78+
let header_value = server_timing_header.to_str().unwrap();
79+
80+
let parts: Vec<&str> = header_value.split(", ").collect();
81+
assert!(parts.len() >= 4, "Should have at least 4 timing events");
82+
83+
for part in parts {
84+
assert!(
85+
part.contains(";dur="),
86+
"Each part should have name;dur= format: {part}"
87+
);
88+
let dur_parts: Vec<&str> = part.split(";dur=").collect();
89+
assert_eq!(dur_parts.len(), 2, "Should have name and duration: {part}");
90+
let duration: f64 = dur_parts[1]
91+
.parse()
92+
.expect("Duration should be a valid number");
93+
assert!(
94+
duration >= 0.0,
95+
"Duration should be non-negative: {duration}"
96+
);
97+
}
98+
99+
Ok(())
100+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SELECT 'text' AS component, 'Server timing test' AS contents;
2+
SELECT 'Hello' AS value FROM (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3);

0 commit comments

Comments
 (0)