perf(ipc): reuse theme snapshots and typed window records

This commit is contained in:
Samuel Onoja committed 2026-10-24 19:56:57 +01:00
1 parent a8ef3967a1
commit f49f62583d
6 files changed
+410 -29

No files matched your search

+33 -18
View File
@@ -1630,24 +1630,31 @@ impl Ferese {
}
}
let windows = if locked {
json!([])
Vec::new()
} else {
Value::Array(self.windows.ordered_ids().filter_map(|id| {
let record = self.windows.record(id)?;
let workspace = self.workspaces.workspace_for_window(id)?;
Some(json!({
"id": id.0, "app_id": record.app_id, "title": record.title,
"focused": self.focused_window == Some(id), "workspace": workspace.0,
"floating": matches!(self.workspaces.placement(id), Some(ferese_core::WindowPlacement::Floating { .. })),
"fullscreen": self.workspaces.workspace(workspace).is_some_and(|workspace| workspace.fullscreen == Some(id)),
}))
}).collect())
self.windows
.ordered_ids()
.filter_map(|id| {
let record = self.windows.record(id)?;
let workspace = self.workspaces.workspace_for_window(id)?;
Some(ferese_ipc::events::Window {
id: id.0,
app_id: record.app_id.clone(),
title: record.title.clone(),
focused: self.focused_window == Some(id),
workspace: workspace.0,
floating: matches!(
self.workspaces.placement(id),
Some(ferese_core::WindowPlacement::Floating { .. })
),
fullscreen: self
.workspaces
.workspace(workspace)
.is_some_and(|workspace| workspace.fullscreen == Some(id)),
})
})
.collect()
};
let live = &self.theme_engine.live;
let theme = json!({
"mode": live.mode, "theme": live.theme, "families": live.families,
"warnings": live.warnings, "error": live.error, "fallback_note": live.fallback_note,
});
ferese_ipc::events::Snapshot {
outputs,
workspaces: if locked {
@@ -1669,7 +1676,7 @@ impl Ferese {
revision: self.ipc_events.config_revision,
error: self.theme_engine.live.error.clone(),
},
theme,
theme: self.theme_engine.event_value(),
lock: self.session_lock.event_state(),
}
}
@@ -2512,7 +2519,15 @@ mod tests {
generation: 1,
last: true,
change: ferese_ipc::events::Change::WindowsChanged {
windows: json!([{"title": "x".repeat(900_000)}]),
windows: vec![ferese_ipc::events::Window {
id: 1,
app_id: "org.example.Test".into(),
title: "x".repeat(900_000),
focused: false,
workspace: 1,
floating: false,
fullscreen: false,
}],
},
}))
.unwrap();
+130 -4
View File
@@ -63,6 +63,9 @@ impl Subscribers {
.values()
.any(|subscriber| subscriber.initial)
{
// Keep the latest shared payload even when a theme publication
// produced equal logical values, so later comparisons stay cheap.
self.previous = self.active().then_some(snapshot);
return;
}
self.generation = self.generation.wrapping_add(1);
@@ -144,14 +147,111 @@ mod tests {
use serde_json::json;
use std::sync::mpsc::{TryRecvError, sync_channel};
#[test]
#[ignore = "manual release benchmark for IPC snapshot construction"]
fn profile_unchanged_event_snapshots() {
let name = "ipc_events::tests::profile_unchanged_event_snapshots";
if !crate::startup_tests::private_runtime(name) {
return;
}
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let state = crate::startup_tests::state(&mut events);
for _ in 0..100 {
std::hint::black_box(state.ipc_event_snapshot());
}
for run in 1..=3 {
let started = std::time::Instant::now();
for _ in 0..2000 {
std::hint::black_box(state.ipc_event_snapshot());
}
eprintln!(
"event_snapshot_benchmark run={run} iterations=2000 elapsed_us={}",
started.elapsed().as_micros()
);
}
}
#[test]
#[ignore = "manual release benchmark for populated IPC publication"]
fn profile_populated_event_publication() {
use crate::state::DesktopOutput;
use smithay::output::{Mode, Output, PhysicalProperties, Scale, Subpixel};
use smithay::utils::Transform;
if !crate::startup_tests::private_runtime(
"ipc_events::tests::profile_populated_event_publication",
) {
return;
}
let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap();
let mut state = crate::startup_tests::state(&mut events);
let output = DesktopOutput {
output: Output::new(
"benchmark".into(),
PhysicalProperties {
serial_number: String::new(),
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
),
identity: "benchmark".into(),
mode: Mode {
size: (1920, 1080).into(),
refresh: 60_000,
},
transform: Transform::Normal,
scale: Scale::Fractional(1.0),
position: (0, 0).into(),
};
state.begin_desktop_transition();
let changes = state.publish_desktop(vec![output]).unwrap();
state.finish_desktop_transition(changes);
let mut clients = Vec::new();
for window_count in [0, 1, 20, 100] {
while clients.len() < window_count {
let (window, client) =
crate::state::test_window(&mut state, &mut events, 0xff204060);
let id = state.windows.ids()[&window];
let record = state.windows.record_mut(id).unwrap();
record.app_id = "org.example.Editor".into();
record.title = format!("Document {} — publication benchmark", id.0);
clients.push(client);
}
let (sender, receive) = sync_channel(QUEUE_CAPACITY);
state.ipc_events.subscribe(1, sender).unwrap();
state.publish_ipc_events();
assert!(matches!(
receive.try_recv().unwrap().change,
Change::Snapshot { .. }
));
for _ in 0..100 {
state.publish_ipc_events();
}
for run in 1..=3 {
let started = std::time::Instant::now();
for _ in 0..2000 {
state.publish_ipc_events();
}
eprintln!(
"event_publication_benchmark windows={window_count} run={run} iterations=2000 elapsed_us={}",
started.elapsed().as_micros()
);
}
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
state.ipc_events.remove(1);
}
}
fn snapshot() -> Snapshot {
Snapshot {
outputs: json!([]),
workspaces: json!([]),
windows: json!([]),
windows: Vec::new(),
focus: json!(null),
config: ConfigState::default(),
theme: json!({}),
theme: json!({}).into(),
lock: LockState {
phase: "unlocked".into(),
sleeping: false,
@@ -186,6 +286,24 @@ mod tests {
assert!(!events[0].last && !events[1].last && events[2].last);
}
#[test]
fn equal_theme_publications_retain_shared_storage_without_emitting_events() {
let mut hub = Subscribers::default();
let (sender, receive) = sync_channel(QUEUE_CAPACITY);
hub.subscribe(1, sender).unwrap();
let first = snapshot();
hub.publish(first.clone());
receive.try_recv().unwrap();
let generation = hub.generation;
let mut next = first;
next.theme = Arc::new(next.theme.as_ref().clone());
let payload = next.theme.clone();
hub.publish(next);
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
assert_eq!(hub.generation, generation);
assert!(Arc::ptr_eq(&hub.previous.as_ref().unwrap().theme, &payload));
}
#[test]
fn full_and_disconnected_subscribers_do_not_block_healthy_readers() {
let mut hub = Subscribers::default();
@@ -240,10 +358,18 @@ mod tests {
receive.try_recv().unwrap();
state.outputs = json!([{"id": 1}]);
state.workspaces = json!([{"output": 1}]);
state.windows = json!([{"id": 7, "title": "Terminal"}]);
state.windows = vec![ferese_ipc::events::Window {
id: 7,
app_id: "org.example.Terminal".into(),
title: "Terminal".into(),
focused: false,
workspace: 1,
floating: false,
fullscreen: false,
}];
state.focus = json!({"window": 7});
state.config.revision = 1;
state.theme = json!({"mode": "light"});
state.theme = json!({"mode": "light"}).into();
state.lock.phase = "locked".into();
hub.publish(state);
let events = receive.try_iter().collect::<Vec<_>>();
+56
View File
@@ -1,3 +1,4 @@
use std::cell::OnceCell;
use std::collections::{HashMap, HashSet};
use std::io;
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
@@ -138,6 +139,7 @@ impl Default for LiveTheme {
pub(crate) struct Engine {
pub(crate) live: LiveTheme,
pub(crate) revision: u64,
event_value: OnceCell<Arc<Value>>,
waiters: HashMap<u64, (u64, mpsc::SyncSender<Response>)>,
watcher: Option<RecommendedWatcher>,
watched_directories: HashSet<PathBuf>,
@@ -207,7 +209,24 @@ impl Engine {
serde_json::to_value(snapshot).expect("serializable theme snapshot")
}
/// Share the logical event payload until the next theme publication.
pub(crate) fn event_value(&self) -> Arc<Value> {
self.event_value
.get_or_init(|| {
Arc::new(json!({
"mode": self.live.mode,
"theme": self.live.theme,
"families": self.live.families,
"warnings": self.live.warnings,
"error": self.live.error,
"fallback_note": self.live.fallback_note,
}))
})
.clone()
}
fn publish(&mut self) {
self.event_value.take();
self.revision = self.revision.wrapping_add(1);
let value = self.value();
for (_, (id, response)) in self.waiters.drain() {
@@ -216,6 +235,7 @@ impl Engine {
}
pub(crate) fn startup_failed(&mut self, path: Option<&Path>, error: String) {
self.event_value.take();
let path = path
.map(|path| path.display().to_string())
.unwrap_or_else(|| "configuration".into());
@@ -793,6 +813,42 @@ mod tests {
assert_eq!(engine.revision, 3);
}
#[test]
fn event_values_reuse_storage_and_follow_theme_and_error_publication() {
let mut engine = Engine::default();
let original = engine.event_value();
let mut expected = engine.value();
for key in ["version", "revision", "presented"] {
expected.as_object_mut().unwrap().remove(key);
}
assert_eq!(original.as_ref(), &expected);
assert!(Arc::ptr_eq(&original, &engine.event_value()));
engine.live.mode = Mode::Light;
engine.publish();
let light = engine.event_value();
assert!(!Arc::ptr_eq(&original, &light));
assert_eq!(light["mode"], "light");
assert_eq!(
original["mode"], "dark",
"queued snapshots remain immutable"
);
engine.reject("invalid theme".into());
let rejected = engine.event_value();
assert_eq!(rejected["error"], "invalid theme");
assert!(light["error"].is_null());
engine.startup_failed(None, "invalid startup configuration".into());
let startup = engine.event_value();
assert!(
startup["error"]
.as_str()
.unwrap()
.contains("invalid startup configuration")
);
assert!(Arc::ptr_eq(&startup, &engine.event_value()));
}
#[test]
fn publication_projects_live_values_and_preserves_revision_semantics() {
let mut engine = Engine::default();