-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Description
Adding large quantities of text to a TextBox using TextBox.AppendText repeatedly becomes significantly slower if the application hosting the textbox has ever had a WPF submenu opened.
Opening a submenu inside the application seems to trigger the AutomationPeer system to be active from that point on. This changes the performance behaviour of TextBox.AppendText, seemingly to the point where the time taken is proportional to the amount of text already in the text box. This leads to classic O(N^2) performance if you repeatedly add content to a text box.
The cause can be confirmed by overriding OnCreateAutomationPeer() on the containing Window to return a dummy AutomationPeer, which restores performance
Reproduction Steps
Set up the following code in a WPF application (.NET 8 shows the problem, as does 7)
<Window x:Class="WPFTextPerf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition/>
</Grid.RowDefinitions>
<Menu>
<MenuItem Header="Measure speed before and after showing this menu">
<MenuItem Header="Open"></MenuItem>
</MenuItem>
</Menu>
<Button Grid.Row="1" Click="Button_Click">Click Me to measure speed of TextBox population!</Button>
<TextBox AutomationProperties.IsOffscreenBehavior="Offscreen" Grid.Row="2" Name="TextBox" IsReadOnly="True" FontFamily="Courier New" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Visible"></TextBox>
</Grid>
</Window>
namespace WPFTextPerf2;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private async void Button_Click(object sender, RoutedEventArgs e)
{
TextBox.Clear();
var sw = Stopwatch.StartNew();
int count = 3000;
for (int i=0; i<count; i++)
{
TextBox.AppendText($"This is line {i} of {count}\r\n");
await Task.Yield();
}
MessageBox.Show($"Loaded {count} lines in {sw.ElapsedMilliseconds}ms");
}
//protected override AutomationPeer OnCreateAutomationPeer()
//{
// return new CustomWindowAutomationPeer(this);
//}
}
public class CustomWindowAutomationPeer : FrameworkElementAutomationPeer
{
public CustomWindowAutomationPeer(FrameworkElement owner) : base(owner) { }
protected override string GetNameCore() => "NoAutomationPeer";
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Window;
protected override List<AutomationPeer> GetChildrenCore() => new();
}
Run the application and press the 'Click Me' button to time how long it takes to add 3000 lines of text to the text box. Click repeatedly to remove any 'cold start' effects. In my testing, after 2-3 clicks, the time taken ends up at about 50-60ms
Now click on the menu in the application to show the sub menu. No need to activate the sub-menu item, just expanding the menu is sufficient.
Return to the 'Click Me' button and re-check the timing. The timing to populate the text box increases significantly (in my measurements about 2.5s, or 50x slower than before showing the menu).
Expected behavior
Performance of other parts of the application should not be significantly affected by unrelated actions (opening and closing a menu)
Actual behavior
Performance of populating the text box slows by a factor of 50 after the menu has been opened.
The size of the effect grows as the amount of text to be added increases, for instance for 5000 lines instead of 3000, the timings go from 100ms to 7.5s - 75x slower.
Regression?
The issue reproduces in 6,7 and 8. Not tested earlier versions than that.
Known Workarounds
override OnCreateAutomationPeer on the Window containing the text box (or, presumably, at some other point in the WPF hierarchy leading to the text box if it is in a more complex Window). Return a custom AutomationPeer that does not list child objects, cutting the TextBox off from automation
protected override AutomationPeer OnCreateAutomationPeer()
{
return new CustomWindowAutomationPeer(this);
}
public class CustomWindowAutomationPeer : FrameworkElementAutomationPeer
{
public CustomWindowAutomationPeer(FrameworkElement owner) : base(owner) { }
protected override string GetNameCore() => "NoAutomationPeer";
protected override AutomationControlType GetAutomationControlTypeCore() => AutomationControlType.Window;
protected override List<AutomationPeer> GetChildrenCore() => new();
}
Impact
While the use of large amounts of text in a TextBox may be quite rare, the very significant nature of the slowdown and the fact that it is triggered by a seemingly unrelated action offset that to add to the impace. Tracking the cause back to automation peers wasn't simple, meaning that many developers may not discover the workaround. In any case, the workaround isn't really a good one, since it cuts the entire Window off from automation/accessibility.
If a performance fix for this issue is impossible, a way to cleanly cut off just particularly XAML elements from automation to avoid this sort of perf problem would be good.
Configuration
.NET 8.0.1
Windows 11 Pro 23H2
x64 system, tested with the application targeted at both x64 and x86. The exact timings vary slightly between x64 and x86, but the problem persists.
Appears not to be configuration specific
Other information
No response