Skip to content

Commit 06df761

Browse files
committed
Refactor: Update ServerTiming to use Arc and simplify header generation
This commit modifies the ServerTiming structure to utilize Arc for thread-safe reference counting, enhancing performance and memory management. The header generation logic is streamlined, and related code is updated to reflect these changes. Additionally, tests are adjusted to ensure proper functionality with the new implementation. Co-authored-by: contact <[email protected]>
1 parent 38e08d7 commit 06df761

File tree

6 files changed

+69
-61
lines changed

6 files changed

+69
-61
lines changed

src/render.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -369,10 +369,8 @@ impl HeaderContext {
369369
}
370370

371371
fn add_server_timing_header(&mut self) {
372-
let header_value = self.request_context.server_timing.as_header_value();
373-
if !header_value.is_empty() {
374-
self.response
375-
.insert_header(("Server-Timing", header_value.to_string()));
372+
if let Some(header_value) = self.request_context.server_timing.header_value() {
373+
self.response.insert_header(("Server-Timing", header_value));
376374
}
377375
}
378376

src/webserver/database/execute_queries.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ async fn take_connection<'a>(
284284
match db.connection.acquire().await {
285285
Ok(c) => {
286286
log::debug!("Acquired a database connection");
287-
request.server_timing.borrow_mut().record("db_conn");
287+
request.server_timing.record("db_conn");
288288
*conn = Some(c);
289289
Ok(conn.as_mut().unwrap())
290290
}

src/webserver/http.rs

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub struct RequestContext {
4747
pub is_embedded: bool,
4848
pub source_path: PathBuf,
4949
pub content_security_policy: ContentSecurityPolicy,
50-
pub server_timing: ServerTiming,
50+
pub server_timing: Arc<ServerTiming>,
5151
}
5252

5353
async fn stream_response(stream: impl Stream<Item = DbItem>, mut renderer: AnyRenderBodyContext) {
@@ -176,23 +176,21 @@ async fn render_sql(
176176
.clone()
177177
.into_inner();
178178

179-
let mut req_param =
180-
extract_request_info(srv_req, Arc::clone(&app_state), server_timing.clone())
181-
.await
182-
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
179+
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
180+
.await
181+
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
183182
log::debug!("Received a request with the following parameters: {req_param:?}");
184183

185-
req_param.server_timing.borrow_mut().record("parse_req");
184+
req_param.server_timing.record("parse_req");
186185

187186
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
188187
let source_path: PathBuf = sql_file.source_path.clone();
189188
actix_web::rt::spawn(async move {
190-
let server_timing_for_context = req_param.server_timing.borrow().clone();
191189
let request_context = RequestContext {
192190
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
193191
source_path,
194192
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
195-
server_timing: server_timing_for_context,
193+
server_timing: Arc::clone(&req_param.server_timing),
196194
};
197195
let mut conn = None;
198196
let database_entries_stream =
@@ -286,8 +284,7 @@ async fn process_sql_request(
286284
sql_path: PathBuf,
287285
) -> actix_web::Result<HttpResponse> {
288286
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
289-
let mut server_timing = ServerTiming::new(!app_state.config.environment.is_prod());
290-
server_timing.record("request");
287+
let server_timing = ServerTiming::for_env(app_state.config.environment);
291288

292289
let sql_file = app_state
293290
.sql_file_cache

src/webserver/http_request_info.rs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::webserver::server_timing::ServerTiming;
12
use crate::AppState;
23
use actix_multipart::form::bytes::Bytes;
34
use actix_multipart::form::tempfile::TempFile;
@@ -42,7 +43,7 @@ pub struct RequestInfo {
4243
pub clone_depth: u8,
4344
pub raw_body: Option<Vec<u8>>,
4445
pub oidc_claims: Option<OidcClaims>,
45-
pub server_timing: std::cell::RefCell<super::server_timing::ServerTiming>,
46+
pub server_timing: Arc<super::server_timing::ServerTiming>,
4647
}
4748

4849
impl RequestInfo {
@@ -63,7 +64,7 @@ impl RequestInfo {
6364
clone_depth: self.clone_depth + 1,
6465
raw_body: self.raw_body.clone(),
6566
oidc_claims: self.oidc_claims.clone(),
66-
server_timing: std::cell::RefCell::new(self.server_timing.borrow().clone()),
67+
server_timing: Arc::clone(&self.server_timing),
6768
}
6869
}
6970
}
@@ -80,7 +81,7 @@ impl Clone for RequestInfo {
8081
pub(crate) async fn extract_request_info(
8182
req: &mut ServiceRequest,
8283
app_state: Arc<AppState>,
83-
server_timing: super::server_timing::ServerTiming,
84+
server_timing: ServerTiming,
8485
) -> anyhow::Result<RequestInfo> {
8586
let (http_req, payload) = req.parts_mut();
8687
let method = http_req.method().clone();
@@ -126,7 +127,7 @@ pub(crate) async fn extract_request_info(
126127
clone_depth: 0,
127128
raw_body,
128129
oidc_claims,
129-
server_timing: std::cell::RefCell::new(server_timing),
130+
server_timing: Arc::new(server_timing),
130131
})
131132
}
132133

@@ -279,7 +280,7 @@ async fn is_file_field_empty(
279280
mod test {
280281
use super::super::http::SingleOrVec;
281282
use super::*;
282-
use crate::app_config::AppConfig;
283+
use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming};
283284
use actix_web::{http::header::ContentType, test::TestRequest};
284285

285286
#[actix_web::test]
@@ -288,7 +289,7 @@ mod test {
288289
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
289290
let mut service_request = TestRequest::default().to_srv_request();
290291
let app_data = Arc::new(AppState::init(&config).await.unwrap());
291-
let server_timing = super::server_timing::ServerTiming::new(false);
292+
let server_timing = ServerTiming::default();
292293
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
293294
.await
294295
.unwrap();
@@ -307,7 +308,7 @@ mod test {
307308
.set_payload("my_array[]=3&my_array[]=Hello%20World&repeated=1&repeated=2")
308309
.to_srv_request();
309310
let app_data = Arc::new(AppState::init(&config).await.unwrap());
310-
let server_timing = super::server_timing::ServerTiming::new(false);
311+
let server_timing = ServerTiming::default();
311312
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
312313
.await
313314
.unwrap();
@@ -357,7 +358,7 @@ mod test {
357358
)
358359
.to_srv_request();
359360
let app_data = Arc::new(AppState::init(&config).await.unwrap());
360-
let server_timing = super::server_timing::ServerTiming::new(false);
361+
let server_timing = ServerTiming::enabled(false);
361362
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
362363
.await
363364
.unwrap();

src/webserver/server_timing.rs

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,70 @@
11
use std::fmt::Write;
2+
use std::sync::Mutex;
23
use std::time::Instant;
34

4-
#[derive(Debug, Clone)]
5+
use crate::app_config::DevOrProd;
6+
7+
#[derive(Debug)]
58
pub struct ServerTiming {
69
enabled: bool,
7-
previous_event: Instant,
8-
header: String,
10+
created_at: Instant,
11+
events: Mutex<Vec<PerfEvent>>,
912
}
1013

11-
impl ServerTiming {
12-
#[must_use]
13-
pub fn new(enabled: bool) -> Self {
14+
#[derive(Debug)]
15+
struct PerfEvent {
16+
time: Instant,
17+
name: &'static str,
18+
}
19+
20+
impl Default for ServerTiming {
21+
fn default() -> Self {
1422
Self {
15-
enabled,
16-
previous_event: Instant::now(),
17-
header: String::new(),
23+
enabled: false,
24+
created_at: Instant::now(),
25+
events: Mutex::new(Vec::new()),
1826
}
1927
}
28+
}
2029

21-
pub fn record(&mut self, name: &'static str) {
22-
if !self.enabled {
23-
return;
24-
}
25-
let now = Instant::now();
26-
let duration_ms = (now - self.previous_event).as_secs_f64() * 1000.0;
27-
self.previous_event = now;
28-
29-
if !self.header.is_empty() {
30-
self.header.push_str(", ");
30+
impl ServerTiming {
31+
#[must_use]
32+
pub fn enabled(enabled: bool) -> Self {
33+
Self {
34+
enabled,
35+
..Default::default()
3136
}
32-
write!(&mut self.header, "{name};dur={duration_ms:.2}").unwrap();
3337
}
3438

3539
#[must_use]
36-
pub fn is_enabled(&self) -> bool {
37-
self.enabled
40+
pub fn for_env(env: DevOrProd) -> Self {
41+
Self::enabled(!env.is_prod())
3842
}
3943

40-
#[must_use]
41-
pub fn as_header_value(&self) -> &str {
44+
pub fn record(&self, name: &'static str) {
4245
if self.enabled {
43-
&self.header
44-
} else {
45-
""
46+
self.events.lock().unwrap().push(PerfEvent {
47+
time: Instant::now(),
48+
name,
49+
});
4650
}
4751
}
48-
}
4952

50-
impl Default for ServerTiming {
51-
fn default() -> Self {
52-
Self::new(false)
53+
pub fn header_value(&self) -> Option<String> {
54+
if !self.enabled {
55+
return None;
56+
}
57+
let evts = self.events.lock().unwrap();
58+
let mut s = String::with_capacity(evts.len() * 16);
59+
let mut last = self.created_at;
60+
for (i, PerfEvent { name, time }) in evts.iter().enumerate() {
61+
if i > 0 {
62+
s.push_str(", ");
63+
}
64+
let millis = time.saturating_duration_since(last).as_millis();
65+
write!(&mut s, "{name};dur={millis}").ok()?;
66+
last = *time;
67+
}
68+
Some(s)
5369
}
5470
}

tests/server_timing/mod.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use actix_web::{http::StatusCode, test};
1+
use actix_web::http::StatusCode;
22
use sqlpage::webserver::http::main_handler;
33

44
use crate::common::{get_request_to, make_app_data_from_config, test_config};
@@ -32,7 +32,7 @@ async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> {
3232
let app_data = make_app_data_from_config(config).await;
3333

3434
let req = crate::common::get_request_to_with_data(
35-
"/tests/sql_test_files/it_works_simple.sql",
35+
"/tests/sql_test_files/it_works_sqrt.sql",
3636
app_data,
3737
)
3838
.await?
@@ -46,10 +46,6 @@ async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> {
4646
.expect("Server-Timing header should be present in development mode");
4747
let header_value = server_timing_header.to_str().unwrap();
4848

49-
assert!(
50-
header_value.contains("request;dur="),
51-
"Should contain request timing: {header_value}"
52-
);
5349
assert!(
5450
header_value.contains("sql_file;dur="),
5551
"Should contain sql_file timing: {header_value}"
@@ -72,7 +68,7 @@ async fn test_server_timing_enabled_in_development() -> actix_web::Result<()> {
7268

7369
#[actix_web::test]
7470
async fn test_server_timing_format() -> actix_web::Result<()> {
75-
let req = get_request_to("/tests/sql_test_files/it_works_simple.sql")
71+
let req = get_request_to("/tests/sql_test_files/it_works_sqrt.sql")
7672
.await?
7773
.to_srv_request();
7874
let resp = main_handler(req).await?;

0 commit comments

Comments
 (0)