From 5b8e551dc0ae5fa7631406c81a0ebce4b0f42581 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 26 Aug 2026 14:33:14 +0000 Subject: [PATCH] Reduce LLVM IR bloat (#63232) Cuts serde monomorphization in settings_content: both parse entry points now share one deserializer instantiation, and the flatten-heavy settings structs deserialize through a small manual routing macro instead of serde's Content buffering. Parsed values seem to be unchanged; JSON schema and serialization derives are untouched. `cargo llvm-lines -p settings_content`: Before (a90d3772f8): 2716889 lines, 92801 copies After: 1203744 lines (-55.7%), 47122 copies (-49.2%) With sccache disabled, `cargo clean` and `cargo build -p zed`: Before: 132.8s wall, 1440.6s CPU, zed binary 1241791992 bytes After: 128.8s wall (-3.0%), 1400.0s CPU (-2.8%), zed binary 1204044312 bytes (-3.0%, -37.7MB) Release Notes: - N/A --- Cargo.lock | 1 + crates/settings_content/Cargo.toml | 1 + .../settings_content/src/fallible_options.rs | 86 ++++++++++++++++- crates/settings_content/src/language.rs | 8 +- crates/settings_content/src/project.rs | 10 +- .../settings_content/src/settings_content.rs | 24 ++++- crates/settings_content/src/terminal.rs | 14 ++- .../tests/flatten_collision_probe.rs | 96 +++++++++++++++++++ 8 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 crates/settings_content/tests/flatten_collision_probe.rs diff --git a/Cargo.lock b/Cargo.lock index c695a0073c..eac23e1c23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16439,6 +16439,7 @@ dependencies = [ "serde", "serde_json", "serde_json_lenient", + "serde_path_to_error", "settings_json", "settings_macros", "strum 0.28.0", diff --git a/crates/settings_content/Cargo.toml b/crates/settings_content/Cargo.toml index 59cccb4167..621f18edf0 100644 --- a/crates/settings_content/Cargo.toml +++ b/crates/settings_content/Cargo.toml @@ -25,6 +25,7 @@ schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_json_lenient.workspace = true +serde_path_to_error.workspace = true settings_json.workspace = true settings_macros.workspace = true strum.workspace = true diff --git a/crates/settings_content/src/fallible_options.rs b/crates/settings_content/src/fallible_options.rs index 8ae164f6c2..91a8e183e0 100644 --- a/crates/settings_content/src/fallible_options.rs +++ b/crates/settings_content/src/fallible_options.rs @@ -17,14 +17,14 @@ where }); let mut deserializer = serde_json_lenient::Deserializer::from_str(json); - let value = T::deserialize(&mut deserializer); + let value = serde_path_to_error::deserialize::<_, T>(&mut deserializer); let value = match value { Ok(value) => value, Err(error) => { return ( None, ParseStatus::Failed { - error: error.to_string(), + error: error.into_inner().to_string(), }, ); } @@ -64,6 +64,88 @@ where pub trait FallibleOption: Default {} impl FallibleOption for Option {} +macro_rules! flattened_deserialize { + ($type_name:ty { + sections: { $($section:ident),* $(,)? }, + options: { $($option_field:ident),* $(,)? }, + defaults: { $($default_field:ident),* $(,)? } $(,)? + }) => { + impl $type_name { + #[doc(hidden)] + pub const NAMED_DESERIALIZE_KEYS: &'static [&'static str] = &[ + $(stringify!($option_field),)* + $(stringify!($default_field),)* + ]; + } + + impl<'de> serde::Deserialize<'de> for $type_name { + fn deserialize>( + deserializer: D, + ) -> Result { + let mut object = + serde_json::Map::::deserialize(deserializer)?; + (|| -> Result { + $( + let $option_field = + $crate::fallible_options::take_option_field( + &mut object, + stringify!($option_field), + )?; + )* + $( + let $default_field = + $crate::fallible_options::take_default_field( + &mut object, + stringify!($default_field), + )?; + )* + let rest = serde_json::Value::Object(object); + Ok(Self { + $($section: $crate::fallible_options::section(&rest)?,)* + $($option_field,)* + $($default_field,)* + }) + })() + .map_err(serde::de::Error::custom) + } + } + }; +} +pub(crate) use flattened_deserialize; + +pub(crate) fn take_option_field( + object: &mut serde_json::Map, + key: &str, +) -> Result +where + T: serde::de::DeserializeOwned + FallibleOption, +{ + match object.remove(key) { + None => Ok(T::default()), + Some(value) => deserialize(&value), + } +} + +pub(crate) fn take_default_field( + object: &mut serde_json::Map, + key: &str, +) -> Result +where + T: serde::de::DeserializeOwned + Default, +{ + match object.remove(key) { + None => Ok(T::default()), + Some(value) => T::deserialize(&value), + } +} + +pub(crate) fn section(rest: &serde_json::Value) -> Result +where + T: serde::de::DeserializeOwned, +{ + T::deserialize(rest) +} + #[cfg(test)] mod tests { use serde::Deserialize; diff --git a/crates/settings_content/src/language.rs b/crates/settings_content/src/language.rs index d4c8223417..e7e9e3a47f 100644 --- a/crates/settings_content/src/language.rs +++ b/crates/settings_content/src/language.rs @@ -34,7 +34,7 @@ pub struct ModifiersContent { } #[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, JsonSchema)] pub struct AllLanguageSettingsContent { /// The edit prediction settings. pub edit_predictions: Option, @@ -49,6 +49,12 @@ pub struct AllLanguageSettingsContent { pub file_types: Option, } +crate::fallible_options::flattened_deserialize!(AllLanguageSettingsContent { + sections: { defaults }, + options: { edit_predictions, file_types }, + defaults: { languages }, +}); + impl merge_from::MergeFrom for AllLanguageSettingsContent { fn merge_from(&mut self, other: &Self) { self.file_types.merge_from(&other.file_types); diff --git a/crates/settings_content/src/project.rs b/crates/settings_content/src/project.rs index 21aee523d3..713cab931e 100644 --- a/crates/settings_content/src/project.rs +++ b/crates/settings_content/src/project.rs @@ -40,7 +40,7 @@ impl RootUserSettings for ProjectSettingsContent { } #[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] +#[derive(Debug, PartialEq, Clone, Default, Serialize, JsonSchema, MergeFrom)] pub struct ProjectSettingsContent { #[serde(flatten)] pub all_languages: AllLanguageSettingsContent, @@ -87,6 +87,14 @@ pub struct ProjectSettingsContent { pub disable_ai: Option, } +crate::fallible_options::flattened_deserialize!(ProjectSettingsContent { + sections: { all_languages, worktree }, + options: { + terminal, context_server_timeout, load_direnv, git_hosting_providers, disable_ai, + }, + defaults: { lsp, dap, context_servers }, +}); + /// When to scan content of linked directories. #[derive( Copy, diff --git a/crates/settings_content/src/settings_content.rs b/crates/settings_content/src/settings_content.rs index 20738c73f8..4966ecc344 100644 --- a/crates/settings_content/src/settings_content.rs +++ b/crates/settings_content/src/settings_content.rs @@ -170,7 +170,7 @@ pub enum ReduceMotionMode { } #[with_fallible_options] -#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] +#[derive(Debug, PartialEq, Default, Clone, Serialize, JsonSchema, MergeFrom)] pub struct SettingsContent { #[serde(flatten)] pub project: ProjectSettingsContent, @@ -394,6 +394,26 @@ impl SettingsContent { } } +fallible_options::flattened_deserialize!(SettingsContent { + sections: { project, theme, extension, workspace, editor, remote }, + options: { + file_finder, git_panel, tabs, tab_bar, status_bar, preview_tabs, agent, agent_servers, + audio, auto_update, base_keymap, collaboration_panel, debugger, diagnostics, git, + global_lsp_settings, image_viewer, markdown_preview, repl, helix_mode, hide_mouse, + journal, log, line_indicator_format, language_models, outline_panel, project_panel, + node, proxy, reduce_motion, server_url, credentials_url, session, telemetry, terminal, + title_bar, vim_mode, calls, which_key, vim, modeline_lines, feature_flags, + instrumentation, + }, + defaults: {}, +}); + +fallible_options::flattened_deserialize!(UserSettingsContent { + sections: { content, release_channel_overrides, platform_overrides }, + options: {}, + defaults: { profiles }, +}); + // These impls are there to optimize builds by avoiding monomorphization downstream. Yes, they're repetitive, but using default impls // break the optimization, for whatever reason. pub trait RootUserSettings: Sized + DeserializeOwned { @@ -469,7 +489,7 @@ pub struct SettingsProfile { } #[with_fallible_options] -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] +#[derive(Debug, Default, PartialEq, Clone, Serialize, JsonSchema, MergeFrom)] pub struct UserSettingsContent { #[serde(flatten)] pub content: Box, diff --git a/crates/settings_content/src/terminal.rs b/crates/settings_content/src/terminal.rs index 7ac398295f..86b9557639 100644 --- a/crates/settings_content/src/terminal.rs +++ b/crates/settings_content/src/terminal.rs @@ -66,7 +66,7 @@ pub struct ProjectTerminalSettingsContent { } #[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] +#[derive(Clone, Debug, PartialEq, Default, Serialize, JsonSchema, MergeFrom)] pub struct TerminalSettingsContent { #[serde(flatten)] pub project: ProjectTerminalSettingsContent, @@ -195,6 +195,18 @@ pub struct TerminalSettingsContent { pub bell: Option, } +crate::fallible_options::flattened_deserialize!(TerminalSettingsContent { + sections: { project }, + options: { + font_size, font_family, font_fallbacks, line_height, font_features, font_weight, + cursor_shape, blinking, alternate_scroll, option_as_meta, copy_on_select, + keep_selection_on_copy, open_links_in_mouse_mode, button, dock, starts_open, flexible, + default_width, default_height, max_scroll_history_lines, scroll_multiplier, toolbar, + scrollbar, minimum_contrast, show_count_badge, bell, + }, + defaults: {}, +}); + /// Shell configuration to open the terminal with. #[derive( Clone, diff --git a/crates/settings_content/tests/flatten_collision_probe.rs b/crates/settings_content/tests/flatten_collision_probe.rs new file mode 100644 index 0000000000..e0e59e4cd9 --- /dev/null +++ b/crates/settings_content/tests/flatten_collision_probe.rs @@ -0,0 +1,96 @@ +use std::collections::BTreeMap; + +const KNOWN_SHADOWED_KEYS: &[(&str, &str, &str)] = &[("SettingsContent", "terminal", "project")]; + +#[test] +fn no_key_collisions_between_named_fields_and_flattened_sections() { + check( + "SettingsContent", + settings_content::SettingsContent::NAMED_DESERIALIZE_KEYS, + vec![ + section::("project"), + section::("theme"), + section::("extension"), + section::("workspace"), + section::("editor"), + section::("remote"), + ], + ); + check( + "UserSettingsContent", + settings_content::UserSettingsContent::NAMED_DESERIALIZE_KEYS, + vec![ + section::("content"), + section::("release_channel_overrides"), + section::("platform_overrides"), + ], + ); + check( + "ProjectSettingsContent", + settings_content::ProjectSettingsContent::NAMED_DESERIALIZE_KEYS, + vec![ + section::("all_languages"), + section::("worktree"), + ], + ); + check( + "AllLanguageSettingsContent", + settings_content::AllLanguageSettingsContent::NAMED_DESERIALIZE_KEYS, + vec![section::( + "defaults", + )], + ); + check( + "TerminalSettingsContent", + settings_content::TerminalSettingsContent::NAMED_DESERIALIZE_KEYS, + vec![section::( + "project", + )], + ); +} + +fn property_names() -> Vec { + let schema = schemars::schema_for!(T); + let value = serde_json::to_value(&schema).unwrap(); + value + .get("properties") + .and_then(|properties| properties.as_object()) + .map(|object| object.keys().cloned().collect()) + .unwrap_or_default() +} + +fn section(name: &'static str) -> (&'static str, Vec) { + (name, property_names::()) +} + +fn check(container: &str, named_keys: &[&str], sections: Vec<(&'static str, Vec)>) { + let mut owners = BTreeMap::<&str, Vec<&str>>::new(); + for (section_name, keys) in §ions { + for key in keys { + owners.entry(key).or_default().push(section_name); + } + } + + for named_key in named_keys { + let Some(shadowed_sections) = owners.get(named_key) else { + continue; + }; + let known = shadowed_sections.iter().all(|section_name| { + KNOWN_SHADOWED_KEYS.contains(&(container, named_key, section_name)) + }); + assert!( + known, + "{container}: named field `{named_key}` shadows the same key in flattened \ + section(s) {shadowed_sections:?}: the named field wins and the section never \ + sees the key; if intended, add it to KNOWN_SHADOWED_KEYS" + ); + } + for (key, sections_with_key) in owners { + assert_eq!( + sections_with_key.len(), + 1, + "{container}: key `{key}` is provided by multiple flattened sections: \ + {sections_with_key:?}" + ); + } +}