git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit13725e22a831efa7aed87592e53416a11d90bb09
parent3b09eeb714
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-05 21:02
perf(config): getters borrow the cached config instead of copying it

Every typed getter (get_bool/get_i64/get_f32/get_string, the _shared
pair) went through cached_config(), which returned a deep clone of the
whole merged config tree to read one key, after a readlink of
/proc/self/exe for the app name. They run many times a frame: the root
plate's corner radius per frame in every app, ~14 per style() call in
cce-grid (three calls a frame), four per text-cache miss through
read_preferred_fonts.

The cache now holds an Arc and the getters read through
cached_config_arc(); the app name is resolved once per process (the
stripped basename cannot change). cached_config() and
cached_shared_config() keep their owned-Value signatures for callers
that want a copy. The mtime check is unchanged, so a config edit is
seen on the next read exactly as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/config.rs | 63 +++++++++++++++++++++++++++++++++++++++++------------------
 1 file changed, 44 insertions(+), 19 deletions(-)

diff --git a/src/config.rs b/src/config.rs
index 6d26959..0845eb3 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -210,16 +210,29 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
 /// its per-app config (`~/.config/cce/<name>/config.kdl`) and its `input.kdl`
 /// domain.
 ///
-/// Derived from `/proc/self/exe` on every call, which the kernel renders as
+/// Derived from `/proc/self/exe` once per process, which the kernel renders as
 /// `<path> (deleted)` once the binary on disk has been replaced (`ccebuild
 /// install` unlinks before writing). Without the strip, a still-running client
 /// would resolve its override to `~/.config/cce/<name> (deleted)/config.kdl`
 /// on its next config reload and silently lose the whole file — the status
 /// bar's droplet bubbles reverted to square boxes this way on 2026-09-03.
+///
+/// Cached because every config getter reaches it (through
+/// [`config_files_modified`]), many of them per frame; the stripped basename
+/// cannot change for the life of the process.
 pub fn get_app_name() -> Option<String> {
-    std::env::current_exe()
-        .ok()
-        .and_then(|p| p.file_name().and_then(|s| s.to_str().map(app_name_from_exe_basename)))
+    app_name().map(str::to_string)
+}
+
+/// [`get_app_name`] without the allocation.
+fn app_name() -> Option<&'static str> {
+    static NAME: std::sync::OnceLock<Option<String>> = std::sync::OnceLock::new();
+    NAME.get_or_init(|| {
+        std::env::current_exe()
+            .ok()
+            .and_then(|p| p.file_name().and_then(|s| s.to_str().map(app_name_from_exe_basename)))
+    })
+    .as_deref()
 }
 
 /// [`get_app_name`]'s normalization: the kernel's ` (deleted)` marker on an
@@ -254,8 +267,8 @@ pub fn parse_kdl_to_json(content: &str) -> serde_json::Value {
         serde_json::json!({})
     };
 
-    if let Some(app_name) = get_app_name() {
-        let app_path = get_app_config_path(&app_name);
+    if let Some(app_name) = app_name() {
+        let app_path = get_app_config_path(app_name);
         if let Ok(override_content) = std::fs::read_to_string(&app_path) {
             if let Ok(override_doc) = override_content.parse::<kdl::KdlDocument>() {
                 let override_val = kdl_to_json(&override_doc);
@@ -604,7 +617,9 @@ pub fn get_config_path() -> std::path::PathBuf {
 
 struct ConfigCache {
     last_modified: Option<std::time::SystemTime>,
-    parsed: Option<serde_json::Value>,
+    /// Shared, not cloned per read: the getters below run many times a frame,
+    /// and a deep copy of the whole tree to read one key was most of their cost.
+    parsed: Option<std::sync::Arc<serde_json::Value>>,
     raw_content: String,
 }
 
@@ -622,8 +637,8 @@ static CONFIG_CACHE: std::sync::RwLock<ConfigCache> = std::sync::RwLock::new(Con
 /// drops back to the shared mtime) also invalidates.
 pub fn config_files_modified() -> Option<std::time::SystemTime> {
     let shared = std::fs::metadata(get_config_path()).ok().and_then(|m| m.modified().ok());
-    let app = get_app_name()
-        .and_then(|n| std::fs::metadata(get_app_config_path(&n)).ok())
+    let app = app_name()
+        .and_then(|n| std::fs::metadata(get_app_config_path(n)).ok())
         .and_then(|m| m.modified().ok());
     match (shared, app) {
         (Some(a), Some(b)) => Some(a.max(b)),
@@ -632,6 +647,11 @@ pub fn config_files_modified() -> Option<std::time::SystemTime> {
 }
 
 pub fn cached_config() -> serde_json::Value {
+    (*cached_config_arc()).clone()
+}
+
+/// [`cached_config`] without the copy — what every getter here reads.
+pub fn cached_config_arc() -> std::sync::Arc<serde_json::Value> {
     let path = get_config_path();
     // Both files participate in the parse (parse_kdl_to_json merges the
     // per-app override), so both participate in the cache key.
@@ -646,7 +666,7 @@ pub fn cached_config() -> serde_json::Value {
     }
 
     let content = std::fs::read_to_string(&path).unwrap_or_default();
-    let val = parse_kdl_to_json(&content);
+    let val = std::sync::Arc::new(parse_kdl_to_json(&content));
     if let Ok(mut cache) = CONFIG_CACHE.write() {
         cache.last_modified = current_modified;
         cache.parsed = Some(val.clone());
@@ -657,7 +677,7 @@ pub fn cached_config() -> serde_json::Value {
 
 /// The raw text of the cce config, cached alongside [`cached_config`].
 pub fn cached_config_content() -> String {
-    let _ = cached_config();
+    let _ = cached_config_arc();
     CONFIG_CACHE.read().map(|c| c.raw_content.clone()).unwrap_or_default()
 }
 
@@ -673,6 +693,11 @@ static SHARED_CONFIG_CACHE: std::sync::RwLock<ConfigCache> = std::sync::RwLock::
 /// but must not desynchronize it from the DE. Mtime-cached like
 /// [`cached_config`].
 pub fn cached_shared_config() -> serde_json::Value {
+    (*cached_shared_config_arc()).clone()
+}
+
+/// [`cached_shared_config`] without the copy.
+pub fn cached_shared_config_arc() -> std::sync::Arc<serde_json::Value> {
     let path = get_config_path();
     let current_modified = std::fs::metadata(&path).ok().and_then(|m| m.modified().ok());
 
@@ -685,11 +710,11 @@ pub fn cached_shared_config() -> serde_json::Value {
     }
 
     let content = std::fs::read_to_string(&path).unwrap_or_default();
-    let val = if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
+    let val = std::sync::Arc::new(if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
         kdl_to_json(&doc)
     } else {
         serde_json::json!({})
-    };
+    });
     if let Ok(mut cache) = SHARED_CONFIG_CACHE.write() {
         cache.last_modified = current_modified;
         cache.parsed = Some(val.clone());
@@ -701,13 +726,13 @@ pub fn cached_shared_config() -> serde_json::Value {
 /// Read an i64 at `pointer` from the SHARED config only (no per-app merge),
 /// or `default` — see [`cached_shared_config`].
 pub fn get_i64_shared(pointer: &str, default: i64) -> i64 {
-    cached_shared_config().pointer(pointer).and_then(|v| v.as_i64()).unwrap_or(default)
+    cached_shared_config_arc().pointer(pointer).and_then(|v| v.as_i64()).unwrap_or(default)
 }
 
 /// [`get_i64_shared`] without a default — for canonical-first alias chains
 /// (RFC Phase 7a) where absence must fall through to the next spelling.
 pub fn get_i64_shared_opt(pointer: &str) -> Option<i64> {
-    cached_shared_config().pointer(pointer).and_then(|v| v.as_i64())
+    cached_shared_config_arc().pointer(pointer).and_then(|v| v.as_i64())
 }
 
 // ── Typed accessors over the cached config ──────────────────────────────────
@@ -716,12 +741,12 @@ pub fn get_i64_shared_opt(pointer: &str) -> Option<i64> {
 
 /// Read a boolean at `pointer` from the cached config, or `default`.
 pub fn get_bool(pointer: &str, default: bool) -> bool {
-    cached_config().pointer(pointer).and_then(|v| v.as_bool()).unwrap_or(default)
+    cached_config_arc().pointer(pointer).and_then(|v| v.as_bool()).unwrap_or(default)
 }
 
 /// Read an f32 at `pointer` from the cached config, or `default`.
 pub fn get_f32(pointer: &str, default: f32) -> f32 {
-    cached_config()
+    cached_config_arc()
         .pointer(pointer)
         .and_then(|v| v.as_f64())
         .map(|f| f as f32)
@@ -730,12 +755,12 @@ pub fn get_f32(pointer: &str, default: f32) -> f32 {
 
 /// Read an i64 at `pointer` from the cached config, or `default`.
 pub fn get_i64(pointer: &str, default: i64) -> i64 {
-    cached_config().pointer(pointer).and_then(|v| v.as_i64()).unwrap_or(default)
+    cached_config_arc().pointer(pointer).and_then(|v| v.as_i64()).unwrap_or(default)
 }
 
 /// Read a string at `pointer` from the cached config.
 pub fn get_string(pointer: &str) -> Option<String> {
-    cached_config().pointer(pointer).and_then(|v| v.as_str()).map(|s| s.to_string())
+    cached_config_arc().pointer(pointer).and_then(|v| v.as_str()).map(|s| s.to_string())
 }
 
 /// Read a hex color string at `pointer` and parse it to raw sRGB RGBA (`[0,1]`).