Skip to content

Commit 44afd16

Browse files
committed
Merge branch 'cursor/add-server-timing-headers-in-development-ee50'
2 parents 8690e4d + 06df761 commit 44afd16

File tree

8 files changed

+216
-13
lines changed

8 files changed

+216
-13
lines changed

src/render.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ pub enum PageContext {
8080
/// Handles the first SQL statements, before the headers have been sent to
8181
pub struct HeaderContext {
8282
app_state: Arc<AppState>,
83-
request_context: RequestContext,
83+
pub request_context: RequestContext,
8484
pub writer: ResponseWriter,
8585
response: HttpResponseBuilder,
8686
has_status: bool,
@@ -368,7 +368,14 @@ impl HeaderContext {
368368
Ok(PageContext::Header(self))
369369
}
370370

371-
async fn start_body(self, data: JsonValue) -> anyhow::Result<PageContext> {
371+
fn add_server_timing_header(&mut self) {
372+
if let Some(header_value) = self.request_context.server_timing.header_value() {
373+
self.response.insert_header(("Server-Timing", header_value));
374+
}
375+
}
376+
377+
async fn start_body(mut self, data: JsonValue) -> anyhow::Result<PageContext> {
378+
self.add_server_timing_header();
372379
let html_renderer =
373380
HtmlRenderContext::new(self.app_state, self.request_context, self.writer, data)
374381
.await
@@ -382,6 +389,7 @@ impl HeaderContext {
382389
}
383390

384391
pub fn close(mut self) -> HttpResponse {
392+
self.add_server_timing_header();
385393
self.response.finish()
386394
}
387395
}

src/webserver/database/execute_queries.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,13 @@ pub fn stream_query_results_with_conn<'a>(
5252
for res in &sql_file.statements {
5353
match res {
5454
ParsedStatement::CsvImport(csv_import) => {
55-
let connection = take_connection(&request.app_state.db, db_connection).await?;
55+
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
5656
log::debug!("Executing CSV import: {csv_import:?}");
5757
run_csv_import(connection, csv_import, request).await.with_context(|| format!("Failed to import the CSV file {:?} into the table {:?}", csv_import.uploaded_file, csv_import.table_name))?;
5858
},
5959
ParsedStatement::StmtWithParams(stmt) => {
6060
let query = bind_parameters(stmt, request, db_connection).await?;
61-
let connection = take_connection(&request.app_state.db, db_connection).await?;
61+
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
6262
log::trace!("Executing query {:?}", query.sql);
6363
let mut stream = connection.fetch_many(query);
6464
let mut error = None;
@@ -192,7 +192,7 @@ async fn execute_set_variable_query<'a>(
192192
source_file: &Path,
193193
) -> anyhow::Result<()> {
194194
let query = bind_parameters(statement, request, db_connection).await?;
195-
let connection = take_connection(&request.app_state.db, db_connection).await?;
195+
let connection = take_connection(&request.app_state.db, db_connection, request).await?;
196196
log::debug!(
197197
"Executing query to set the {variable:?} variable: {:?}",
198198
query.sql
@@ -276,13 +276,15 @@ fn vars_and_name<'a, 'b>(
276276
async fn take_connection<'a>(
277277
db: &'a Database,
278278
conn: &'a mut DbConn,
279+
request: &RequestInfo,
279280
) -> anyhow::Result<&'a mut PoolConnection<sqlx::Any>> {
280281
if let Some(c) = conn {
281282
return Ok(c);
282283
}
283284
match db.connection.acquire().await {
284285
Ok(c) => {
285286
log::debug!("Acquired a database connection");
287+
request.server_timing.record("db_conn");
286288
*conn = Some(c);
287289
Ok(conn.as_mut().unwrap())
288290
}

src/webserver/http.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::webserver::content_security_policy::ContentSecurityPolicy;
77
use crate::webserver::database::execute_queries::stop_at_first_error;
88
use crate::webserver::database::{execute_queries::stream_query_results_with_conn, DbItem};
99
use crate::webserver::http_request_info::extract_request_info;
10+
use crate::webserver::server_timing::ServerTiming;
1011
use crate::webserver::ErrorWithStatus;
1112
use crate::{AppConfig, AppState, ParsedSqlFile, DEFAULT_404_FILE};
1213
use actix_web::dev::{fn_service, ServiceFactory, ServiceRequest};
@@ -46,6 +47,7 @@ pub struct RequestContext {
4647
pub is_embedded: bool,
4748
pub source_path: PathBuf,
4849
pub content_security_policy: ContentSecurityPolicy,
50+
pub server_timing: Arc<ServerTiming>,
4951
}
5052

5153
async fn stream_response(stream: impl Stream<Item = DbItem>, mut renderer: AnyRenderBodyContext) {
@@ -106,7 +108,10 @@ async fn build_response_header_and_stream<S: Stream<Item = DbItem>>(
106108
let mut stream = Box::pin(database_entries);
107109
while let Some(item) = stream.next().await {
108110
let page_context = match item {
109-
DbItem::Row(data) => head_context.handle_row(data).await?,
111+
DbItem::Row(data) => {
112+
head_context.request_context.server_timing.record("row");
113+
head_context.handle_row(data).await?
114+
}
110115
DbItem::FinishedQuery => {
111116
log::debug!("finished query");
112117
continue;
@@ -163,25 +168,29 @@ enum ResponseWithWriter<S> {
163168
async fn render_sql(
164169
srv_req: &mut ServiceRequest,
165170
sql_file: Arc<ParsedSqlFile>,
171+
server_timing: ServerTiming,
166172
) -> actix_web::Result<HttpResponse> {
167173
let app_state = srv_req
168174
.app_data::<web::Data<AppState>>()
169175
.ok_or_else(|| ErrorInternalServerError("no state"))?
170-
.clone() // Cheap reference count increase
176+
.clone()
171177
.into_inner();
172178

173-
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state))
179+
let mut req_param = extract_request_info(srv_req, Arc::clone(&app_state), server_timing)
174180
.await
175181
.map_err(|e| anyhow_err_to_actix(e, &app_state))?;
176182
log::debug!("Received a request with the following parameters: {req_param:?}");
177183

184+
req_param.server_timing.record("parse_req");
185+
178186
let (resp_send, resp_recv) = tokio::sync::oneshot::channel::<HttpResponse>();
179187
let source_path: PathBuf = sql_file.source_path.clone();
180188
actix_web::rt::spawn(async move {
181189
let request_context = RequestContext {
182190
is_embedded: req_param.get_variables.contains_key("_sqlpage_embed"),
183191
source_path,
184192
content_security_policy: ContentSecurityPolicy::with_random_nonce(),
193+
server_timing: Arc::clone(&req_param.server_timing),
185194
};
186195
let mut conn = None;
187196
let database_entries_stream =
@@ -275,13 +284,17 @@ async fn process_sql_request(
275284
sql_path: PathBuf,
276285
) -> actix_web::Result<HttpResponse> {
277286
let app_state: &web::Data<AppState> = req.app_data().expect("app_state");
287+
let server_timing = ServerTiming::for_env(app_state.config.environment);
288+
278289
let sql_file = app_state
279290
.sql_file_cache
280291
.get_with_privilege(app_state, &sql_path, false)
281292
.await
282293
.with_context(|| format!("Unable to read SQL file \"{}\"", sql_path.display()))
283294
.map_err(|e| anyhow_err_to_actix(e, app_state))?;
284-
render_sql(req, sql_file).await
295+
server_timing.record("sql_file");
296+
297+
render_sql(req, sql_file, server_timing).await
285298
}
286299

287300
async fn serve_file(

src/webserver/http_request_info.rs

Lines changed: 12 additions & 4 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,6 +43,7 @@ pub struct RequestInfo {
4243
pub clone_depth: u8,
4344
pub raw_body: Option<Vec<u8>>,
4445
pub oidc_claims: Option<OidcClaims>,
46+
pub server_timing: Arc<super::server_timing::ServerTiming>,
4547
}
4648

4749
impl RequestInfo {
@@ -62,6 +64,7 @@ impl RequestInfo {
6264
clone_depth: self.clone_depth + 1,
6365
raw_body: self.raw_body.clone(),
6466
oidc_claims: self.oidc_claims.clone(),
67+
server_timing: Arc::clone(&self.server_timing),
6568
}
6669
}
6770
}
@@ -78,6 +81,7 @@ impl Clone for RequestInfo {
7881
pub(crate) async fn extract_request_info(
7982
req: &mut ServiceRequest,
8083
app_state: Arc<AppState>,
84+
server_timing: ServerTiming,
8185
) -> anyhow::Result<RequestInfo> {
8286
let (http_req, payload) = req.parts_mut();
8387
let method = http_req.method().clone();
@@ -123,6 +127,7 @@ pub(crate) async fn extract_request_info(
123127
clone_depth: 0,
124128
raw_body,
125129
oidc_claims,
130+
server_timing: Arc::new(server_timing),
126131
})
127132
}
128133

@@ -275,7 +280,7 @@ async fn is_file_field_empty(
275280
mod test {
276281
use super::super::http::SingleOrVec;
277282
use super::*;
278-
use crate::app_config::AppConfig;
283+
use crate::{app_config::AppConfig, webserver::server_timing::ServerTiming};
279284
use actix_web::{http::header::ContentType, test::TestRequest};
280285

281286
#[actix_web::test]
@@ -284,7 +289,8 @@ mod test {
284289
serde_json::from_str::<AppConfig>(r#"{"listen_on": "localhost:1234"}"#).unwrap();
285290
let mut service_request = TestRequest::default().to_srv_request();
286291
let app_data = Arc::new(AppState::init(&config).await.unwrap());
287-
let request_info = extract_request_info(&mut service_request, app_data)
292+
let server_timing = ServerTiming::default();
293+
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
288294
.await
289295
.unwrap();
290296
assert_eq!(request_info.post_variables.len(), 0);
@@ -302,7 +308,8 @@ mod test {
302308
.set_payload("my_array[]=3&my_array[]=Hello%20World&repeated=1&repeated=2")
303309
.to_srv_request();
304310
let app_data = Arc::new(AppState::init(&config).await.unwrap());
305-
let request_info = extract_request_info(&mut service_request, app_data)
311+
let server_timing = ServerTiming::default();
312+
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
306313
.await
307314
.unwrap();
308315
assert_eq!(
@@ -351,7 +358,8 @@ mod test {
351358
)
352359
.to_srv_request();
353360
let app_data = Arc::new(AppState::init(&config).await.unwrap());
354-
let request_info = extract_request_info(&mut service_request, app_data)
361+
let server_timing = ServerTiming::enabled(false);
362+
let request_info = extract_request_info(&mut service_request, app_data, server_timing)
355363
.await
356364
.unwrap();
357365
assert_eq!(

src/webserver/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ pub mod http_client;
3838
pub mod http_request_info;
3939
mod https;
4040
pub mod request_variables;
41+
pub mod server_timing;
4142

4243
pub use database::Database;
4344
pub use error_with_status::ErrorWithStatus;

src/webserver/server_timing.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use std::fmt::Write;
2+
use std::sync::Mutex;
3+
use std::time::Instant;
4+
5+
use crate::app_config::DevOrProd;
6+
7+
#[derive(Debug)]
8+
pub struct ServerTiming {
9+
enabled: bool,
10+
created_at: Instant,
11+
events: Mutex<Vec<PerfEvent>>,
12+
}
13+
14+
#[derive(Debug)]
15+
struct PerfEvent {
16+
time: Instant,
17+
name: &'static str,
18+
}
19+
20+
impl Default for ServerTiming {
21+
fn default() -> Self {
22+
Self {
23+
enabled: false,
24+
created_at: Instant::now(),
25+
events: Mutex::new(Vec::new()),
26+
}
27+
}
28+
}
29+
30+
impl ServerTiming {
31+
#[must_use]
32+
pub fn enabled(enabled: bool) -> Self {
33+
Self {
34+
enabled,
35+
..Default::default()
36+
}
37+
}
38+
39+
#[must_use]
40+
pub fn for_env(env: DevOrProd) -> Self {
41+
Self::enabled(!env.is_prod())
42+
}
43+
44+
pub fn record(&self, name: &'static str) {
45+
if self.enabled {
46+
self.events.lock().unwrap().push(PerfEvent {
47+
time: Instant::now(),
48+
name,
49+
});
50+
}
51+
}
52+
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)
69+
}
70+
}

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;

0 commit comments

Comments
 (0)