Window closing time increases quadratically with the number of controls
See original GitHub issueDescribe the bug
If a Window
contains many controls, closing it can take a very long time. Specifically, the time between the Window.Closing
event and the Window.Closed
event increases quadratically with the number of controls in the window; for a window with 10000 controls, it takes about 30 seconds. It also appears that there are a lot of memory allocations and de-allocations that happen between the two events.
To Reproduce Create a new project with an empty window, and replace the constructor code with the following:
public MainWindow()
{
InitializeComponent();
StackPanel panel = new StackPanel();
for (int i = 0; i < 10000; i++)
{
Button btn = new Button() { Content = "Test" };
panel.Children.Add(btn);
}
this.Content = panel;
Stopwatch sw = new Stopwatch();
this.Opened += (s, e) =>
{
sw.Stop();
Debug.WriteLine(sw.ElapsedMilliseconds);
};
this.Closing += (s, e) =>
{
sw.Restart();
};
this.Closed += (s, e) =>
{
sw.Stop();
Debug.WriteLine(sw.ElapsedMilliseconds);
};
sw.Start();
}
Then change the max value in the for loop, and see how the opening time (i.e. the interval between the end of the constructor and the invocation of Window.Opened
) and the closing time (i.e. the interval between Window.Closing
and Window.Closed
) increase.
Expected behavior Closing a window should be a relatively quick process; in any case, the time it takes should increase linearly with the number of controls in the window. Indeed, the opening time increases linearly as expected (see plots below).
Screenshots
Opening time (in milliseconds, Y axis) in function of the number of buttons in the window (X axis). It appears to increase linearly. Blue is a debug build with the CPU profiler activated (which is useless, most of the time is spent in “external code”); orange is a debug build without the CPU profiler; grey is a Release build run without the debugger. Dots are the measured times (median of 3 samples), the dashed line is the linear interpolation.
Closing time (in milliseconds, Y axis) in function of the number of buttons in the window (X axis). It appears to increase quadratically.
Screenshot of the diagnostic tools. Note the high number of memory allocations and garbage collections between Window.Closing
and Window.Closed
. The CPU plot might be slightly misleading because my machine has 36 logical processors, the amount you see here corresponds to about 100% of one processor.
Desktop (please complete the following information):
- OS: tried only on Windows 10
- Version: seems about the same on 0.9.12, 0.10.3 and 0.10.7
Additional context
The results are similar if I manually detach the controls in the Window.Closing
event by setting this.Content = null;
or by doing something like while (panel.Children.Count > 0) { panel.Children.RemoveAt(0); }
.
Using the DeferredRenderer
or the ImmediateRenderer
does not make a difference. Also, replacing the StackPanel
with a Grid
or a Canvas
makes no difference.
Setting panel.IsVisible = false
before the window is shown improves both the opening and closing time (with 10000 buttons, opening time goes down from 5.6 seconds to 160 ms and closing time goes from 28.7 seconds to 4.3 seconds).
However, if I then set panel.IsVisible = true
after the window has been shown, this action blocks the UI for about the same time as the opening time.
If I set panel.IsVisible = false
after the window has been shown (before closing it), this works instantly. However, closing the window still takes the “normal” amount of time (i.e. ~30 seconds).
The quadratic time might be explained if “something” happened every time a child is removed from the panel, which causes the panel to go through every other child it still has. However, I tried to override all the overridable methods and I couldn’t find anything that gets called multiple times.
Issue Analytics
- State:
- Created 2 years ago
- Comments:9 (9 by maintainers)
Top GitHub Comments
Observed quadratic complexity seems to be caused by
WeakEventHandlerManager.Remove
callingCompact
which traverses the entire handler list. I guess we should only do that on actual events rather than on each Add/Remove or at least schedule compacting to be done asynchronously.Not fixed since Visual.cs is actually unchanged.