mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-03 15:08:11 +08:00
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:
@@ -8,6 +8,7 @@ use crate::messages::prelude::*;
|
|||||||
pub struct Dispatcher {
|
pub struct Dispatcher {
|
||||||
message_queues: Vec<VecDeque<Message>>,
|
message_queues: Vec<VecDeque<Message>>,
|
||||||
pub responses: Vec<FrontendMessage>,
|
pub responses: Vec<FrontendMessage>,
|
||||||
|
pub frontend_update_messages: Vec<Message>,
|
||||||
pub message_handlers: DispatcherMessageHandlers,
|
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.
|
/// 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).
|
/// In addition, these messages do not change any state in the backend (aside from caches).
|
||||||
const SIDE_EFFECT_FREE_MESSAGES: &[MessageDiscriminant] = &[
|
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::DocumentStructureChanged)),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::Overlays(OverlaysMessageDiscriminant::Draw))),
|
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::NodeGraph(
|
||||||
NodeGraphMessageDiscriminant::RunDocumentGraph,
|
NodeGraphMessageDiscriminant::RunDocumentGraph,
|
||||||
))),
|
))),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::SubmitActiveGraphRender),
|
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::RenderRulers)),
|
||||||
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
|
MessageDiscriminant::Portfolio(PortfolioMessageDiscriminant::Document(DocumentMessageDiscriminant::RenderScrollbars)),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::UpdateDocumentLayerStructure),
|
||||||
MessageDiscriminant::Frontend(FrontendMessageDiscriminant::TriggerFontLoad),
|
|
||||||
];
|
];
|
||||||
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
const DEBUG_MESSAGE_BLOCK_LIST: &[MessageDiscriminant] = &[
|
||||||
MessageDiscriminant::Broadcast(BroadcastMessageDiscriminant::TriggerEvent(EventMessageDiscriminant::AnimationFrame)),
|
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) {
|
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)
|
// 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()) {
|
if SIDE_EFFECT_FREE_MESSAGES.contains(&message.to_discriminant()) {
|
||||||
let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some();
|
let already_in_queue = self.message_queues.first().filter(|queue| queue.contains(&message)).is_some();
|
||||||
if already_in_queue {
|
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
|
// Process the action by forwarding it to the relevant message handler, or saving the FrontendMessage to be sent to the frontend
|
||||||
match message {
|
match message {
|
||||||
Message::Animation(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, ());
|
self.message_handlers.animation_message_handler.process_message(message, &mut queue, ());
|
||||||
}
|
}
|
||||||
Message::AppWindow(message) => {
|
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 {
|
pub struct WidgetHolder {
|
||||||
#[serde(rename = "widgetId")]
|
#[serde(rename = "widgetId")]
|
||||||
pub widget_id: WidgetId,
|
pub widget_id: WidgetId,
|
||||||
pub widget: Widget,
|
pub widget: Widget,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for WidgetHolder {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.widget == other.widget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WidgetHolder {
|
impl WidgetHolder {
|
||||||
#[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_holder()")]
|
#[deprecated(since = "0.0.0", note = "Please use the builder pattern, e.g. TextLabel::new(\"hello\").widget_holder()")]
|
||||||
pub fn new(widget: Widget) -> Self {
|
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.
|
/// 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>) {
|
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 there have been changes to the actual widget (not just the id)
|
||||||
if self.widget != new.widget {
|
if self.widget != new.widget {
|
||||||
// We should update to the new widget value as well as a new widget id
|
// 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,
|
pub tooltip: String,
|
||||||
|
|
||||||
#[serde(rename = "forLabel")]
|
#[serde(rename = "forLabel")]
|
||||||
|
#[derivative(Debug = "ignore", PartialEq = "ignore")]
|
||||||
pub for_label: CheckboxId,
|
pub for_label: CheckboxId,
|
||||||
|
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ pub enum SeparatorType {
|
|||||||
Section,
|
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 struct TextLabel {
|
||||||
pub disabled: bool,
|
pub disabled: bool,
|
||||||
|
|
||||||
@@ -62,6 +63,7 @@ pub struct TextLabel {
|
|||||||
pub tooltip: String,
|
pub tooltip: String,
|
||||||
|
|
||||||
#[serde(rename = "forCheckbox")]
|
#[serde(rename = "forCheckbox")]
|
||||||
|
#[derivative(PartialEq = "ignore")]
|
||||||
pub for_checkbox: CheckboxId,
|
pub for_checkbox: CheckboxId,
|
||||||
|
|
||||||
// Body
|
// Body
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use graph_craft::document::value::{RenderOutput, TaggedValue};
|
|||||||
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
use graph_craft::document::{DocumentNode, DocumentNodeImplementation, NodeId, NodeInput};
|
||||||
use graph_craft::proto::GraphErrors;
|
use graph_craft::proto::GraphErrors;
|
||||||
use graph_craft::wasm_application_io::EditorPreferences;
|
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::{NodeGraphUpdateMessage, RenderConfig};
|
||||||
|
use graphene_std::application_io::{SurfaceFrame, TimingInformation};
|
||||||
use graphene_std::renderer::{RenderMetadata, format_transform_matrix};
|
use graphene_std::renderer::{RenderMetadata, format_transform_matrix};
|
||||||
use graphene_std::text::FontCache;
|
use graphene_std::text::FontCache;
|
||||||
use graphene_std::transform::Footprint;
|
use graphene_std::transform::Footprint;
|
||||||
@@ -54,6 +54,7 @@ pub struct NodeGraphExecutor {
|
|||||||
futures: VecDeque<(u64, ExecutionContext)>,
|
futures: VecDeque<(u64, ExecutionContext)>,
|
||||||
node_graph_hash: u64,
|
node_graph_hash: u64,
|
||||||
previous_node_to_inspect: Option<NodeId>,
|
previous_node_to_inspect: Option<NodeId>,
|
||||||
|
last_svg_canvas: Option<SurfaceFrame>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -76,6 +77,7 @@ impl NodeGraphExecutor {
|
|||||||
node_graph_hash: 0,
|
node_graph_hash: 0,
|
||||||
current_execution_id: 0,
|
current_execution_id: 0,
|
||||||
previous_node_to_inspect: None,
|
previous_node_to_inspect: None,
|
||||||
|
last_svg_canvas: None,
|
||||||
};
|
};
|
||||||
(node_runtime, node_executor)
|
(node_runtime, node_executor)
|
||||||
}
|
}
|
||||||
@@ -413,14 +415,19 @@ impl NodeGraphExecutor {
|
|||||||
// Send to frontend
|
// Send to frontend
|
||||||
responses.add(FrontendMessage::UpdateImageData { image_data });
|
responses.add(FrontendMessage::UpdateImageData { image_data });
|
||||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
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 matrix = format_transform_matrix(frame.transform);
|
||||||
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
|
let transform = if matrix.is_empty() { String::new() } else { format!(" transform=\"{matrix}\"") };
|
||||||
let svg = format!(
|
let svg = format!(
|
||||||
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
|
r#"<svg><foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="{}"></div></foreignObject></svg>"#,
|
||||||
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
frame.resolution.x, frame.resolution.y, frame.surface_id.0
|
||||||
);
|
);
|
||||||
|
self.last_svg_canvas = Some(frame);
|
||||||
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
responses.add(FrontendMessage::UpdateDocumentArtwork { svg });
|
||||||
}
|
}
|
||||||
RenderOutputType::Texture { .. } => {}
|
RenderOutputType::Texture { .. } => {}
|
||||||
|
|||||||
@@ -1045,8 +1045,12 @@ async fn poll_node_graph_evaluation() {
|
|||||||
crate::NODE_GRAPH_ERROR_DISPLAYED.store(false, Ordering::SeqCst);
|
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
|
// 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);
|
handle.send_frontend_message_to_js(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user