perf(ipc): cache unchanged output event payloads
This commit is contained in:
1 parent
835aadc3eb
commit
e9f36efdb6
6 files changed
+397
-16
No files matched your search
@@ -246,7 +246,7 @@ impl DirectBackendState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct ConnectedOutputInfo {
|
||||
pub connector: String,
|
||||
pub connected: bool,
|
||||
@@ -266,7 +266,7 @@ pub(crate) struct ConnectedOutputInfo {
|
||||
pub configured_position: Option<[i32; 2]>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct ConnectedModeInfo {
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
|
||||
+34
-7
@@ -14,6 +14,7 @@ use ferese_core::LayoutMode;
|
||||
use ferese_ipc::{Request, Response, VERSION};
|
||||
|
||||
pub(crate) mod activity_owner;
|
||||
pub(crate) mod outputs;
|
||||
mod transport;
|
||||
use ferese_layout::Direction;
|
||||
use serde_json::{Value, json};
|
||||
@@ -1620,15 +1621,41 @@ impl Ferese {
|
||||
|
||||
pub(crate) fn ipc_event_snapshot(&self) -> ferese_ipc::events::Snapshot {
|
||||
let locked = self.session_lock.active();
|
||||
let mut outputs = self.outputs_json();
|
||||
let outputs = if let Some(backend) = &self.direct_backend {
|
||||
let state = outputs::State {
|
||||
connected: &backend.connected_outputs,
|
||||
focused: self.output_workspaces.focused_output(),
|
||||
error: backend.output_configuration_error(),
|
||||
confirmation_pending: backend.confirmation_pending(),
|
||||
low_power: backend.low_power,
|
||||
};
|
||||
let presentations = backend.connected_outputs.iter().map(|info| {
|
||||
let mapped = self.output_by_identity(&info.identity);
|
||||
let id = self.persistent_output_id(&info.identity);
|
||||
outputs::Presentation {
|
||||
id,
|
||||
geometry: mapped.and_then(|output| self.space.output_geometry(output)),
|
||||
workspace: id.and_then(|id| self.output_workspaces.active_workspace(id)),
|
||||
scale: mapped
|
||||
.map(|output| output.current_scale().fractional_scale())
|
||||
.unwrap_or(info.scale),
|
||||
transform: mapped
|
||||
.map(|output| transform_name(output.current_transform()))
|
||||
.unwrap_or_else(|| configured_transform_name(info.transform)),
|
||||
}
|
||||
});
|
||||
self.ipc_events
|
||||
.outputs
|
||||
.borrow_mut()
|
||||
.get_or_update(state, presentations, locked, || self.outputs_json())
|
||||
} else {
|
||||
// Nested outputs have no connector inventory or DRM mode lists.
|
||||
let mut value = self.outputs_json();
|
||||
if locked {
|
||||
for output in outputs.as_array_mut().into_iter().flatten() {
|
||||
if let Some(fields) = output.as_object_mut() {
|
||||
fields.remove("workspace");
|
||||
fields.remove("focused");
|
||||
}
|
||||
}
|
||||
outputs::redact(&mut value);
|
||||
}
|
||||
Arc::new(value)
|
||||
};
|
||||
let windows = if locked {
|
||||
Vec::new()
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use ferese_core::{OutputId, WorkspaceId};
|
||||
use serde_json::Value;
|
||||
use smithay::utils::{Logical, Rectangle};
|
||||
|
||||
use crate::backends::direct::ConnectedOutputInfo;
|
||||
|
||||
/// Connector data and policy borrowed for an exact, allocation-free comparison.
|
||||
pub(crate) struct State<'a> {
|
||||
pub connected: &'a [ConnectedOutputInfo],
|
||||
pub focused: Option<OutputId>,
|
||||
pub error: Option<&'a str>,
|
||||
pub confirmation_pending: bool,
|
||||
pub low_power: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub(crate) struct Presentation {
|
||||
pub id: Option<OutputId>,
|
||||
pub geometry: Option<Rectangle<i32, Logical>>,
|
||||
pub workspace: Option<WorkspaceId>,
|
||||
pub scale: f64,
|
||||
pub transform: &'static str,
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
connected: Vec<ConnectedOutputInfo>,
|
||||
focused: Option<OutputId>,
|
||||
error: Option<String>,
|
||||
confirmation_pending: bool,
|
||||
low_power: bool,
|
||||
presentations: Vec<Presentation>,
|
||||
unlocked: Arc<Value>,
|
||||
locked: Option<Arc<Value>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct Cache {
|
||||
entry: Option<Entry>,
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub(crate) fn clear(&mut self) {
|
||||
self.entry = None;
|
||||
}
|
||||
|
||||
pub(crate) fn get_or_update(
|
||||
&mut self,
|
||||
state: State<'_>,
|
||||
presentations: impl Iterator<Item = Presentation> + Clone,
|
||||
locked: bool,
|
||||
build: impl FnOnce() -> Value,
|
||||
) -> Arc<Value> {
|
||||
let unchanged = self.entry.as_ref().is_some_and(|entry| {
|
||||
entry.connected == state.connected
|
||||
&& entry.focused == state.focused
|
||||
&& entry.error.as_deref() == state.error
|
||||
&& entry.confirmation_pending == state.confirmation_pending
|
||||
&& entry.low_power == state.low_power
|
||||
&& presentations
|
||||
.clone()
|
||||
.eq(entry.presentations.iter().copied())
|
||||
});
|
||||
if !unchanged {
|
||||
self.entry = Some(Entry {
|
||||
connected: state.connected.to_vec(),
|
||||
focused: state.focused,
|
||||
error: state.error.map(str::to_owned),
|
||||
confirmation_pending: state.confirmation_pending,
|
||||
low_power: state.low_power,
|
||||
presentations: presentations.collect(),
|
||||
unlocked: Arc::new(build()),
|
||||
locked: None,
|
||||
});
|
||||
}
|
||||
let entry = self.entry.as_mut().expect("output snapshot was populated");
|
||||
if locked {
|
||||
entry
|
||||
.locked
|
||||
.get_or_insert_with(|| {
|
||||
let mut value = entry.unlocked.as_ref().clone();
|
||||
redact(&mut value);
|
||||
Arc::new(value)
|
||||
})
|
||||
.clone()
|
||||
} else {
|
||||
entry.unlocked.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn redact(value: &mut Value) {
|
||||
for output in value.as_array_mut().into_iter().flatten() {
|
||||
if let Some(fields) = output.as_object_mut() {
|
||||
fields.remove("workspace");
|
||||
fields.remove("focused");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::backends::direct::ConnectedModeInfo;
|
||||
use crate::config::OutputTransform;
|
||||
use serde_json::json;
|
||||
|
||||
fn connector() -> ConnectedOutputInfo {
|
||||
let mode = ConnectedModeInfo {
|
||||
width: 3840,
|
||||
height: 2160,
|
||||
refresh_millihertz: 60_000,
|
||||
preferred: true,
|
||||
};
|
||||
ConnectedOutputInfo {
|
||||
connector: "HDMI-A-1".into(),
|
||||
connected: true,
|
||||
identity: "test-monitor".into(),
|
||||
internal: false,
|
||||
requested_enabled: true,
|
||||
mirror_source: None,
|
||||
enabled: true,
|
||||
profile: Some("desktop".into()),
|
||||
requested_profile: None,
|
||||
auto_refresh: true,
|
||||
physical_size: Some((600, 340)),
|
||||
current_mode: Some(mode),
|
||||
available_modes: vec![mode],
|
||||
scale: 1.75,
|
||||
transform: OutputTransform::Normal,
|
||||
configured_position: Some([0, 0]),
|
||||
}
|
||||
}
|
||||
|
||||
fn state(connected: &[ConnectedOutputInfo]) -> State<'_> {
|
||||
State {
|
||||
connected,
|
||||
focused: Some(OutputId(1)),
|
||||
error: None,
|
||||
confirmation_pending: false,
|
||||
low_power: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn presentation() -> Presentation {
|
||||
Presentation {
|
||||
id: Some(OutputId(1)),
|
||||
geometry: Some(Rectangle::new((0, 0).into(), (2195, 1235).into())),
|
||||
workspace: Some(WorkspaceId(3)),
|
||||
scale: 1.75,
|
||||
transform: "normal",
|
||||
}
|
||||
}
|
||||
|
||||
fn value() -> Value {
|
||||
json!([{"id": 1, "workspace": 3, "focused": true, "scale": 1.75}])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_outputs_reuse_storage_without_serializing_and_release_when_cleared() {
|
||||
let connected = [connector()];
|
||||
let mut cache = Cache::default();
|
||||
let first = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
value,
|
||||
);
|
||||
for _ in 0..1000 {
|
||||
let next = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
|| panic!("unchanged outputs must not rebuild JSON"),
|
||||
);
|
||||
assert!(Arc::ptr_eq(&first, &next));
|
||||
}
|
||||
cache.clear();
|
||||
let next = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
value,
|
||||
);
|
||||
assert!(!Arc::ptr_eq(&first, &next));
|
||||
assert_eq!(first, next);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lock_redaction_is_cached_without_mutating_public_or_older_snapshots() {
|
||||
let connected = [connector()];
|
||||
let mut cache = Cache::default();
|
||||
let public = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
value,
|
||||
);
|
||||
let locked = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
true,
|
||||
|| panic!("lock redaction must reuse connector data"),
|
||||
);
|
||||
assert_eq!(*locked, json!([{"id": 1, "scale": 1.75}]));
|
||||
assert_eq!(*public, value());
|
||||
let again = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
true,
|
||||
|| panic!("repeated locked snapshots must not rebuild"),
|
||||
);
|
||||
assert!(Arc::ptr_eq(&locked, &again));
|
||||
let unlocked = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
|| panic!("unlock must reuse public data"),
|
||||
);
|
||||
assert!(Arc::ptr_eq(&public, &unlocked));
|
||||
let changed = cache.get_or_update(state(&[]), [].into_iter(), true, || json!([]));
|
||||
assert_eq!(*changed, json!([]));
|
||||
assert_eq!(*locked, json!([{"id": 1, "scale": 1.75}]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connector_inventory_and_every_reported_property_invalidate_the_snapshot() {
|
||||
let changes: &[fn(&mut ConnectedOutputInfo)] = &[
|
||||
|info| info.connector.push('2'),
|
||||
|info| info.connected = false,
|
||||
|info| info.identity.push('2'),
|
||||
|info| info.internal = true,
|
||||
|info| info.requested_enabled = false,
|
||||
|info| info.mirror_source = Some("DP-1".into()),
|
||||
|info| info.enabled = false,
|
||||
|info| info.profile = None,
|
||||
|info| info.requested_profile = Some("mobile".into()),
|
||||
|info| info.auto_refresh = false,
|
||||
|info| info.physical_size = None,
|
||||
|info| info.current_mode = None,
|
||||
|info| info.available_modes[0].width = 1920,
|
||||
|info| info.available_modes[0].height = 1080,
|
||||
|info| info.available_modes[0].refresh_millihertz = 120_000,
|
||||
|info| info.available_modes[0].preferred = false,
|
||||
|info| info.available_modes.clear(),
|
||||
|info| info.scale = 1.5,
|
||||
|info| info.transform = OutputTransform::Rotate90,
|
||||
|info| info.configured_position = None,
|
||||
];
|
||||
for (index, change) in changes.iter().enumerate() {
|
||||
let mut cache = Cache::default();
|
||||
let mut connected = [connector()];
|
||||
let first = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
value,
|
||||
);
|
||||
change(&mut connected[0]);
|
||||
let next = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
|| json!({"changed": index}),
|
||||
);
|
||||
assert_eq!(*next, json!({"changed": index}));
|
||||
assert!(!Arc::ptr_eq(&first, &next));
|
||||
}
|
||||
let mut cache = Cache::default();
|
||||
let first = cache.get_or_update(
|
||||
state(&[connector()]),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
value,
|
||||
);
|
||||
let next = cache.get_or_update(state(&[]), [].into_iter(), false, || json!([]));
|
||||
assert_eq!(*next, json!([]));
|
||||
assert_eq!(*first, value());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_workspace_geometry_and_output_policy_changes_refresh_the_snapshot() {
|
||||
let connected = [connector()];
|
||||
for index in 0..10 {
|
||||
let mut cache = Cache::default();
|
||||
let first = cache.get_or_update(
|
||||
state(&connected),
|
||||
[presentation()].into_iter(),
|
||||
false,
|
||||
value,
|
||||
);
|
||||
let mut state = state(&connected);
|
||||
let mut presentation = presentation();
|
||||
match index {
|
||||
0 => state.focused = None,
|
||||
1 => state.error = Some("output rejected"),
|
||||
2 => state.confirmation_pending = true,
|
||||
3 => state.low_power = true,
|
||||
4 => presentation.id = None,
|
||||
5 => presentation.geometry = None,
|
||||
6 => presentation.geometry.as_mut().unwrap().loc.x += 100,
|
||||
7 => presentation.workspace = Some(WorkspaceId(4)),
|
||||
8 => presentation.scale = 1.25,
|
||||
9 => presentation.transform = "rotate_90",
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let next = cache.get_or_update(
|
||||
state,
|
||||
[presentation].into_iter(),
|
||||
false,
|
||||
|| json!({"changed": index}),
|
||||
);
|
||||
assert_eq!(*next, json!({"changed": index}));
|
||||
assert!(!Arc::ptr_eq(&first, &next));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::sync::mpsc::SyncSender;
|
||||
@@ -18,6 +19,7 @@ pub(crate) struct Subscribers {
|
||||
previous: Option<Snapshot>,
|
||||
generation: u64,
|
||||
pub(crate) config_revision: u64,
|
||||
pub(crate) outputs: RefCell<crate::ipc::outputs::Cache>,
|
||||
}
|
||||
|
||||
impl Subscribers {
|
||||
@@ -43,6 +45,7 @@ impl Subscribers {
|
||||
self.subscribers.remove(&owner);
|
||||
if self.subscribers.is_empty() {
|
||||
self.previous = None;
|
||||
self.outputs.get_mut().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +124,9 @@ impl Subscribers {
|
||||
delivered
|
||||
});
|
||||
self.previous = self.active().then_some(snapshot);
|
||||
if !self.active() {
|
||||
self.outputs.get_mut().clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,7 +252,7 @@ mod tests {
|
||||
|
||||
fn snapshot() -> Snapshot {
|
||||
Snapshot {
|
||||
outputs: json!([]),
|
||||
outputs: json!([]).into(),
|
||||
workspaces: json!([]),
|
||||
windows: Vec::new(),
|
||||
focus: json!(null),
|
||||
@@ -272,7 +278,7 @@ mod tests {
|
||||
));
|
||||
hub.publish(state.clone());
|
||||
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
|
||||
state.outputs = json!([{"id": 2}]);
|
||||
state.outputs = json!([{"id": 2}]).into();
|
||||
state.workspaces = json!([{"output": 2}]);
|
||||
state.focus = json!({"output": 2});
|
||||
hub.publish(state);
|
||||
@@ -356,7 +362,7 @@ mod tests {
|
||||
let mut state = snapshot();
|
||||
hub.publish(state.clone());
|
||||
receive.try_recv().unwrap();
|
||||
state.outputs = json!([{"id": 1}]);
|
||||
state.outputs = json!([{"id": 1}]).into();
|
||||
state.workspaces = json!([{"output": 1}]);
|
||||
state.windows = vec![ferese_ipc::events::Window {
|
||||
id: 7,
|
||||
|
||||
@@ -33,7 +33,7 @@ pub struct Window {
|
||||
/// A coherent logical desktop view. Desktop objects are redacted while locked.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||
pub struct Snapshot {
|
||||
pub outputs: Value,
|
||||
pub outputs: Arc<Value>,
|
||||
pub workspaces: Value,
|
||||
pub windows: Vec<Window>,
|
||||
pub focus: Value,
|
||||
@@ -80,9 +80,9 @@ impl Snapshot {
|
||||
#[must_use]
|
||||
pub fn changes_since(&self, previous: &Self) -> Vec<Change> {
|
||||
let mut changes = Vec::new();
|
||||
if self.outputs != previous.outputs {
|
||||
if !Arc::ptr_eq(&self.outputs, &previous.outputs) && self.outputs != previous.outputs {
|
||||
changes.push(Change::OutputsChanged {
|
||||
outputs: self.outputs.clone(),
|
||||
outputs: self.outputs.as_ref().clone(),
|
||||
});
|
||||
}
|
||||
if self.workspaces != previous.workspaces {
|
||||
@@ -123,6 +123,36 @@ impl Snapshot {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn shared_outputs_preserve_wire_fields_and_detect_value_changes() {
|
||||
let outputs = serde_json::json!([{
|
||||
"id": 1, "connector": "HDMI-A-1", "scale": 1.75,
|
||||
"available_modes": [{"width": 3840, "height": 2160, "preferred": true}],
|
||||
"configuration_error": null
|
||||
}]);
|
||||
let wire = serde_json::json!({
|
||||
"outputs": outputs, "workspaces": [], "windows": [], "focus": null,
|
||||
"config": {"revision": 0, "error": null}, "theme": null,
|
||||
"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.outputs, &next.outputs));
|
||||
assert!(next.changes_since(&snapshot).is_empty());
|
||||
next.outputs = Arc::new(outputs.clone());
|
||||
assert!(next.changes_since(&snapshot).is_empty());
|
||||
next.outputs = serde_json::json!([]).into();
|
||||
let changes = next.changes_since(&snapshot);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&changes).unwrap(),
|
||||
serde_json::json!([
|
||||
{"type": "outputs_changed", "outputs": []}
|
||||
])
|
||||
);
|
||||
assert_eq!(*snapshot.outputs, outputs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_windows_preserve_snapshot_and_replacement_wire_fields() {
|
||||
let windows = serde_json::json!([{
|
||||
|
||||
@@ -527,7 +527,7 @@ mod tests {
|
||||
last: true,
|
||||
change: Change::Snapshot {
|
||||
desktop: Box::new(ferese_ipc::events::Snapshot {
|
||||
outputs: serde_json::json!([]),
|
||||
outputs: serde_json::json!([]).into(),
|
||||
workspaces: serde_json::json!([]),
|
||||
windows: vec![window(1, "one.desktop", true, 1)],
|
||||
focus: serde_json::json!({"window":1}),
|
||||
|
||||
Reference in new issue
Block a user