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(crate) struct ConnectedOutputInfo {
|
||||||
pub connector: String,
|
pub connector: String,
|
||||||
pub connected: bool,
|
pub connected: bool,
|
||||||
@@ -266,7 +266,7 @@ pub(crate) struct ConnectedOutputInfo {
|
|||||||
pub configured_position: Option<[i32; 2]>,
|
pub configured_position: Option<[i32; 2]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
pub(crate) struct ConnectedModeInfo {
|
pub(crate) struct ConnectedModeInfo {
|
||||||
pub width: u16,
|
pub width: u16,
|
||||||
pub height: u16,
|
pub height: u16,
|
||||||
|
|||||||
+34
-7
@@ -14,6 +14,7 @@ use ferese_core::LayoutMode;
|
|||||||
use ferese_ipc::{Request, Response, VERSION};
|
use ferese_ipc::{Request, Response, VERSION};
|
||||||
|
|
||||||
pub(crate) mod activity_owner;
|
pub(crate) mod activity_owner;
|
||||||
|
pub(crate) mod outputs;
|
||||||
mod transport;
|
mod transport;
|
||||||
use ferese_layout::Direction;
|
use ferese_layout::Direction;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
@@ -1620,15 +1621,41 @@ impl Ferese {
|
|||||||
|
|
||||||
pub(crate) fn ipc_event_snapshot(&self) -> ferese_ipc::events::Snapshot {
|
pub(crate) fn ipc_event_snapshot(&self) -> ferese_ipc::events::Snapshot {
|
||||||
let locked = self.session_lock.active();
|
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 {
|
if locked {
|
||||||
for output in outputs.as_array_mut().into_iter().flatten() {
|
outputs::redact(&mut value);
|
||||||
if let Some(fields) = output.as_object_mut() {
|
|
||||||
fields.remove("workspace");
|
|
||||||
fields.remove("focused");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Arc::new(value)
|
||||||
|
};
|
||||||
let windows = if locked {
|
let windows = if locked {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
} else {
|
} 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::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::mpsc::SyncSender;
|
use std::sync::mpsc::SyncSender;
|
||||||
@@ -18,6 +19,7 @@ pub(crate) struct Subscribers {
|
|||||||
previous: Option<Snapshot>,
|
previous: Option<Snapshot>,
|
||||||
generation: u64,
|
generation: u64,
|
||||||
pub(crate) config_revision: u64,
|
pub(crate) config_revision: u64,
|
||||||
|
pub(crate) outputs: RefCell<crate::ipc::outputs::Cache>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Subscribers {
|
impl Subscribers {
|
||||||
@@ -43,6 +45,7 @@ impl Subscribers {
|
|||||||
self.subscribers.remove(&owner);
|
self.subscribers.remove(&owner);
|
||||||
if self.subscribers.is_empty() {
|
if self.subscribers.is_empty() {
|
||||||
self.previous = None;
|
self.previous = None;
|
||||||
|
self.outputs.get_mut().clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +124,9 @@ impl Subscribers {
|
|||||||
delivered
|
delivered
|
||||||
});
|
});
|
||||||
self.previous = self.active().then_some(snapshot);
|
self.previous = self.active().then_some(snapshot);
|
||||||
|
if !self.active() {
|
||||||
|
self.outputs.get_mut().clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,7 +252,7 @@ mod tests {
|
|||||||
|
|
||||||
fn snapshot() -> Snapshot {
|
fn snapshot() -> Snapshot {
|
||||||
Snapshot {
|
Snapshot {
|
||||||
outputs: json!([]),
|
outputs: json!([]).into(),
|
||||||
workspaces: json!([]),
|
workspaces: json!([]),
|
||||||
windows: Vec::new(),
|
windows: Vec::new(),
|
||||||
focus: json!(null),
|
focus: json!(null),
|
||||||
@@ -272,7 +278,7 @@ mod tests {
|
|||||||
));
|
));
|
||||||
hub.publish(state.clone());
|
hub.publish(state.clone());
|
||||||
assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty)));
|
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.workspaces = json!([{"output": 2}]);
|
||||||
state.focus = json!({"output": 2});
|
state.focus = json!({"output": 2});
|
||||||
hub.publish(state);
|
hub.publish(state);
|
||||||
@@ -356,7 +362,7 @@ mod tests {
|
|||||||
let mut state = snapshot();
|
let mut state = snapshot();
|
||||||
hub.publish(state.clone());
|
hub.publish(state.clone());
|
||||||
receive.try_recv().unwrap();
|
receive.try_recv().unwrap();
|
||||||
state.outputs = json!([{"id": 1}]);
|
state.outputs = json!([{"id": 1}]).into();
|
||||||
state.workspaces = json!([{"output": 1}]);
|
state.workspaces = json!([{"output": 1}]);
|
||||||
state.windows = vec![ferese_ipc::events::Window {
|
state.windows = vec![ferese_ipc::events::Window {
|
||||||
id: 7,
|
id: 7,
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ pub struct Window {
|
|||||||
/// 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: Arc<Value>,
|
||||||
pub workspaces: Value,
|
pub workspaces: Value,
|
||||||
pub windows: Vec<Window>,
|
pub windows: Vec<Window>,
|
||||||
pub focus: Value,
|
pub focus: Value,
|
||||||
@@ -80,9 +80,9 @@ impl Snapshot {
|
|||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn changes_since(&self, previous: &Self) -> Vec<Change> {
|
pub fn changes_since(&self, previous: &Self) -> Vec<Change> {
|
||||||
let mut changes = Vec::new();
|
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 {
|
changes.push(Change::OutputsChanged {
|
||||||
outputs: self.outputs.clone(),
|
outputs: self.outputs.as_ref().clone(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if self.workspaces != previous.workspaces {
|
if self.workspaces != previous.workspaces {
|
||||||
@@ -123,6 +123,36 @@ impl Snapshot {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
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]
|
#[test]
|
||||||
fn typed_windows_preserve_snapshot_and_replacement_wire_fields() {
|
fn typed_windows_preserve_snapshot_and_replacement_wire_fields() {
|
||||||
let windows = serde_json::json!([{
|
let windows = serde_json::json!([{
|
||||||
|
|||||||
@@ -527,7 +527,7 @@ mod tests {
|
|||||||
last: true,
|
last: true,
|
||||||
change: Change::Snapshot {
|
change: Change::Snapshot {
|
||||||
desktop: Box::new(ferese_ipc::events::Snapshot {
|
desktop: Box::new(ferese_ipc::events::Snapshot {
|
||||||
outputs: serde_json::json!([]),
|
outputs: serde_json::json!([]).into(),
|
||||||
workspaces: serde_json::json!([]),
|
workspaces: serde_json::json!([]),
|
||||||
windows: vec![window(1, "one.desktop", true, 1)],
|
windows: vec![window(1, "one.desktop", true, 1)],
|
||||||
focus: serde_json::json!({"window":1}),
|
focus: serde_json::json!({"window":1}),
|
||||||
|
|||||||
Reference in new issue
Block a user