Skip to content

Commit 72a291d

Browse files
authored
Improve web frontend performance during zooming and panning (#3337)
* WIP debounce frontend ui updates * Reduce the number of frontend updates performed * Improve menu bar diffing * Cleanup in dispatcher * Fix comment
1 parent 9be207f commit 72a291d

File tree

6 files changed

+72
-10
lines changed

6 files changed

+72
-10
lines changed

editor/src/dispatcher.rs

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use crate::messages::prelude::*;
88
pub struct Dispatcher {
99
message_queues: Vec<VecDeque<Message>>,
1010
pub responses: Vec<FrontendMessage>,
11+
pub frontend_update_messages: Vec<Message>,
1112
pub message_handlers: DispatcherMessageHandlers,
1213
}
1314

@@ -42,19 +43,24 @@ impl DispatcherMessageHandlers {
4243
/// The last occurrence of the message in the message queue is sufficient to ensure correct behavior.
4344
/// In addition, these messages do not change any state in the backend (aside from caches).
4445
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
45-
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
46-
PropertiesPanelMessageDiscriminant::Refresh,
47-
))),
4846
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::DocumentStructureChanged)),
49-
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
5047
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(
5148
NodeGraphMessageDiscriminant::RunDocumentGraph,
5249
))),
5350
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
51+
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
52+
];
53+
/// Since we don't need to update the frontend multiple times per frame,
54+
/// we have a set of messages which we will buffer until the next frame is requested.
55+
const FRONTEND_UPDATE_MESSAGES: &[MessageDiscriminant] = &[
56+
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::PropertiesPanel(
57+
PropertiesPanelMessageDiscriminant::Refresh,
58+
))),
59+
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::UpdateDocumentWidgets),
60+
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
5461
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)),
5562
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
5663
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
57-
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
5864
];
5965
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
6066
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
@@ -105,6 +111,19 @@ impl Dispatcher {
105111

106112
while let Some(message) = self.message_queues.last_mut().and_then(VecDeque::pop_front) {
107113
// Skip processing of this message if it will be processed later (at the end of the shallowest level queue)
114+
if FRONTEND_UPDATE_MESSAGES.contains(&message.to_discriminant()) {
115+
let already_in_queue = self.message_queues.first().is_some_and(|queue| queue.contains(&message));
116+
if already_in_queue {
117+
self.cleanup_queues(false);
118+
continue;
119+
} else if self.message_queues.len() > 1 {
120+
if !self.frontend_update_messages.contains(&message) {
121+
self.frontend_update_messages.push(message);
122+
}
123+
self.cleanup_queues(false);
124+
continue;
125+
}
126+
}
108127
if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) {
109128
let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some();
110129
if already_in_queue {
@@ -128,6 +147,9 @@ impl Dispatcher {
128147
// Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend
129148
match message {
130149
Message::Animation(message) => {
150+
if let AnimationMessage::IncrementFrameCounter = &message {
151+
self.message_queues[0].extend(self.frontend_update_messages.drain(..));
152+
}
131153
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
132154
}
133155
Message::AppWindow(message) => {

editor/src/messages/layout/utility_types/layout_widget.rs

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,13 +484,19 @@ impl LayoutGroup {
484484
}
485485
}
486486

487-
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)]
487+
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, specta::Type)]
488488
pub struct WidgetHolder {
489489
#[serde(rename = "widgetId")]
490490
pub widget_id: WidgetId,
491491
pub widget: Widget,
492492
}
493493

494+
impl PartialEq for WidgetHolder {
495+
fn eq(&self, other: &Self) -> bool {
496+
self.widget == other.widget
497+
}
498+
}
499+
494500
impl WidgetHolder {
495501
#[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_holder()")]
496502
pub fn new(widget: Widget) -> Self {
@@ -502,6 +508,26 @@ impl WidgetHolder {
502508

503509
/// Diffing updates self (where self is old) based on new, updating the list of modifications as it does so.
504510
pub fn diff(&mut self, new: Self, widget_path: &mut [usize], widget_diffs: &mut Vec<WidgetDiff>) {
511+
if let (Widget::PopoverButton(button1), Widget::PopoverButton(button2)) = (&mut self.widget, &new.widget) {
512+
if button1.disabled == button2.disabled
513+
&& button1.style == button2.style
514+
&& button1.menu_direction == button2.menu_direction
515+
&& button1.icon == button2.icon
516+
&& button1.tooltip == button2.tooltip
517+
&& button1.tooltip_shortcut == button2.tooltip_shortcut
518+
&& button1.popover_min_width == button2.popover_min_width
519+
{
520+
let mut new_widget_path = widget_path.to_vec();
521+
for (i, (a, b)) in button1.popover_layout.iter_mut().zip(button2.popover_layout.iter()).enumerate() {
522+
new_widget_path.push(i);
523+
a.diff(b.clone(), &mut new_widget_path, widget_diffs);
524+
new_widget_path.pop();
525+
}
526+
self.widget = new.widget;
527+
return;
528+
}
529+
}
530+
505531
// If there have been changes to the actual widget (not just the id)
506532
if self.widget != new.widget {
507533
// We should update to the new widget value as well as a new widget id

editor/src/messages/layout/utility_types/widgets/input_widgets.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ pub struct CheckboxInput {
1919
pub tooltip: String,
2020

2121
#[serde(rename = "forLabel")]
22+
#[derivative(Debug = "ignore", PartialEq = "ignore")]
2223
pub for_label: CheckboxId,
2324

2425
#[serde(skip)]

editor/src/messages/layout/utility_types/widgets/label_widgets.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ pub enum SeparatorType {
3636
Section,
3737
}
3838

39-
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, PartialEq, Eq, Default, WidgetBuilder, specta::Type)]
39+
#[derive(Clone, serde::Serialize, serde::Deserialize, Derivative, Debug, Eq, Default, WidgetBuilder, specta::Type)]
40+
#[derivative(PartialEq)]
4041
pub struct TextLabel {
4142
pub disabled: bool,
4243

@@ -62,6 +63,7 @@ pub struct TextLabel {
6263
pub tooltip: String,
6364

6465
#[serde(rename = "forCheckbox")]
66+
#[derivative(PartialEq = "ignore")]
6567
pub for_checkbox: CheckboxId,
6668

6769
// Body

editor/src/node_graph_executor.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use graph_craft::document::value::{RenderOutput, TaggedValue};
55
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
66
use graph_craft::proto::GraphErrors;
77
use graph_craft::wasm_application_io::EditorPreferences;
8-
use graphene_std::application_io::TimingInformation;
98
use graphene_std::application_io::{NodeGraphUpdateMessage, RenderConfig};
9+
use graphene_std::application_io::{SurfaceFrame, TimingInformation};
1010
use graphene_std::renderer::{RenderMetadata, format_transform_matrix};
1111
use graphene_std::text::FontCache;
1212
use graphene_std::transform::Footprint;
@@ -54,6 +54,7 @@ pub struct NodeGraphExecutor {
5454
futures: VecDeque<(u64, ExecutionContext)>,
5555
node_graph_hash: u64,
5656
previous_node_to_inspect: Option<NodeId>,
57+
last_svg_canvas: Option<SurfaceFrame>,
5758
}
5859

5960
#[derive(Debug, Clone)]
@@ -76,6 +77,7 @@ impl NodeGraphExecutor {
7677
node_graph_hash: 0,
7778
current_execution_id: 0,
7879
previous_node_to_inspect: None,
80+
last_svg_canvas: None,
7981
};
8082
(node_runtime, node_executor)
8183
}
@@ -413,14 +415,19 @@ impl NodeGraphExecutor {
413415
// Send to frontend
414416
responses.add(FrontendMessage::UpdateImageData { image_data });
415417
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
418+
self.last_svg_canvas = None;
416419
}
417-
RenderOutputType::CanvasFrame(frame) => {
420+
RenderOutputType::CanvasFrame(frame) => 'block: {
421+
if self.last_svg_canvas == Some(frame) {
422+
break 'block;
423+
}
418424
let matrix = format_transform_matrix(frame.transform);
419425
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
420426
let svg = format!(
421427
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
422428
frame.resolution.x, frame.resolution.y, frame.surface_id.0
423429
);
430+
self.last_svg_canvas = Some(frame);
424431
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
425432
}
426433
RenderOutputType::Texture { .. } => {}

frontend/wasm/src/editor_api.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1045,8 +1045,12 @@ async fn poll_node_graph_evaluation() {
10451045
crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst);
10461046
}
10471047

1048+
// Batch responses to pool frontend updates
1049+
let batched = Message::Batched {
1050+
messages: messages.into_iter().collect(),
1051+
};
10481052
// Send each `FrontendMessage` to the JavaScript frontend
1049-
for response in messages.into_iter().flat_map(|message| editor.handle_message(message)) {
1053+
for response in editor.handle_message(batched) {
10501054
handle.send_frontend_message_to_js(response);
10511055
}
10521056

0 commit comments

Comments
 (0)