From 1a519e5d6e0c8fe9c056597603f3462ec267088f Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sun, 2 Nov 2025 09:03:51 +0100 Subject: [PATCH 1/5] WIP debounce frontend ui updates --- editor/src/dispatcher.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index ba970c451d..da567e9349 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -8,6 +8,8 @@ use crate::messages::prelude::*; pub struct Dispatcher { message_queues: Vec>, pub responses: Vec, + pub frontend_update_messages: Vec, + pub queue_frontend_updates: bool, pub message_handlers: DispatcherMessageHandlers, } @@ -56,12 +58,24 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), ]; +/// For optimization, these are messages guaranteed to be redundant when repeated. +/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. +/// In addition, these messages do not change any state in the backend (aside from caches). +const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[ + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( + PropertiesPanelMessageDiscriminant::Refresh, + ))), + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))), + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)), + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)), +]; const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)), MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter), ]; // TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best. -const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; +const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &[]; +// "PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; impl Dispatcher { pub fn new() -> Self { @@ -105,6 +119,12 @@ impl Dispatcher { while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) { // Skip processing of this message if it will be processed later (at the end of the shallowest level queue) + if self.queue_frontend_updates && FRONTEND_UPDATE_MESSAGES.contains(&message.to_discriminant()) { + if !self.frontend_update_messages.contains(&message) { + self.frontend_update_messages.push(message); + } + continue; + } if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) { let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some(); if already_in_queue { @@ -128,6 +148,10 @@ impl Dispatcher { // Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend match message { Message::Animation(message) => { + if let AnimationMessage::IncrementFrameCounter = &message { + self.message_queues[0].extend(self.frontend_update_messages.drain(..)); + } + self.message_handlers.animation_message_handler.process_message(message, &mut queue, ()); } Message::AppWindow(message) => { @@ -278,7 +302,11 @@ impl Dispatcher { } pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses) + let result = self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses); + if !responses.is_empty() && result.is_ok() { + self.queue_frontend_updates = true; + } + result } /// Create the tree structure for logging the messages as a tree From 69fe75e164fad27fd2721f2f742c997c20f2974c Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 3 Nov 2025 10:08:15 +0100 Subject: [PATCH 2/5] Reduce the number of frontend updates performed --- editor/src/dispatcher.rs | 24 +++++++++++++++++++----- editor/src/node_graph_executor.rs | 11 +++++++++-- frontend/wasm/src/editor_api.rs | 6 +++++- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index da567e9349..aa2d7ce142 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -65,6 +65,7 @@ const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( PropertiesPanelMessageDiscriminant::Refresh, ))), + MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::UpdateDocumentWidgets), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)), @@ -120,10 +121,17 @@ impl Dispatcher { while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) { // Skip processing of this message if it will be processed later (at the end of the shallowest level queue) if self.queue_frontend_updates && FRONTEND_UPDATE_MESSAGES.contains(&message.to_discriminant()) { - if !self.frontend_update_messages.contains(&message) { - self.frontend_update_messages.push(message); + let already_in_queue = self.message_queues.first().is_some_and(|queue| queue.contains(&message)); + if already_in_queue { + self.cleanup_queues(false); + continue; + } else if self.message_queues.len() > 1 { + if !self.frontend_update_messages.contains(&message) { + self.frontend_update_messages.push(message); + } + self.cleanup_queues(false); + continue; } - continue; } if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) { let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some(); @@ -148,11 +156,16 @@ impl Dispatcher { // Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend match message { Message::Animation(message) => { + self.message_handlers.animation_message_handler.process_message(message.clone(), &mut queue, ()); + if let AnimationMessage::IncrementFrameCounter = &message { + // self.queue_frontend_updates = false; + // log::debug!("dispatching {:?}", self.frontend_update_messages); + + self.cleanup_queues(true); self.message_queues[0].extend(self.frontend_update_messages.drain(..)); + // self.queue_frontend_updates = true; } - - self.message_handlers.animation_message_handler.process_message(message, &mut queue, ()); } Message::AppWindow(message) => { self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ()); @@ -331,6 +344,7 @@ impl Dispatcher { if !is_blocked { match message_logging_verbosity { MessageLoggingVerbosity::Off => {} + // MessageLoggingVerbosity::Off | MessageLoggingVerbosity::Names => { info!("{}{:?}", Self::create_indents(queues), message.to_discriminant()); } diff --git a/editor/src/node_graph_executor.rs b/editor/src/node_graph_executor.rs index 7889e4e540..fc0e299082 100644 --- a/editor/src/node_graph_executor.rs +++ b/editor/src/node_graph_executor.rs @@ -5,8 +5,8 @@ use graph_craft::document::value::{RenderOutput, TaggedValue}; use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput}; use graph_craft::proto::GraphErrors; use graph_craft::wasm_application_io::EditorPreferences; -use graphene_std::application_io::TimingInformation; use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig}; +use graphene_std::application_io::{SurfaceFrame, TimingInformation}; use graphene_std::renderer::{RenderMetadata, format_transform_matrix}; use graphene_std::text::FontCache; use graphene_std::transform::Footprint; @@ -54,6 +54,7 @@ pub struct NodeGraphExecutor { futures: VecDeque<(u64, ExecutionContext)>, node_graph_hash: u64, previous_node_to_inspect: Option, + last_svg_canvas: Option, } #[derive(Debug, Clone)] @@ -76,6 +77,7 @@ impl NodeGraphExecutor { node_graph_hash: 0, current_execution_id: 0, previous_node_to_inspect: None, + last_svg_canvas: None, }; (node_runtime, node_executor) } @@ -413,14 +415,19 @@ impl NodeGraphExecutor { // Send to frontend responses.add(FrontendMessage::UpdateImageData { image_data }); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); + self.last_svg_canvas = None; } - RenderOutputType::CanvasFrame(frame) => { + RenderOutputType::CanvasFrame(frame) => 'block: { + if self.last_svg_canvas == Some(frame) { + break 'block; + } let matrix = format_transform_matrix(frame.transform); let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") }; let svg = format!( r#"
"#, frame.resolution.x, frame.resolution.y, frame.surface_id.0 ); + self.last_svg_canvas = Some(frame); responses.add(FrontendMessage::UpdateDocumentArtwork { svg }); } RenderOutputType::Texture { .. } => {} diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 66ef5e82bc..e66f43ac34 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -1045,8 +1045,12 @@ async fn poll_node_graph_evaluation() { crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst); } + // Batch responses to pool frontend updates + let batched = Message::Batched { + messages: messages.into_iter().collect(), + }; // Send each `FrontendMessage` to the JavaScript frontend - for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) { + for response in editor.handle_message(batched) { handle.send_frontend_message_to_js(response); } From 2fdcb5babe38905f7b86898e1b8ff695bc2bac38 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Mon, 3 Nov 2025 22:53:18 +0100 Subject: [PATCH 3/5] Improve menu bar diffing --- .../layout/utility_types/layout_widget.rs | 28 ++++++++++++++++++- .../utility_types/widgets/input_widgets.rs | 1 + .../utility_types/widgets/label_widgets.rs | 4 ++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/editor/src/messages/layout/utility_types/layout_widget.rs b/editor/src/messages/layout/utility_types/layout_widget.rs index fa2eebf0ad..a24c47bf63 100644 --- a/editor/src/messages/layout/utility_types/layout_widget.rs +++ b/editor/src/messages/layout/utility_types/layout_widget.rs @@ -484,13 +484,19 @@ impl LayoutGroup { } } -#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)] pub struct WidgetHolder { #[serde(rename = "widgetId")] pub widget_id: WidgetId, pub widget: Widget, } +impl PartialEq for WidgetHolder { + fn eq(&self, other: &Self) -> bool { + self.widget == other.widget + } +} + impl WidgetHolder { #[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_holder()")] pub fn new(widget: Widget) -> Self { @@ -502,6 +508,26 @@ impl WidgetHolder { /// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so. pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec) { + if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut self.widget, &new.widget) { + if button1.disabled == button2.disabled + && button1.style == button2.style + && button1.menu_direction == button2.menu_direction + && button1.icon == button2.icon + && button1.tooltip == button2.tooltip + && button1.tooltip_shortcut == button2.tooltip_shortcut + && button1.popover_min_width == button2.popover_min_width + { + let mut new_widget_path = widget_path.to_vec(); + for (i, (a, b)) in button1.popover_layout.iter_mut().zip(button2.popover_layout.iter()).enumerate() { + new_widget_path.push(i); + a.diff(b.clone(), &mut new_widget_path, widget_diffs); + new_widget_path.pop(); + } + self.widget = new.widget; + return; + } + } + // If there have been changes to the actual widget (not just the id) if self.widget != new.widget { // We should update to the new widget value as well as a new widget id diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index eaf9efb47a..77ec19fff7 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -19,6 +19,7 @@ pub struct CheckboxInput { pub tooltip: String, #[serde(rename = "forLabel")] + #[derivative(Debug = "ignore", PartialEq = "ignore")] pub for_label: CheckboxId, #[serde(skip)] diff --git a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs index 876cd8b572..2a102d7cbf 100644 --- a/editor/src/messages/layout/utility_types/widgets/label_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/label_widgets.rs @@ -36,7 +36,8 @@ pub enum SeparatorType { Section, } -#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, PartialEq, Eq, Default, WidgetBuilder, specta::Type)] +#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Eq, Default, WidgetBuilder, specta::Type)] +#[derivative(PartialEq)] pub struct TextLabel { pub disabled: bool, @@ -62,6 +63,7 @@ pub struct TextLabel { pub tooltip: String, #[serde(rename = "forCheckbox")] + #[derivative(PartialEq = "ignore")] pub for_checkbox: CheckboxId, // Body From 8943803d68ea5f8c31d9868c0a33e4db3ed6a8c3 Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 8 Nov 2025 13:45:05 +0100 Subject: [PATCH 4/5] Cleanup in dispatcher --- editor/src/dispatcher.rs | 29 +++++------------------------ 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index aa2d7ce142..c5c9566691 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -9,7 +9,6 @@ pub struct Dispatcher { message_queues: Vec>, pub responses: Vec, pub frontend_update_messages: Vec, - pub queue_frontend_updates: bool, pub message_handlers: DispatcherMessageHandlers, } @@ -44,18 +43,11 @@ impl DispatcherMessageHandlers { /// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. /// In addition, these messages do not change any state in the backend (aside from caches). const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ - MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( - PropertiesPanelMessageDiscriminant::Refresh, - ))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)), - MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph( NodeGraphMessageDiscriminant::RunDocumentGraph, ))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender), - MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)), - MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)), - MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), ]; /// For optimization, these are messages guaranteed to be redundant when repeated. @@ -69,14 +61,14 @@ const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)), MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)), + MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure), ]; const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[ MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)), MessageDiscriminant::Animation(AnimationMessageDiscriminant::IncrementFrameCounter), ]; // TODO: Find a way to combine these with the list above. We use strings for now since these are the standard variant names used by multiple messages. But having these also type-checked would be best. -const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &[]; -// "PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; +const DEBUG_MESSAGE_ENDING_BLOCK_LIST: &[&str] = &["PointerMove", "PointerOutsideViewport", "Overlays", "Draw", "CurrentTime", "Time"]; impl Dispatcher { pub fn new() -> Self { @@ -120,7 +112,7 @@ impl Dispatcher { while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) { // Skip processing of this message if it will be processed later (at the end of the shallowest level queue) - if self.queue_frontend_updates && FRONTEND_UPDATE_MESSAGES.contains(&message.to_discriminant()) { + if FRONTEND_UPDATE_MESSAGES.contains(&message.to_discriminant()) { let already_in_queue = self.message_queues.first().is_some_and(|queue| queue.contains(&message)); if already_in_queue { self.cleanup_queues(false); @@ -156,16 +148,10 @@ impl Dispatcher { // Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend match message { Message::Animation(message) => { - self.message_handlers.animation_message_handler.process_message(message.clone(), &mut queue, ()); - if let AnimationMessage::IncrementFrameCounter = &message { - // self.queue_frontend_updates = false; - // log::debug!("dispatching {:?}", self.frontend_update_messages); - - self.cleanup_queues(true); self.message_queues[0].extend(self.frontend_update_messages.drain(..)); - // self.queue_frontend_updates = true; } + self.message_handlers.animation_message_handler.process_message(message, &mut queue, ()); } Message::AppWindow(message) => { self.message_handlers.app_window_message_handler.process_message(message, &mut queue, ()); @@ -315,11 +301,7 @@ impl Dispatcher { } pub fn poll_node_graph_evaluation(&mut self, responses: &mut VecDeque) -> Result<(), String> { - let result = self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses); - if !responses.is_empty() && result.is_ok() { - self.queue_frontend_updates = true; - } - result + self.message_handlers.portfolio_message_handler.poll_node_graph_evaluation(responses) } /// Create the tree structure for logging the messages as a tree @@ -344,7 +326,6 @@ impl Dispatcher { if !is_blocked { match message_logging_verbosity { MessageLoggingVerbosity::Off => {} - // MessageLoggingVerbosity::Off | MessageLoggingVerbosity::Names => { info!("{}{:?}", Self::create_indents(queues), message.to_discriminant()); } From 25401af5d692303ea601bfd2f9a47833ecdf4b1d Mon Sep 17 00:00:00 2001 From: Dennis Kobert Date: Sat, 8 Nov 2025 13:48:28 +0100 Subject: [PATCH 5/5] Fix comment --- editor/src/dispatcher.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/editor/src/dispatcher.rs b/editor/src/dispatcher.rs index c5c9566691..4119eb0aa9 100644 --- a/editor/src/dispatcher.rs +++ b/editor/src/dispatcher.rs @@ -50,9 +50,8 @@ const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender), MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad), ]; -/// For optimization, these are messages guaranteed to be redundant when repeated. -/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior. -/// In addition, these messages do not change any state in the backend (aside from caches). +/// Since we don't need to update the frontend multiple times per frame, +/// we have a set of messages which we will buffer until the next frame is requested. const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[ MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel( PropertiesPanelMessageDiscriminant::Refresh,