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
This commit is contained in:
parent
9be207f4c5
commit
72a291d808
|
|
@ -8,6 +8,7 @@ use crate::messages::prelude::*;
|
|||
pub struct Dispatcher {
|
||||
message_queues: Vec<VecDeque<Message>>,
|
||||
pub responses: Vec<FrontendMessage>,
|
||||
pub frontend_update_messages: Vec<Message>,
|
||||
pub message_handlers: DispatcherMessageHandlers,
|
||||
}
|
||||
|
||||
|
|
@ -42,19 +43,24 @@ 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::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
];
|
||||
/// 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,
|
||||
))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::UpdateDocumentWidgets),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderRulers)),
|
||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
||||
];
|
||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
|
||||
|
|
@ -105,6 +111,19 @@ 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 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);
|
||||
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;
|
||||
}
|
||||
}
|
||||
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 +147,9 @@ 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) => {
|
||||
|
|
|
|||
|
|
@ -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<WidgetDiff>) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ pub struct CheckboxInput {
|
|||
pub tooltip: String,
|
||||
|
||||
#[serde(rename = "forLabel")]
|
||||
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||
pub for_label: CheckboxId,
|
||||
|
||||
#[serde(skip)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<NodeId>,
|
||||
last_svg_canvas: Option<SurfaceFrame>,
|
||||
}
|
||||
|
||||
#[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#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
|
||||
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
||||
);
|
||||
self.last_svg_canvas = Some(frame);
|
||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||
}
|
||||
RenderOutputType::Texture { .. } => {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue