Skip to content

Dynamo UI Performance

Michael Kirschner edited this page Feb 8, 2022 · 2 revisions

Dynamo UI Performance Considerations

At the time of writing, Dynamo primarily uses WPF (windows presentation foundation) to render its UI. WPF is a complex and powerful xaml/binding based system. Because Dynamo has a complex UI, it's easy to create UI hangs, memory leaks, or wrap the graph execution and UI updates together in ways that degrade performance.

This wiki page will try to help you avoid a few common pitfalls making changes to Dynamo's code.

We will try to use actual code examples / pull requests that fixed these issues when possible to avoid hand waving.

Threads in Dynamo.

It's also important to understand that Dynamo is used in two main contexts. When Dynamo is run in-process within a host application like Revit or Civil3D, Dynamo's scheduler thread (where the execution runs) is the same thread as it's main UI thread. This is because the API's of these host application's dictate that API calls are made on the main UI thread.

When Dynamo is run out of process, and interacts with the host product via some IPC (inter process communication) mechanism (FormIt,Alias etc) the UI thread is not the same as the scheduler thread, this provides a better experience as the UI does not hang while the graph executes.

UI Hangs

When running in a context where the UI and main thread are the same thread, (with our current architecture) executing the graph will always block the UI, as before the UI can update, the complete graph evaluation must finish.

When running a in a context where the UI main thread and the scheduler thread are distinct threads it's still possible to block the UI.

If during a graph execution, while the scheduler thread is busy executing a dynamo program some code on the UI thread decides that it needs access to the computed value of a node, it will end up needing to wait for the graph to complete executing. The scheduler thread guards access to things like node value's with a lock statement. The UI thread may wait to aquire this lock, and will block. The UI will hang until the execution is complete and the scheduler thread releases the lock. See simplified diagram below:

image

uml for diagram
@startuml
actor User

box "Dynamo" #LightBlue
    participant "Dynamo Scheduler Thread" as EXE
    participant "UI Thread" as UI
end box
== UI Hang ==
User -> UI: add a new node to graph, trigger execution.
UI -> EXE : schedule graph execution
EXE -> EXE: start executing nodes
EXE -> EXE: aquire vm lock on node internals. (in liverunner)
User -> UI: click save button (while graph still running)
UI-> UI: start saving graph on UI thread
UI-> UI: saving graph requires output values of some nodes (workspace references computation) 
UI-> UI: attempt to aquire lock - now BLOCKED as Scheduler thread still owns the lock.
EXE -> EXE: execution complete, release lock.
UI -> UI: aquire lock, complete save operation.
UI -> UI: release lock
UI -> User: update UI.
@enduml

You can see an example of code like this in the function GetExternalFiles() in the following PR.

GetExternalFiles() tries to use the GetMirror(id) method to retrieve the value of a computed node, while the graph is still running and blocks the UI.

Resolution:

Don't write code that runs on the main UI thread that requires access to data from the VM which is computed on the scheduler thread.

WPF Dispatcher Issues

The WPF dispatcher is essentially a queue/scheduler for the UI thread in a WPF application.

The WPF model expects that objects implement the INotifyPropertyChanged interface, allowing them to raise a property changed event when setting public properties. WPF can then bind to these property change event sources and update the UI in response to data changes.

The previous link explains that only code running on the UI thread can modify the WPF UI controls (buttons,windows etc). So how does the node execution code, running on the scheduler thread, computing things like resulting errors or warnings from graph execution make its way to the UI thread?

When we need to move some computation to the UI thread so that it can update the UI controls, we will use some methods on the WPF Dispatcher that represent the UI thread. (each thread can have a dispatcher, but only the UI one is useful in this context)

Dispatcher.Invoke and Dispatcher.BeginInvoke will execute a delegate on the UI thread by inserting the work into the dispatcher queue.

Alternatively, WPF will automatically marshall certain property updates to the UI thread. It's important to understand this when tracking down performance issues.

Marshaling work from one thread to another has a performance cost. If you make many granular updates which trigger many Dispatcher.Invoke/BeginInvoke calls, or if you raise many property notifications that WPF automatically marshalls to the UI thread, you are constantly switching between threads incurring that cost over and over.

See thread contention.

For a concrete example, here is some code that updates the warnings on a Node after the graph is done executing. This code raises a property changed event, which eventually hits this code in the UI layer.

dispatcher.invoke

This code will call Dispatcher.Invoke and do some work on the UI thread, then control will return to the scheduler thread, this will occur for each warning in the original list of Messages.

This results in extremely poor performance of the UI as the scheduler and UI thread wait for each other many times, the Dynamo graph execution completion now appears to depend on the UI updating, even though the computation is already complete.

Resolution:

To resolve this issue, this PR was sent. It resolves multiple similar issues by introducing a new class PropertyChangeManager which can be used to suppress propertyChange notifications on Dynamo Model types (those inheriting from NotificationObject). WPF makes it easy to bind property changes to UI updates, but it also makes it easy to cause performance issues. By using a class like this we can by explicit about controlling when UI updates should occur. The solution here was to use this class to stop property changes from being raised when setting the node warning, and to manually update the UI when the graph execution completed, all at once, on the UI thread. If you inspect the linked PR you will see we still use Dispatcher.Invoke/BeginInvoke to marshall this update to the UI thread, but it's batched as a single update, so we don't have the overhead of multiple thread aquire and release cycles.

diagram

uml for diagram
@startuml
actor User

box "Dynamo" #LightBlue
    participant "Dynamo Scheduler Thread" as EXE
    participant "UI Thread" as UI
end box
== Slow PropertyChange Updates ==
User -> UI: run graph button
UI -> EXE : schedule graph execution
EXE -> EXE: start executing nodes
EXE -> EXE: Complete execution
loop i to n times
EXE -> EXE: Set nodes[i[] warning
EXE -> UI: property change node[i].tooltip/state trigger dispatcher update.
UI -> UI: modify UI.
UI -> EXE: done updating UI, return control.
end
EXE -> UI: graph run complete
@enduml

Releases

Roadmap

How To

Dynamo Internals

Contributing

Python3 Upgrade Work

Libraries

FAQs

API and Dynamo Nodes

Clone this wiki locally