perf(ipc): reuse theme snapshots and typed window records
This commit is contained in:
1 parent
a8ef3967a1
commit
f49f62583d
6 files changed
+408
-27
No files matched your search
+1
-1
@@ -61,7 +61,7 @@ smithay = { git = "https://github.com/Smithay/smithay", rev = "8a270232f1bfc416c
|
|||||||
] }
|
] }
|
||||||
tracing = "0.1.41"
|
tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive", "rc"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
tempfile = "3.23.0"
|
tempfile = "3.23.0"
|
||||||
xcursor = "0.3.11"
|
xcursor = "0.3.11"
|
||||||
|
|||||||
+31
-16
@@ -1630,24 +1630,31 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let windows = if locked {
|
let windows = if locked {
|
||||||
json!([])
|
Vec::new()
|
||||||
} else {
|
} else {
|
||||||
Value::Array(self.windows.ordered_ids().filter_map(|id| {
|
self.windows
|
||||||
|
.ordered_ids()
|
||||||
|
.filter_map(|id| {
|
||||||
let record = self.windows.record(id)?;
|
let record = self.windows.record(id)?;
|
||||||
let workspace = self.workspaces.workspace_for_window(id)?;
|
let workspace = self.workspaces.workspace_for_window(id)?;
|
||||||
Some(json!({
|
Some(ferese_ipc::events::Window {
|
||||||
"id": id.0, "app_id": record.app_id, "title": record.title,
|
id: id.0,
|
||||||
"focused": self.focused_window == Some(id), "workspace": workspace.0,
|
app_id: record.app_id.clone(),
|
||||||
"floating": matches!(self.workspaces.placement(id), Some(ferese_core::WindowPlacement::Floating { .. })),
|
title: record.title.clone(),
|
||||||
"fullscreen": self.workspaces.workspace(workspace).is_some_and(|workspace| workspace.fullscreen == Some(id)),
|
focused: self.focused_window == Some(id),
|
||||||
}))
|
workspace: workspace.0,
|
||||||
}).collect())
|
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 {
|
ferese_ipc::events::Snapshot {
|
||||||
outputs,
|
outputs,
|
||||||
workspaces: if locked {
|
workspaces: if locked {
|
||||||
@@ -1669,7 +1676,7 @@ impl Ferese {
|
|||||||
revision: self.ipc_events.config_revision,
|
revision: self.ipc_events.config_revision,
|
||||||
error: self.theme_engine.live.error.clone(),
|
error: self.theme_engine.live.error.clone(),
|
||||||
},
|
},
|
||||||
theme,
|
theme: self.theme_engine.event_value(),
|
||||||
lock: self.session_lock.event_state(),
|
lock: self.session_lock.event_state(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2512,7 +2519,15 @@ mod tests {
|
|||||||
generation: 1,
|
generation: 1,
|
||||||
last: true,
|
last: true,
|
||||||
change: ferese_ipc::events::Change::WindowsChanged {
|
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();
|
.unwrap();
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ impl Subscribers {
|
|||||||
.values()
|
.values()
|
||||||
.any(|subscriber| subscriber.initial)
|
.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;
|
return;
|
||||||
}
|
}
|
||||||
self.generation = self.generation.wrapping_add(1);
|
self.generation = self.generation.wrapping_add(1);
|
||||||
@@ -144,14 +147,111 @@ mod tests {
|
|||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::mpsc::{TryRecvError, sync_channel};
|
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 {
|
fn snapshot() -> Snapshot {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
outputs: json!([]),
|
outputs: json!([]),
|
||||||
workspaces: json!([]),
|
workspaces: json!([]),
|
||||||
windows: json!([]),
|
windows: Vec::new(),
|
||||||
focus: json!(null),
|
focus: json!(null),
|
||||||
config: ConfigState::default(),
|
config: ConfigState::default(),
|
||||||
theme: json!({}),
|
theme: json!({}).into(),
|
||||||
lock: LockState {
|
lock: LockState {
|
||||||
phase: "unlocked".into(),
|
phase: "unlocked".into(),
|
||||||
sleeping: false,
|
sleeping: false,
|
||||||
@@ -186,6 +286,24 @@ mod tests {
|
|||||||
assert!(!events[0].last && !events[1].last && events[2].last);
|
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]
|
#[test]
|
||||||
fn full_and_disconnected_subscribers_do_not_block_healthy_readers() {
|
fn full_and_disconnected_subscribers_do_not_block_healthy_readers() {
|
||||||
let mut hub = Subscribers::default();
|
let mut hub = Subscribers::default();
|
||||||
@@ -240,10 +358,18 @@ mod tests {
|
|||||||
receive.try_recv().unwrap();
|
receive.try_recv().unwrap();
|
||||||
state.outputs = json!([{"id": 1}]);
|
state.outputs = json!([{"id": 1}]);
|
||||||
state.workspaces = json!([{"output": 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.focus = json!({"window": 7});
|
||||||
state.config.revision = 1;
|
state.config.revision = 1;
|
||||||
state.theme = json!({"mode": "light"});
|
state.theme = json!({"mode": "light"}).into();
|
||||||
state.lock.phase = "locked".into();
|
state.lock.phase = "locked".into();
|
||||||
hub.publish(state);
|
hub.publish(state);
|
||||||
let events = receive.try_iter().collect::<Vec<_>>();
|
let events = receive.try_iter().collect::<Vec<_>>();
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::cell::OnceCell;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io;
|
use std::io;
|
||||||
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
|
||||||
@@ -138,6 +139,7 @@ impl Default for LiveTheme {
|
|||||||
pub(crate) struct Engine {
|
pub(crate) struct Engine {
|
||||||
pub(crate) live: LiveTheme,
|
pub(crate) live: LiveTheme,
|
||||||
pub(crate) revision: u64,
|
pub(crate) revision: u64,
|
||||||
|
event_value: OnceCell<Arc<Value>>,
|
||||||
waiters: HashMap<u64, (u64, mpsc::SyncSender<Response>)>,
|
waiters: HashMap<u64, (u64, mpsc::SyncSender<Response>)>,
|
||||||
watcher: Option<RecommendedWatcher>,
|
watcher: Option<RecommendedWatcher>,
|
||||||
watched_directories: HashSet<PathBuf>,
|
watched_directories: HashSet<PathBuf>,
|
||||||
@@ -207,7 +209,24 @@ impl Engine {
|
|||||||
serde_json::to_value(snapshot).expect("serializable theme snapshot")
|
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) {
|
fn publish(&mut self) {
|
||||||
|
self.event_value.take();
|
||||||
self.revision = self.revision.wrapping_add(1);
|
self.revision = self.revision.wrapping_add(1);
|
||||||
let value = self.value();
|
let value = self.value();
|
||||||
for (_, (id, response)) in self.waiters.drain() {
|
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) {
|
pub(crate) fn startup_failed(&mut self, path: Option<&Path>, error: String) {
|
||||||
|
self.event_value.take();
|
||||||
let path = path
|
let path = path
|
||||||
.map(|path| path.display().to_string())
|
.map(|path| path.display().to_string())
|
||||||
.unwrap_or_else(|| "configuration".into());
|
.unwrap_or_else(|| "configuration".into());
|
||||||
@@ -793,6 +813,42 @@ mod tests {
|
|||||||
assert_eq!(engine.revision, 3);
|
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]
|
#[test]
|
||||||
fn publication_projects_live_values_and_preserves_revision_semantics() {
|
fn publication_projects_live_values_and_preserves_revision_semantics() {
|
||||||
let mut engine = Engine::default();
|
let mut engine = Engine::default();
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
@@ -16,15 +18,27 @@ pub struct LockState {
|
|||||||
pub sleeping: bool,
|
pub sleeping: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Window metadata included in desktop event snapshots.
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||||
|
pub struct Window {
|
||||||
|
pub id: u64,
|
||||||
|
pub app_id: String,
|
||||||
|
pub title: String,
|
||||||
|
pub focused: bool,
|
||||||
|
pub workspace: u64,
|
||||||
|
pub floating: bool,
|
||||||
|
pub fullscreen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// A coherent logical desktop view. Desktop objects are redacted while locked.
|
/// A coherent logical desktop view. Desktop objects are redacted while locked.
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
pub struct Snapshot {
|
pub struct Snapshot {
|
||||||
pub outputs: Value,
|
pub outputs: Value,
|
||||||
pub workspaces: Value,
|
pub workspaces: Value,
|
||||||
pub windows: Value,
|
pub windows: Vec<Window>,
|
||||||
pub focus: Value,
|
pub focus: Value,
|
||||||
pub config: ConfigState,
|
pub config: ConfigState,
|
||||||
pub theme: Value,
|
pub theme: Arc<Value>,
|
||||||
pub lock: LockState,
|
pub lock: LockState,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,7 +49,7 @@ pub enum Change {
|
|||||||
Snapshot { desktop: Box<Snapshot> },
|
Snapshot { desktop: Box<Snapshot> },
|
||||||
OutputsChanged { outputs: Value },
|
OutputsChanged { outputs: Value },
|
||||||
WorkspacesChanged { workspaces: Value },
|
WorkspacesChanged { workspaces: Value },
|
||||||
WindowsChanged { windows: Value },
|
WindowsChanged { windows: Vec<Window> },
|
||||||
FocusChanged { focus: Value },
|
FocusChanged { focus: Value },
|
||||||
ConfigChanged { config: ConfigState },
|
ConfigChanged { config: ConfigState },
|
||||||
ThemeChanged { theme: Value },
|
ThemeChanged { theme: Value },
|
||||||
@@ -91,9 +105,9 @@ impl Snapshot {
|
|||||||
config: self.config.clone(),
|
config: self.config.clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.theme != previous.theme {
|
if !Arc::ptr_eq(&self.theme, &previous.theme) && self.theme != previous.theme {
|
||||||
changes.push(Change::ThemeChanged {
|
changes.push(Change::ThemeChanged {
|
||||||
theme: self.theme.clone(),
|
theme: self.theme.as_ref().clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.lock != previous.lock {
|
if self.lock != previous.lock {
|
||||||
@@ -108,6 +122,75 @@ impl Snapshot {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_windows_preserve_snapshot_and_replacement_wire_fields() {
|
||||||
|
let windows = serde_json::json!([{
|
||||||
|
"id": u64::MAX,
|
||||||
|
"app_id": "org.example.Editor.desktop",
|
||||||
|
"title": "Chapter \"one\"\n第二课",
|
||||||
|
"focused": true,
|
||||||
|
"workspace": u64::MAX - 1,
|
||||||
|
"floating": true,
|
||||||
|
"fullscreen": false
|
||||||
|
}]);
|
||||||
|
let snapshot = serde_json::json!({
|
||||||
|
"outputs": [], "workspaces": [], "windows": windows,
|
||||||
|
"focus": {"window": u64::MAX},
|
||||||
|
"config": {"revision": 0, "error": null},
|
||||||
|
"theme": null, "lock": {"phase": "unlocked", "sleeping": false}
|
||||||
|
});
|
||||||
|
for change in [
|
||||||
|
serde_json::json!({"type": "snapshot", "desktop": snapshot}),
|
||||||
|
serde_json::json!({"type": "windows_changed", "windows": windows}),
|
||||||
|
] {
|
||||||
|
let decoded: Change = serde_json::from_value(change.clone()).unwrap();
|
||||||
|
assert_eq!(serde_json::to_value(&decoded).unwrap(), change);
|
||||||
|
let decoded_windows = match decoded {
|
||||||
|
Change::Snapshot { desktop } => desktop.windows,
|
||||||
|
Change::WindowsChanged { windows } => windows,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
decoded_windows,
|
||||||
|
vec![Window {
|
||||||
|
id: u64::MAX,
|
||||||
|
app_id: "org.example.Editor.desktop".into(),
|
||||||
|
title: "Chapter \"one\"\n第二课".into(),
|
||||||
|
focused: true,
|
||||||
|
workspace: u64::MAX - 1,
|
||||||
|
floating: true,
|
||||||
|
fullscreen: false,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shared_themes_preserve_the_wire_format_and_compare_by_value_after_replacement() {
|
||||||
|
let wire = serde_json::json!({
|
||||||
|
"outputs": [], "workspaces": [], "windows": [], "focus": null,
|
||||||
|
"config": {"revision": 0, "error": null},
|
||||||
|
"theme": {"mode": "dark"},
|
||||||
|
"lock": {"phase": "unlocked", "sleeping": false}
|
||||||
|
});
|
||||||
|
let snapshot: Snapshot = serde_json::from_value(wire.clone()).unwrap();
|
||||||
|
assert_eq!(serde_json::to_value(&snapshot).unwrap(), wire);
|
||||||
|
let mut next = snapshot.clone();
|
||||||
|
assert!(Arc::ptr_eq(&snapshot.theme, &next.theme));
|
||||||
|
assert!(next.changes_since(&snapshot).is_empty());
|
||||||
|
next.theme = Arc::new(next.theme.as_ref().clone());
|
||||||
|
assert!(!Arc::ptr_eq(&snapshot.theme, &next.theme));
|
||||||
|
assert!(next.changes_since(&snapshot).is_empty());
|
||||||
|
next.theme = serde_json::json!({"mode": "light"}).into();
|
||||||
|
assert_eq!(
|
||||||
|
next.changes_since(&snapshot),
|
||||||
|
[Change::ThemeChanged {
|
||||||
|
theme: serde_json::json!({"mode": "light"})
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn events_round_trip_with_an_independent_schema_version() {
|
fn events_round_trip_with_an_independent_schema_version() {
|
||||||
let event = Event {
|
let event = Event {
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
|
|||||||
{
|
{
|
||||||
let _ = state
|
let _ = state
|
||||||
.sender
|
.sender
|
||||||
.send(ControlUpdate::Snapshot(state.pending.clone()));
|
.send(ControlUpdate::Snapshot(std::mem::take(&mut state.pending)));
|
||||||
}
|
}
|
||||||
ferese_shell_v1::Event::LogoutRequested {
|
ferese_shell_v1::Event::LogoutRequested {
|
||||||
serial,
|
serial,
|
||||||
@@ -656,6 +656,107 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "manual release benchmark for shell snapshot handoff"]
|
||||||
|
fn profile_snapshot_handoff() {
|
||||||
|
let (control, mut state, queue, _peer) = control_fixture();
|
||||||
|
for window_count in [1, 20, 100] {
|
||||||
|
let template = ShellSnapshot {
|
||||||
|
windows: (0..window_count)
|
||||||
|
.map(|id| WindowSnapshot {
|
||||||
|
id,
|
||||||
|
workspace: 1,
|
||||||
|
geometry: Some([0., 0., 800., 600.]),
|
||||||
|
app_id: "org.example.Editor".into(),
|
||||||
|
title: format!("Document {id} — Ferese workspace"),
|
||||||
|
focused: id == 0,
|
||||||
|
urgent: false,
|
||||||
|
fullscreen: false,
|
||||||
|
maximized: false,
|
||||||
|
floating: false,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
..ShellSnapshot::default()
|
||||||
|
};
|
||||||
|
for run in 1..=3 {
|
||||||
|
// Decoding is outside the timed handoff, as it is unchanged.
|
||||||
|
let snapshots: Vec<_> = (0..2000).map(|_| template.clone()).collect();
|
||||||
|
let started = std::time::Instant::now();
|
||||||
|
for snapshot in snapshots {
|
||||||
|
state.pending = snapshot;
|
||||||
|
state.serial = Some(1);
|
||||||
|
dispatch_event(
|
||||||
|
&control,
|
||||||
|
&mut state,
|
||||||
|
&queue,
|
||||||
|
ferese_shell_v1::Event::SnapshotEnd { serial: 1 },
|
||||||
|
);
|
||||||
|
std::hint::black_box(control.poll().snapshot.unwrap());
|
||||||
|
}
|
||||||
|
// Include destruction of the last pending snapshot in both builds.
|
||||||
|
state.pending = ShellSnapshot::default();
|
||||||
|
eprintln!(
|
||||||
|
"snapshot_handoff_benchmark windows={window_count} run={run} iterations=2000 elapsed_us={}",
|
||||||
|
started.elapsed().as_micros()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completed_snapshots_transfer_their_storage_and_remain_independent() {
|
||||||
|
let (control, mut state, queue, _peer) = control_fixture();
|
||||||
|
dispatch_event(
|
||||||
|
&control,
|
||||||
|
&mut state,
|
||||||
|
&queue,
|
||||||
|
ferese_shell_v1::Event::SnapshotBegin { serial: 1 },
|
||||||
|
);
|
||||||
|
dispatch_event(&control, &mut state, &queue, window_event(10, 0));
|
||||||
|
let windows = state.pending.windows.as_ptr();
|
||||||
|
let app_id = state.pending.windows[0].app_id.as_ptr();
|
||||||
|
let title = state.pending.windows[0].title.as_ptr();
|
||||||
|
assert!(control.poll().snapshot.is_none());
|
||||||
|
dispatch_event(
|
||||||
|
&control,
|
||||||
|
&mut state,
|
||||||
|
&queue,
|
||||||
|
ferese_shell_v1::Event::SnapshotEnd { serial: 1 },
|
||||||
|
);
|
||||||
|
let first = control.poll().snapshot.unwrap();
|
||||||
|
assert_eq!(first.windows.as_ptr(), windows);
|
||||||
|
assert_eq!(first.windows[0].app_id.as_ptr(), app_id);
|
||||||
|
assert_eq!(first.windows[0].title.as_ptr(), title);
|
||||||
|
for (begin, end, id) in [(2, 99, 20), (3, 3, 30)] {
|
||||||
|
dispatch_event(
|
||||||
|
&control,
|
||||||
|
&mut state,
|
||||||
|
&queue,
|
||||||
|
ferese_shell_v1::Event::SnapshotBegin { serial: begin },
|
||||||
|
);
|
||||||
|
dispatch_event(&control, &mut state, &queue, window_event(id, 0));
|
||||||
|
assert!(control.poll().snapshot.is_none());
|
||||||
|
dispatch_event(
|
||||||
|
&control,
|
||||||
|
&mut state,
|
||||||
|
&queue,
|
||||||
|
ferese_shell_v1::Event::SnapshotEnd { serial: end },
|
||||||
|
);
|
||||||
|
let next = control.poll().snapshot;
|
||||||
|
if begin == end {
|
||||||
|
let next = next.unwrap();
|
||||||
|
assert_eq!(next.windows.len(), 1);
|
||||||
|
assert_eq!(next.windows[0].id, id);
|
||||||
|
} else {
|
||||||
|
assert!(next.is_none(), "mismatched serials must not publish");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert_eq!(first.windows.len(), 1);
|
||||||
|
assert_eq!(first.windows[0].id, 10);
|
||||||
|
assert_eq!(first.windows[0].app_id, "dev.ferese.Test");
|
||||||
|
assert_eq!(first.windows[0].title, "Same title");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn window_id_words_round_trip_without_truncation() {
|
fn window_id_words_round_trip_without_truncation() {
|
||||||
for (id, hi, lo) in [
|
for (id, hi, lo) in [
|
||||||
|
|||||||
Reference in new issue
Block a user