Reduce app-server JSON serialization overhead (#34761)

## What changed

- Decode `JSONRPCRequest` into `ClientRequest` without serializing the entire request back into an intermediate JSON value.
- Define every client method's wire name explicitly and use it directly for request, response, and tracing method lookups.
- Serialize outgoing transport messages directly to JSON strings instead of first building a `serde_json::Value`.

## Testing

- Add regression coverage that compares direct request conversion with Serde decoding, including omitted and null parameters, legacy methods, and unknown methods.
- Verify that direct outgoing-message serialization preserves the notification wire shape.

GitOrigin-RevId: 304e14bcaea24e32743bf852a60a208e2f273db3
This commit is contained in:
jif
2026-07-22 14:16:51 +00:00
committed by copyberry
parent 65ae4c26e0
commit f69dc49b15
4 changed files with 148 additions and 47 deletions
@@ -200,7 +200,7 @@ macro_rules! client_request_definitions {
$(
$(#[experimental($reason:expr)])?
$(#[doc = $variant_doc:literal])*
$variant:ident $(=> $wire:literal)? {
$variant:ident => $wire:literal {
params: $(#[$params_meta:meta])* $params:ty,
$(inspect_params: $inspect_params:tt,)?
serialization: $serialization:ident $( ( $($serialization_args:tt)* ) )?,
@@ -215,7 +215,8 @@ macro_rules! client_request_definitions {
pub enum ClientRequest {
$(
$(#[doc = $variant_doc])*
$(#[serde(rename = $wire)] #[ts(rename = $wire)])?
#[serde(rename = $wire)]
#[ts(rename = $wire)]
$variant {
#[serde(rename = "id")]
request_id: RequestId,
@@ -232,16 +233,14 @@ macro_rules! client_request_definitions {
}
}
pub const fn method_name(&self) -> &'static str {
match self {
$(Self::$variant { .. } => $wire,)*
}
}
pub fn method(&self) -> String {
serde_json::to_value(self)
.ok()
.and_then(|value| {
value
.get("method")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
})
.unwrap_or_else(|| "<unknown>".to_string())
self.method_name().to_string()
}
pub fn serialization_scope(&self) -> Option<ClientRequestSerializationScope> {
@@ -258,6 +257,26 @@ macro_rules! client_request_definitions {
}
}
impl TryFrom<JSONRPCRequest> for ClientRequest {
type Error = serde_json::Error;
fn try_from(request: JSONRPCRequest) -> Result<Self, Self::Error> {
let JSONRPCRequest {
id: request_id,
method,
params,
trace: _,
} = request;
let mut request = serde_json::Map::new();
request.insert("id".to_string(), serde_json::to_value(request_id)?);
request.insert("method".to_string(), serde_json::Value::String(method));
if let Some(params) = params {
request.insert("params".to_string(), params);
}
serde_json::from_value(serde_json::Value::Object(request))
}
}
/// Typed response from the server to the client.
#[derive(Serialize, Deserialize, Debug, Clone)]
#[allow(clippy::large_enum_variant)]
@@ -265,7 +284,7 @@ macro_rules! client_request_definitions {
pub enum ClientResponse {
$(
$(#[doc = $variant_doc])*
$(#[serde(rename = $wire)])?
#[serde(rename = $wire)]
$variant {
#[serde(rename = "id")]
request_id: RequestId,
@@ -282,15 +301,9 @@ macro_rules! client_request_definitions {
}
pub fn method(&self) -> String {
serde_json::to_value(self)
.ok()
.and_then(|value| {
value
.get("method")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
})
.unwrap_or_else(|| "<unknown>".to_string())
match self {
$(Self::$variant { .. } => $wire.to_string(),)*
}
}
pub fn into_jsonrpc_parts(
@@ -405,7 +418,7 @@ macro_rules! client_request_definitions {
pub(crate) const EXPERIMENTAL_CLIENT_METHODS: &[&str] = &[
$(
experimental_method_entry!($(#[experimental($reason)])? $(=> $wire)?),
experimental_method_entry!($(#[experimental($reason)])? => $wire),
)*
];
pub(crate) const EXPERIMENTAL_CLIENT_METHOD_PARAM_TYPES: &[&str] = &[
@@ -470,7 +483,7 @@ macro_rules! client_response_payload_from_impl {
}
client_request_definitions! {
Initialize {
Initialize => "initialize" {
params: v1::InitializeParams,
serialization: None,
response: v1::InitializeResponse,
@@ -1177,25 +1190,25 @@ client_request_definitions! {
},
/// DEPRECATED APIs below
GetConversationSummary {
GetConversationSummary => "getConversationSummary" {
params: v1::GetConversationSummaryParams,
serialization: None,
response: v1::GetConversationSummaryResponse,
},
GitDiffToRemote {
GitDiffToRemote => "gitDiffToRemote" {
params: v1::GitDiffToRemoteParams,
serialization: None,
response: v1::GitDiffToRemoteResponse,
},
/// DEPRECATED in favor of GetAccount
GetAuthStatus {
GetAuthStatus => "getAuthStatus" {
params: v1::GetAuthStatusParams,
serialization: global("account-auth"),
response: v1::GetAuthStatusResponse,
},
// Legacy fuzzy search cancellation is intentionally concurrent: clients reuse a
// cancellation token so a newer request can cancel an older in-flight search.
FuzzyFileSearch {
FuzzyFileSearch => "fuzzyFileSearch" {
params: FuzzyFileSearchParams,
serialization: None,
response: FuzzyFileSearchResponse,
@@ -1798,6 +1811,81 @@ mod tests {
RequestId::Integer(REQUEST_ID)
}
fn decode_client_request_through_json(
request: &JSONRPCRequest,
) -> std::result::Result<ClientRequest, String> {
serde_json::to_value(request)
.and_then(serde_json::from_value)
.map_err(|err| err.to_string())
}
#[test]
fn jsonrpc_request_conversion_preserves_serde_enum_decoding() {
let requests = [
JSONRPCRequest {
id: RequestId::Integer(1),
method: "thread/archive".to_string(),
params: Some(json!({"threadId": "thread-1"})),
trace: Some(codex_protocol::protocol::W3cTraceContext {
traceparent: Some("traceparent".to_string()),
tracestate: Some("tracestate".to_string()),
}),
},
// Required params preserve distinct omitted and explicit-null errors.
JSONRPCRequest {
id: RequestId::Integer(2),
method: "thread/archive".to_string(),
params: None,
trace: None,
},
JSONRPCRequest {
id: RequestId::Integer(3),
method: "thread/archive".to_string(),
params: Some(serde_json::Value::Null),
trace: None,
},
// Optional unit params preserve omitted, null, and empty-object behavior.
JSONRPCRequest {
id: RequestId::Integer(4),
method: "memory/reset".to_string(),
params: None,
trace: None,
},
JSONRPCRequest {
id: RequestId::Integer(5),
method: "memory/reset".to_string(),
params: Some(serde_json::Value::Null),
trace: None,
},
JSONRPCRequest {
id: RequestId::Integer(6),
method: "memory/reset".to_string(),
params: Some(json!({})),
trace: None,
},
JSONRPCRequest {
id: RequestId::Integer(7),
method: "getConversationSummary".to_string(),
params: Some(json!({
"conversationId": "67e55044-10b1-426f-9247-bb680e5fe0c8"
})),
trace: None,
},
JSONRPCRequest {
id: RequestId::Integer(8),
method: "unknown/method".to_string(),
params: Some(json!({})),
trace: None,
},
];
for request in requests {
let expected = decode_client_request_through_json(&request);
let actual = ClientRequest::try_from(request).map_err(|err| err.to_string());
assert_eq!(actual, expected);
}
}
#[test]
fn client_request_serialization_scope_covers_keyed_families() {
let thread_id = "thread-1".to_string();