From 4ec3bb3aaa58ada54f1194c8bcf428410a678ba0 Mon Sep 17 00:00:00 2001 From: Weihang Lo Date: Fri, 17 Oct 2025 16:22:29 -0400 Subject: [PATCH] refactor: JSON message with less allocations This was found during experimenting `-Zbuild-analysis` with ndjson. From me tracing the code with `cargo expand`, basically there shouldn't have any significant performance difference between `serde(flatten)` and inlining all the fields. Here the differences between them * flatten one calls `Serialize::serialize_map` without fields size hint so cannot pre-allocate Vec with `Vec::with_capacity`, whereas inline case calls `Serialize::serialize_struct` with a known length of fields. * flatten would end up calling `Serializer::serialize_map` and line calls `Serializer::serialize_struct`. And in serde_json serializer `serialize_struct` actually call `serailze_map`. So no difference on serializer side. * There might be some function calls not inlined I like `FlatMapSerializer` but I doubt it is costly than allocation. Here is the `cargo-expand`'d result: ```rust #[derive(Serialize)] pub struct Foo { id: u8, #[serde(flatten)] data: D, } #[derive(Serialize)] struct Bar { a: bool, } // Expand to extern crate serde as _serde; impl _serde::Serialize for Foo where D: _serde::Serialize, { fn serialize<__S>( &self, __serializer: __S, ) -> _serde::__private228::Result<__S::Ok, __S::Error> where __S: _serde::Serializer, { let mut __serde_state = _serde::Serializer::serialize_map( __serializer, _serde::__private228::None, )?; _serde::ser::SerializeMap::serialize_entry( &mut __serde_state, "id", &self.id, )?; _serde::Serialize::serialize( &&self.data, _serde::__private228::ser::FlatMapSerializer(&mut __serde_state), )?; _serde::ser::SerializeMap::end(__serde_state) } } impl _serde::Serialize for Bar { fn serialize<__S>( &self, __serializer: __S, ) -> _serde::__private228::Result<__S::Ok, __S::Error> where __S: _serde::Serializer, { let mut __serde_state = _serde::Serializer::serialize_struct( __serializer, "Bar", false as usize + 1, )?; _serde::ser::SerializeStruct::serialize_field( &mut __serde_state, "a", &self.a, )?; _serde::ser::SerializeStruct::end(__serde_state) } } ``` ```rust #[derive(Serialize)] pub struct Foo { id: u8, a: bool, } // Expand to impl _serde::Serialize for Foo { fn serialize<__S>( &self, __serializer: __S, ) -> _serde::__private228::Result<__S::Ok, __S::Error> where __S: _serde::Serializer, { let mut __serde_state = _serde::Serializer::serialize_struct( __serializer, "Foo", false as usize + 1 + 1, )?; _serde::ser::SerializeStruct::serialize_field( &mut __serde_state, "id", &self.id, )?; _serde::ser::SerializeStruct::serialize_field( &mut __serde_state, "a", &self.a, )?; _serde::ser::SerializeStruct::end(__serde_state) } } ``` --- src/cargo/util/machine_message.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/cargo/util/machine_message.rs b/src/cargo/util/machine_message.rs index 64e64ee3bcc..0be2a4f1e33 100644 --- a/src/cargo/util/machine_message.rs +++ b/src/cargo/util/machine_message.rs @@ -3,7 +3,7 @@ use std::path::{Path, PathBuf}; use cargo_util_schemas::core::PackageIdSpec; use serde::Serialize; use serde::ser; -use serde_json::{json, value::RawValue}; +use serde_json::value::RawValue; use crate::core::Target; use crate::core::compiler::{CompilationSection, CompileMode}; @@ -12,10 +12,17 @@ pub trait Message: ser::Serialize { fn reason(&self) -> &str; fn to_json_string(&self) -> String { - let json = serde_json::to_string(self).unwrap(); - assert!(json.starts_with("{\"")); - let reason = json!(self.reason()); - format!("{{\"reason\":{},{}", reason, &json[1..]) + #[derive(Serialize)] + struct WithReason<'a, S: Serialize> { + reason: &'a str, + #[serde(flatten)] + msg: &'a S, + } + let with_reason = WithReason { + reason: self.reason(), + msg: &self, + }; + serde_json::to_string(&with_reason).unwrap() } }