Skip to content

Commit 502b740

Browse files
committed
Make query_json_api return a QueryApiJsonResult instead of a String, to allow chunked serialization in the caller.
This lets us overlap the JSON serialization with the compression of the HTTP response: https://share.firefox.dev/46AS9jZ
1 parent a206a11 commit 502b740

File tree

10 files changed

+156
-76
lines changed

10 files changed

+156
-76
lines changed

samply-api/src/asm/mod.rs

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,17 @@ use samply_symbols::{
55
object, CodeByteReadingError, CodeId, FileAndPathHelper, FileAndPathHelperError, LibraryInfo,
66
LookupAddress, SymbolManager,
77
};
8-
use serde_json::json;
98
use yaxpeax_arch::{Arch, DecodeError, LengthedInstruction, Reader, U8Reader};
109
use yaxpeax_x86::amd64::{Opcode, Operand};
1110

1211
use self::response_json::Response;
1312
use crate::asm::response_json::DecodedInstruction;
1413

15-
mod request_json;
16-
mod response_json;
14+
pub mod request_json;
15+
pub mod response_json;
1716

1817
#[derive(thiserror::Error, Debug)]
19-
enum AsmError {
20-
#[error("Couldn't parse request: {0}")]
21-
ParseRequestErrorSerde(#[from] serde_json::error::Error),
22-
18+
pub enum AsmError {
2319
#[error("An error occurred when loading the binary: {0}")]
2420
LoadBinaryError(#[from] samply_symbols::Error),
2521

@@ -49,20 +45,15 @@ impl<'a, H: FileAndPathHelper> AsmApi<'a, H> {
4945
Self { symbol_manager }
5046
}
5147

52-
pub async fn query_api_json(&self, request_json: &str) -> String {
53-
match self.query_api_fallible_json(request_json).await {
54-
Ok(response_json) => response_json,
55-
Err(err) => json!({ "error": err.to_string() }).to_string(),
56-
}
57-
}
58-
59-
async fn query_api_fallible_json(&self, request_json: &str) -> Result<String, AsmError> {
48+
pub async fn query_api_json(
49+
&self,
50+
request_json: &str,
51+
) -> Result<response_json::Response, crate::Error> {
6052
let request: request_json::Request = serde_json::from_str(request_json)?;
61-
let response = self.query_api(&request).await?;
62-
Ok(serde_json::to_string(&response)?)
53+
Ok(self.query_api(&request).await?)
6354
}
6455

65-
async fn query_api(
56+
pub async fn query_api(
6657
&self,
6758
request: &request_json::Request,
6859
) -> Result<response_json::Response, AsmError> {

samply-api/src/error.rs

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,44 @@
1+
use serde::Serialize;
12
use thiserror::Error;
23

34
#[derive(Error, Debug)]
45
pub enum Error {
6+
#[error("Unrecognized URL: {0}")]
7+
UnrecognizedUrl(String),
8+
9+
#[error("Couldn't parse request: {0}")]
10+
ParseRequestErrorSerde(#[from] serde_json::error::Error),
11+
12+
#[error("Malformed request JSON: {0}")]
13+
ParseRequestErrorContents(&'static str),
14+
515
#[error("{0}")]
616
Symbols(
717
#[from]
818
#[source]
919
samply_symbols::Error,
1020
),
1121

12-
#[error("Couldn't parse request: {0}")]
13-
ParseRequestErrorSerde(#[from] serde_json::error::Error),
22+
#[error("{0}")]
23+
Source(
24+
#[from]
25+
#[source]
26+
super::source::SourceError,
27+
),
1428

15-
#[error("Malformed request JSON: {0}")]
16-
ParseRequestErrorContents(&'static str),
29+
#[error("{0}")]
30+
Asm(
31+
#[from]
32+
#[source]
33+
super::asm::AsmError,
34+
),
35+
}
36+
37+
impl Serialize for Error {
38+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
39+
where
40+
S: serde::Serializer,
41+
{
42+
self.to_string().serialize(serializer)
43+
}
1744
}

samply-api/src/lib.rs

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
//! let symbol_manager = SymbolManager::with_helper(helper);
2929
//! let api = samply_api::Api::new(&symbol_manager);
3030
//!
31-
//! api.query_api(
31+
//! let api_result = api.query_api(
3232
//! "/symbolicate/v5",
3333
//! r#"{
3434
//! "memoryMap": [
@@ -46,7 +46,8 @@
4646
//! ]
4747
//! ]
4848
//! }"#,
49-
//! ).await
49+
//! ).await;
50+
//! serde_json::to_string(&api_result).unwrap()
5051
//! }
5152
//!
5253
//! struct ExampleHelper {
@@ -147,7 +148,6 @@ use debugid::DebugId;
147148
pub use samply_symbols;
148149
pub use samply_symbols::debugid;
149150
use samply_symbols::{FileAndPathHelper, SymbolManager};
150-
use serde_json::json;
151151
use source::SourceApi;
152152
use symbolicate::SymbolicateApi;
153153

@@ -158,6 +158,8 @@ mod hex;
158158
mod source;
159159
mod symbolicate;
160160

161+
pub use error::Error;
162+
161163
pub(crate) fn to_debug_id(breakpad_id: &str) -> Result<DebugId, samply_symbols::Error> {
162164
// Only accept breakpad IDs with the right syntax, and which aren't all-zeros.
163165
match DebugId::from_breakpad(breakpad_id) {
@@ -168,6 +170,42 @@ pub(crate) fn to_debug_id(breakpad_id: &str) -> Result<DebugId, samply_symbols::
168170
}
169171
}
170172

173+
#[derive(serde_derive::Serialize)]
174+
pub struct QueryApiJsonResult(QueryApiJsonResultInner);
175+
176+
#[derive(serde_derive::Serialize)]
177+
#[serde(untagged)]
178+
enum QueryApiJsonResultInner {
179+
SymbolicateResponse(symbolicate::response_json::Response),
180+
SourceResponse(source::response_json::Response),
181+
AsmResponse(asm::response_json::Response),
182+
Err(Error),
183+
}
184+
185+
impl From<symbolicate::response_json::Response> for QueryApiJsonResult {
186+
fn from(value: symbolicate::response_json::Response) -> Self {
187+
Self(QueryApiJsonResultInner::SymbolicateResponse(value))
188+
}
189+
}
190+
191+
impl From<source::response_json::Response> for QueryApiJsonResult {
192+
fn from(value: source::response_json::Response) -> Self {
193+
Self(QueryApiJsonResultInner::SourceResponse(value))
194+
}
195+
}
196+
197+
impl From<asm::response_json::Response> for QueryApiJsonResult {
198+
fn from(value: asm::response_json::Response) -> Self {
199+
Self(QueryApiJsonResultInner::AsmResponse(value))
200+
}
201+
}
202+
203+
impl From<Error> for QueryApiJsonResult {
204+
fn from(value: Error) -> Self {
205+
Self(QueryApiJsonResultInner::Err(value))
206+
}
207+
}
208+
171209
#[derive(Clone, Copy)]
172210
pub struct Api<'a, H: FileAndPathHelper> {
173211
symbol_manager: &'a SymbolManager<H>,
@@ -194,18 +232,30 @@ impl<'a, H: FileAndPathHelper> Api<'a, H> {
194232
/// symbol information for that address.
195233
/// - `/asm/v1`: Experimental API. Symbolicates an address and lets you read one of the files in the
196234
/// symbol information for that address.
197-
pub async fn query_api(self, request_url: &str, request_json_data: &str) -> String {
235+
pub async fn query_api(self, request_url: &str, request_json_data: &str) -> QueryApiJsonResult {
236+
self.query_api_fallible(request_url, request_json_data)
237+
.await
238+
.unwrap_or_else(|e| e.into())
239+
}
240+
pub async fn query_api_fallible(
241+
self,
242+
request_url: &str,
243+
request_json_data: &str,
244+
) -> Result<QueryApiJsonResult, Error> {
198245
if request_url == "/symbolicate/v5" {
199246
let symbolicate_api = SymbolicateApi::new(self.symbol_manager);
200-
symbolicate_api.query_api_json(request_json_data).await
247+
Ok(symbolicate_api
248+
.query_api_json(request_json_data)
249+
.await?
250+
.into())
201251
} else if request_url == "/source/v1" {
202252
let source_api = SourceApi::new(self.symbol_manager);
203-
source_api.query_api_json(request_json_data).await
253+
Ok(source_api.query_api_json(request_json_data).await?.into())
204254
} else if request_url == "/asm/v1" {
205255
let asm_api = AsmApi::new(self.symbol_manager);
206-
asm_api.query_api_json(request_json_data).await
256+
Ok(asm_api.query_api_json(request_json_data).await?.into())
207257
} else {
208-
json!({ "error": format!("Unrecognized URL {request_url}") }).to_string()
258+
Err(Error::UnrecognizedUrl(request_url.into()))
209259
}
210260
}
211261
}

samply-api/src/source/mod.rs

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
use samply_symbols::{
22
FileAndPathHelper, FileAndPathHelperError, LibraryInfo, LookupAddress, SymbolManager,
33
};
4-
use serde_json::json;
54

65
use crate::api_file_path::to_api_file_path;
7-
use crate::to_debug_id;
6+
use crate::{to_debug_id, Error};
87

9-
mod request_json;
10-
mod response_json;
8+
pub mod request_json;
9+
pub mod response_json;
1110

1211
#[derive(thiserror::Error, Debug)]
13-
enum SourceError {
14-
#[error("Couldn't parse request: {0}")]
15-
ParseRequestErrorSerde(#[from] serde_json::error::Error),
16-
12+
pub enum SourceError {
1713
#[error("Could not obtain symbols for the requested library: {0}")]
1814
NoSymbols(#[from] samply_symbols::Error),
1915

@@ -37,20 +33,15 @@ impl<'a, H: FileAndPathHelper> SourceApi<'a, H> {
3733
Self { symbol_manager }
3834
}
3935

40-
pub async fn query_api_json(&self, request_json: &str) -> String {
41-
match self.query_api_fallible_json(request_json).await {
42-
Ok(response_json) => response_json,
43-
Err(err) => json!({ "error": err.to_string() }).to_string(),
44-
}
45-
}
46-
47-
async fn query_api_fallible_json(&self, request_json: &str) -> Result<String, SourceError> {
36+
pub async fn query_api_json(
37+
&self,
38+
request_json: &str,
39+
) -> Result<response_json::Response, Error> {
4840
let request: request_json::Request = serde_json::from_str(request_json)?;
49-
let response = self.query_api(&request).await?;
50-
Ok(serde_json::to_string(&response)?)
41+
Ok(self.query_api(&request).await?)
5142
}
5243

53-
async fn query_api(
44+
pub async fn query_api(
5445
&self,
5546
request: &request_json::Request,
5647
) -> Result<response_json::Response, SourceError> {

samply-api/src/symbolicate/mod.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ pub mod response_json;
1313

1414
use looked_up_addresses::LookedUpAddresses;
1515
use request_json::Lib;
16-
use serde_json::json;
1716

1817
pub struct SymbolicateApi<'a, H: FileAndPathHelper> {
1918
symbol_manager: &'a SymbolManager<H>,
@@ -25,17 +24,12 @@ impl<'a, H: FileAndPathHelper> SymbolicateApi<'a, H> {
2524
Self { symbol_manager }
2625
}
2726

28-
pub async fn query_api_json(&self, request_json: &str) -> String {
29-
match self.query_api_fallible_json(request_json).await {
30-
Ok(response_json) => response_json,
31-
Err(err) => json!({ "error": err.to_string() }).to_string(),
32-
}
33-
}
34-
35-
pub async fn query_api_fallible_json(&self, request_json: &str) -> Result<String, Error> {
27+
pub async fn query_api_json(
28+
&self,
29+
request_json: &str,
30+
) -> Result<response_json::Response, Error> {
3631
let request: request_json::Request = serde_json::from_str(request_json)?;
37-
let response = self.query_api(request).await?;
38-
Ok(serde_json::to_string(&response)?)
32+
self.query_api(request).await
3933
}
4034

4135
pub async fn query_api(

samply-api/tests/integration_tests/main.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,23 @@ use std::path::PathBuf;
44

55
use assert_json_diff::assert_json_eq;
66
pub use samply_api::debugid::DebugId;
7-
use samply_api::{samply_symbols, Api};
7+
use samply_api::{samply_symbols, Api, QueryApiJsonResult};
88
use samply_symbols::{
99
CandidatePathInfo, FileAndPathHelper, FileAndPathHelperResult, FileLocation, LibraryInfo,
1010
OptionallySendFuture, SymbolManager,
1111
};
1212

13-
pub async fn query_api(request_url: &str, request_json: &str, symbol_directory: PathBuf) -> String {
13+
pub async fn query_api(
14+
request_url: &str,
15+
request_json: &str,
16+
symbol_directory: PathBuf,
17+
) -> QueryApiJsonResult {
1418
let helper = Helper { symbol_directory };
1519
let symbol_manager = SymbolManager::with_helper(helper);
1620
let api = Api::new(&symbol_manager);
1721
api.query_api(request_url, request_json).await
1822
}
23+
1924
struct Helper {
2025
symbol_directory: PathBuf,
2126
}
@@ -229,6 +234,7 @@ fn compare_snapshot(
229234
request_json,
230235
symbol_directory,
231236
));
237+
let output = serde_json::to_string(&output).unwrap();
232238

233239
let output_json: serde_json::Value = serde_json::from_str(&output).unwrap();
234240

samply/src/server.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::collections::HashMap;
2+
use std::convert::Infallible;
23
use std::ffi::OsStr;
4+
use std::io::BufWriter;
35
use std::net::{IpAddr, SocketAddr};
46
use std::ops::Range;
57
use std::path::{Path, PathBuf};
@@ -8,7 +10,7 @@ use std::sync::Arc;
810

911
use futures_util::TryStreamExt;
1012
use http_body_util::combinators::BoxBody;
11-
use http_body_util::{BodyExt, Either, StreamBody};
13+
use http_body_util::{BodyExt, Either, Full, StreamBody};
1214
use hyper::body::{Bytes, Frame};
1315
use hyper::server::conn::http1;
1416
use hyper::service::service_fn;
@@ -240,13 +242,15 @@ async fn run_server(
240242
}
241243
}
242244

245+
type MyBody = Either<String, Either<BoxBody<Bytes, std::io::Error>, BoxBody<Bytes, Infallible>>>;
246+
243247
async fn symbolication_service(
244248
req: Request<hyper::body::Incoming>,
245249
template_values: Arc<HashMap<&'static str, String>>,
246250
symbol_manager: Arc<SymbolManager>,
247251
profile_filename: Option<PathBuf>,
248252
path_prefix: String,
249-
) -> Result<Response<Either<String, BoxBody<Bytes, std::io::Error>>>, hyper::Error> {
253+
) -> Result<Response<MyBody>, hyper::Error> {
250254
let has_profile = profile_filename.is_some();
251255
let method = req.method();
252256
let path = req.uri().path();
@@ -339,7 +343,7 @@ async fn symbolication_service(
339343
let reader_stream = ReaderStream::new(reader);
340344

341345
let stream_body = StreamBody::new(reader_stream.map_ok(Frame::data));
342-
*response.body_mut() = Either::Right(stream_body.boxed());
346+
*response.body_mut() = Either::Right(Either::Left(stream_body.boxed()));
343347
}
344348
(&Method::POST, path, _) => {
345349
response.headers_mut().insert(
@@ -348,13 +352,17 @@ async fn symbolication_service(
348352
);
349353
let path = path.to_string();
350354
// Await the full body to be concatenated into a `Collected<Bytes>`.
351-
let full_body = req.into_body().collect().await?;
355+
let request_body = req.into_body().collect().await?;
352356
// Convert the `Collected<Bytes>` into a `String`.
353-
let full_body =
354-
String::from_utf8(full_body.to_bytes().to_vec()).expect("invalid utf-8");
355-
let response_json = symbol_manager.query_json_api(&path, &full_body).await;
356-
357-
*response.body_mut() = Either::Left(response_json);
357+
let request_body =
358+
String::from_utf8(request_body.to_bytes().to_vec()).expect("invalid utf-8");
359+
let response_json = symbol_manager.query_json_api(&path, &request_body).await;
360+
let mut response_bytes = Vec::new();
361+
let response_writer = BufWriter::new(&mut response_bytes);
362+
serde_json::to_writer(response_writer, &response_json).expect("json writing error");
363+
let response_body = Full::new(Bytes::from(response_bytes));
364+
365+
*response.body_mut() = Either::Right(Either::Right(response_body.boxed()));
358366
}
359367
_ => {
360368
*response.status_mut() = StatusCode::NOT_FOUND;

0 commit comments

Comments
 (0)