Merge branch 'config-cache'

This commit is contained in:
server2
2026-10-20 15:37:42 +02:00
+89 -20
View File
@@ -1,7 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fs; use std::fs;
use std::path::PathBuf; use std::path::PathBuf;
use std::cell::RefCell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{LazyLock, Mutex, PoisonError, RwLock}; use std::sync::{LazyLock, Mutex, PoisonError, RwLock};
use std::thread::LocalKey;
use amane::Color; use amane::Color;
@@ -181,7 +184,11 @@ macro_rules! settings {
$( $(
#[allow(dead_code)] #[allow(dead_code)]
pub fn $name() -> $ty { pub fn $name() -> $ty {
super::super::get(concat!(stringify!($module), ".", stringify!($group), ".", stringify!($name)), || $default) std::thread_local! {
static SLOT: std::cell::RefCell<(u64, Option<$ty>)> = const { std::cell::RefCell::new((0, None)) };
}
super::super::cached(&SLOT, concat!(stringify!($module), ".", stringify!($group), ".", stringify!($name)), || $default)
} }
)* )*
} )* } )*
@@ -615,9 +622,12 @@ settings! {
} }
} }
// what config.toml set, by path; a missing or bad setting takes its default // what config.toml set, by path; a missing or bad setting takes its default. written only through change()
static VALUES: LazyLock<RwLock<HashMap<String, Value>>> = LazyLock::new(Default::default); static VALUES: LazyLock<RwLock<HashMap<String, Value>>> = LazyLock::new(Default::default);
// how many times VALUES changed; an accessor's cached answer from an older count is read again
static GENERATION: AtomicU64 = AtomicU64::new(1);
// what was wrong in the file when it was last read, for the config window // what was wrong in the file when it was last read, for the config window
static ERRORS: Mutex<Vec<String>> = Mutex::new(Vec::new()); static ERRORS: Mutex<Vec<String>> = Mutex::new(Vec::new());
@@ -632,11 +642,43 @@ fn written() -> std::sync::MutexGuard<'static, String> {
WRITTEN.lock().unwrap_or_else(PoisonError::into_inner) WRITTEN.lock().unwrap_or_else(PoisonError::into_inner)
} }
// ponytail: a lock and a hash lookup per call, a few hundred a frame; a snapshot struct if a profile ever shows it
fn get<T: Setting>(path: &str, default: impl FnOnce() -> T) -> T { fn get<T: Setting>(path: &str, default: impl FnOnce() -> T) -> T {
VALUES.read().unwrap_or_else(PoisonError::into_inner).get(path).and_then(T::read).unwrap_or_else(default) VALUES.read().unwrap_or_else(PoisonError::into_inner).get(path).and_then(T::read).unwrap_or_else(default)
} }
// an accessor's last answer on this thread, with the GENERATION it was read at
type Slot<T> = LocalKey<RefCell<(u64, Option<T>)>>;
/*
* what every accessor (settings!) returns: its last answer while VALUES hasn't changed
* since, else a fresh get(). a few hundred reads a frame with a card open, each a lock,
* a hash of the path and a conversion (a colour parsed from its text) at 160 to 570 ns
* unoptimised; a cached one is an atomic load and a copy, 13 ns. per thread, so it takes
* no lock. a read racing a change may return the old value once, as get() could
*/
fn cached<T: Setting + Clone>(slot: &'static Slot<T>, path: &str, default: impl FnOnce() -> T) -> T {
let now = GENERATION.load(Ordering::Acquire);
if let Some(value) = slot.with_borrow(|(seen, value)| value.clone().filter(|_| *seen == now)) {
return value;
}
let value = get(path, default);
slot.set((now, Some(value.clone())));
value
}
// every change to VALUES: made, then counted (the write lock is gone by then), so the accessors read again
fn change<R>(edit: impl FnOnce(&mut HashMap<String, Value>) -> R) -> R {
let done = edit(&mut VALUES.write().unwrap_or_else(PoisonError::into_inner));
GENERATION.fetch_add(1, Ordering::Release);
done
}
pub fn file() -> PathBuf { pub fn file() -> PathBuf {
theme::config_file("config.toml") theme::config_file("config.toml")
} }
@@ -683,23 +725,24 @@ pub fn reread() -> bool {
// shell.conf's four settings, moved to their places in config.toml // shell.conf's four settings, moved to their places in config.toml
fn migrate() { fn migrate() {
let old = fs::read_to_string(theme::config_file("shell.conf")).unwrap_or_default(); let old = fs::read_to_string(theme::config_file("shell.conf")).unwrap_or_default();
let mut values = VALUES.write().unwrap_or_else(PoisonError::into_inner);
for (key, text) in parse::settings(&old) { change(|values| {
let path = match key.as_str() { for (key, text) in parse::settings(&old) {
"cover_colors" => "appearance.colors.cover_colors", let path = match key.as_str() {
"transition_ms" => "appearance.colors.transition_ms", "cover_colors" => "appearance.colors.cover_colors",
"hotspot" => "control.services.hotspot", "transition_ms" => "appearance.colors.transition_ms",
"extra_output" => "control.services.extra_output", "hotspot" => "control.services.hotspot",
_ => continue, "extra_output" => "control.services.extra_output",
}; _ => continue,
};
if let Some(option) = option(path) if let Some(option) = option(path)
&& let Ok(value) = from_text(option, &text) && let Ok(value) = from_text(option, &text)
{ {
values.insert(path.to_string(), value); values.insert(path.to_string(), value);
}
} }
} });
} }
// the file's text, read anew: what it sets replaces everything set before // the file's text, read anew: what it sets replaces everything set before
@@ -716,7 +759,7 @@ pub fn apply_text(text: &str) {
} }
} }
*VALUES.write().unwrap_or_else(PoisonError::into_inner) = values; change(|old| *old = values);
*ERRORS.lock().unwrap_or_else(PoisonError::into_inner) = errors; *ERRORS.lock().unwrap_or_else(PoisonError::into_inner) = errors;
hypr::sync_soon(); hypr::sync_soon();
@@ -816,7 +859,7 @@ pub fn set(path: &str, value: Value) -> Result<(), String> {
let mut written = written(); let mut written = written();
VALUES.write().unwrap_or_else(PoisonError::into_inner).insert(path.to_string(), value); change(|values| values.insert(path.to_string(), value));
save(&mut written) save(&mut written)
} }
@@ -825,7 +868,7 @@ pub fn set(path: &str, value: Value) -> Result<(), String> {
pub fn reset(path: &str) -> Result<(), String> { pub fn reset(path: &str) -> Result<(), String> {
let mut written = written(); let mut written = written();
VALUES.write().unwrap_or_else(PoisonError::into_inner).remove(path); change(|values| values.remove(path));
save(&mut written) save(&mut written)
} }
@@ -1004,3 +1047,29 @@ pub fn label(name: &str) -> String {
title(name) title(name)
} }
#[cfg(test)]
mod tests {
use super::*;
// the cached accessors follow every change, on this thread and on others
#[test]
fn accessors_follow_changes() {
let (width, family, accent) = (island::pill::width(), appearance::font::family(), appearance::fallback::accent());
change(|values| {
values.insert("island.pill.width".into(), Value::Float(321.0));
values.insert("appearance.font.family".into(), Value::Text("Iosevka".into()));
values.insert("appearance.fallback.accent".into(), Value::Text("#102030".into()));
});
assert_eq!(island::pill::width(), 321.0);
assert_eq!(appearance::font::family(), "Iosevka");
assert_eq!(appearance::fallback::accent(), Color::rgb(16, 32, 48));
assert_eq!(std::thread::spawn(island::pill::width).join().unwrap(), 321.0);
change(|values| values.clear());
assert_eq!((island::pill::width(), appearance::font::family(), appearance::fallback::accent()), (width, family, accent));
}
}