Skip to content

Memory leaking in MarkdownTextBlock #759

@NeverMorewd

Description

@NeverMorewd

Describe the bug

A memory leak occurs in MarkdownTextBlock when the Text property is bound to a live stream.
The same update logic works correctly with TextBox and does not exhibit any memory issues.

  • Using MarkdownTextBlock causes a memory leak.
    The private bytes start at around 100 MB and grow to over 3 GB after loading the entire text.
  • Using a TextBox instead of MarkdownTextBlock does not cause a memory leak.
    The private bytes start at around 100 MB and only increase to about 150 MB after loading the entire text.
xaml code:
<Page
    x:Class="Views.ChatPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="using:Views"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:models="using:Models"
    xmlns:templates="using:Templates"
    xmlns:vm="using:ViewModels"
    mc:Ignorable="d">
    <Page.Resources>
        <DataTemplate x:Key="ModelMessageTemplate" x:DataType="models:ModelMessage">
            <Grid>
                <!-- Using MarkdownTextBlock causes a memory leak. 
     The private bytes start at around 100 MB and grow to over 3 GB after loading the entire text. -->
                <controls:MarkdownTextBlock Text="{x:Bind MessageText, Mode=TwoWay}" />

                <!-- Using a TextBox instead of MarkdownTextBlock does not cause a memory leak. 
     The private bytes start at around 100 MB and only increase to about 150 MB after loading the entire text. -->
                <!-- <TextBox TextWrapping="Wrap" Text="{x:Bind MessageText, Mode=TwoWay}" /> -->

            </Grid>
        </DataTemplate>
        <DataTemplate x:Key="PromptMessageTemplate" x:DataType="models:PromptMessage">
            <Grid>
                <TextBox IsReadOnly="True" Text="{x:Bind MessageText}" />
            </Grid>
        </DataTemplate>
        <templates:MessageTemplateSelector
            x:Key="MessageTemplateSelector"
            ModelMessageTemplate="{StaticResource ModelMessageTemplate}"
            PromptMessageTemplate="{StaticResource PromptMessageTemplate}" />
    </Page.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="200" />
        </Grid.RowDefinitions>
        <ScrollViewer>
            <ItemsControl ItemTemplateSelector="{StaticResource MessageTemplateSelector}"         ItemsSource="{Binding Messages}">
            </ItemsControl>
        </ScrollViewer>
    </Grid>
</Page>
csharp code:
public partial class BaseMessage : ReactiveObject
{
    [Reactive]
    private string _messageText = string.Empty;
}
public partial class ModelMessage : BaseMessage
{
    public Task SubscribeAsync(IAsyncEnumerable<string> strings, CancellationToken cancellationToken)
    {
        var observable = strings
            .ToObservable()
            .Buffer(TimeSpan.FromMilliseconds(100))
            .Where(buffer => buffer.Count > 0)
            .ObserveOn(RxApp.MainThreadScheduler);
        observable.Subscribe(buffer =>
        {
            MessageText += string.Concat(buffer);
        });
        return Task.CompletedTask;
    }
}
public partial class ChatViewModel:ReactiveObject
{
    private const string _markdown = "**"
    public ObservableCollection<BaseMessage> Messages { get; } = [];

    [ReactiveCommand]
    public async Task SendMessage(string message)
    {
        var modelMessage = new ModelMessage();
        Messages.Add(modelMessage);
        var textStream = GetTextStream(_markdown, delayMs: 5);
        _= modelMessage.SubscribeAsync(textStream,cancellationToken:System.Threading.CancellationToken.None);
    }
    
    public static async IAsyncEnumerable<string> GetTextStream(
        string text,
        int delayMs = 10,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        foreach (var ch in text)
        {
            cancellationToken.ThrowIfCancellationRequested();
            yield return ch.ToString();
            await Task.Delay(delayMs, cancellationToken);
        }
    }

** _markdown is a sample markdown code :

Steps to reproduce

- Create a new winui3 project
- Paste the code
- Run/Debug
- Start a memory profile tool:process informer

Expected behavior

Although the memory may temporarily spike to a higher value, it eventually returns to a normal level.

Screenshots

No response

Code Platform

  • UWP
  • WinAppSDK / WinUI 3
  • Web Assembly (WASM)
  • Android
  • iOS
  • MacOS
  • Linux / GTK

Windows Build Number

  • Windows 10 1809 (Build 17763)
  • Windows 10 1903 (Build 18362)
  • Windows 10 1909 (Build 18363)
  • Windows 10 2004 (Build 19041)
  • Windows 10 20H2 (Build 19042)
  • Windows 10 21H1 (Build 19043)
  • Windows 11 21H2 (Build 22000)
  • Other (specify)

Other Windows Build number

No response

App minimum and target SDK version

  • Windows 10, version 1809 (Build 17763)
  • Windows 10, version 1903 (Build 18362)
  • Windows 10, version 1909 (Build 18363)
  • Windows 10, version 2004 (Build 19041)
  • Other (specify)

Other SDK version

No response

Visual Studio Version

2022

Visual Studio Build Number

17.14.18

Device form factor

Desktop

Additional context

No response

Help us help you

Yes, I'd like to be assigned to work on this item.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug 🐛Something isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions