Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reduce TextFormatter allocations #3991

Merged
merged 12 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions Benchmarks/Text/StringExtensions/ToStringEnumerable.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;

namespace Terminal.Gui.Benchmarks.Text.StringExtensions;

/// <summary>
/// Benchmarks for <see cref="Tui.StringExtensions.ToString(IEnumerable{Rune})"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
public class ToStringEnumerable
{

/// <summary>
/// Benchmark for previous implementation.
/// </summary>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public string Previous (IEnumerable<Rune> runes, int len)
{
return StringConcatInLoop (runes);
}

/// <summary>
/// Benchmark for current implementation with char buffer and
/// fallback to rune chars appending to StringBuilder.
/// </summary>
/// <param name="runes"></param>
/// <returns></returns>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public string Current (IEnumerable<Rune> runes, int len)
{
return Tui.StringExtensions.ToString (runes);
}

/// <summary>
/// Previous implementation with string concatenation in a loop.
/// </summary>
private static string StringConcatInLoop (IEnumerable<Rune> runes)
{
var str = string.Empty;

foreach (Rune rune in runes)
{
str += rune.ToString ();
}

return str;
}

public IEnumerable<object []> DataSource ()
{
// Extra length argument as workaround for the summary grouping
// different length collections to same baseline making comparison difficult.
foreach (string text in GetTextData ())
{
Rune [] runes = [..text.EnumerateRunes ()];
yield return [runes, runes.Length];
}
}

private IEnumerable<string> GetTextData ()
{
string textSource =
"""
Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
""";

int[] lengths = [1, 10, 100, textSource.Length / 2, textSource.Length];

foreach (int length in lengths)
{
yield return textSource [..length];
}

string textLongerThanStackallocThreshold = string.Concat(Enumerable.Repeat(textSource, 10));
yield return textLongerThanStackallocThreshold;
}
}
97 changes: 97 additions & 0 deletions Benchmarks/Text/TextFormatter/RemoveHotKeySpecifier.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;

namespace Terminal.Gui.Benchmarks.Text.TextFormatter;

/// <summary>
/// Benchmarks for <see cref="Tui.TextFormatter.RemoveHotKeySpecifier"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof(Tui.TextFormatter))]
public class RemoveHotKeySpecifier
{
// Omit from summary table.
private static readonly Rune HotkeySpecifier = (Rune)'_';

/// <summary>
/// Benchmark for previous implementation.
/// </summary>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public string Previous (string text, int hotPos)
{
return StringConcatLoop (text, hotPos, HotkeySpecifier);
}

/// <summary>
/// Benchmark for current implementation with stackalloc char buffer and fallback to rented array.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public string Current (string text, int hotPos)
{
return Tui.TextFormatter.RemoveHotKeySpecifier (text, hotPos, HotkeySpecifier);
}

/// <summary>
/// Previous implementation with string concatenation in a loop.
/// </summary>
public static string StringConcatLoop (string text, int hotPos, Rune hotKeySpecifier)
{
if (string.IsNullOrEmpty (text))
{
return text;
}

// Scan
var start = string.Empty;
var i = 0;

foreach (Rune c in text.EnumerateRunes ())
{
if (c == hotKeySpecifier && i == hotPos)
{
i++;

continue;
}

start += c;
i++;
}

return start;
}

public IEnumerable<object []> DataSource ()
{
string[] texts = [
"",
// Typical scenario.
"_Save file (Ctrl+S)",
// Medium text, hotkey specifier somewhere in the middle.
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. _Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla.",
// Long text, hotkey specifier almost at the beginning.
"Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. _Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ. " +
"Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé. " +
"Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.",
// Long text, hotkey specifier almost at the end.
"Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ. " +
"Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé. " +
"Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. _Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.",
];

foreach (string text in texts)
{
int hotPos = text.EnumerateRunes()
.Select((r, i) => r == HotkeySpecifier ? i : -1)
.FirstOrDefault(i => i > -1, -1);

yield return [text, hotPos];
}

// Typical scenario but without hotkey and with misleading position.
yield return ["Save file (Ctrl+S)", 3];
}
}
90 changes: 90 additions & 0 deletions Benchmarks/Text/TextFormatter/ReplaceCRLFWithSpace.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;

namespace Terminal.Gui.Benchmarks.Text.TextFormatter;

/// <summary>
/// Benchmarks for <see cref="Tui.TextFormatter.ReplaceCRLFWithSpace"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.TextFormatter))]
public class ReplaceCRLFWithSpace
{

/// <summary>
/// Benchmark for previous implementation.
/// </summary>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public string Previous (string str)
{
return ToRuneListReplaceImplementation (str);
}

/// <summary>
/// Benchmark for current implementation.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public string Current (string str)
{
return Tui.TextFormatter.ReplaceCRLFWithSpace (str);
}

/// <summary>
/// Previous implementation with intermediate rune list.
/// </summary>
/// <param name="str"></param>
/// <returns></returns>
private static string ToRuneListReplaceImplementation (string str)
{
var runes = str.ToRuneList ();
for (int i = 0; i < runes.Count; i++)
{
switch (runes [i].Value)
{
case '\n':
runes [i] = (Rune)' ';
break;

case '\r':
if ((i + 1) < runes.Count && runes [i + 1].Value == '\n')
{
runes [i] = (Rune)' ';
runes.RemoveAt (i + 1);
i++;
}
else
{
runes [i] = (Rune)' ';
}
break;
}
}
return Tui.StringExtensions.ToString (runes);
}

public IEnumerable<object> DataSource ()
{
// Extreme newline scenario
yield return "E\r\nx\r\nt\r\nr\r\ne\r\nm\r\ne\r\nn\r\ne\r\nw\r\nl\r\ni\r\nn\r\ne\r\ns\r\nc\r\ne\r\nn\r\na\r\nr\r\ni\r\no\r\n";
// Long text with few line endings
yield return
"""
Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
"""
// Consistent line endings between systems for more consistent performance evaluation.
.ReplaceLineEndings ("\r\n");
// Long text without line endings
yield return
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla. " +
"Curabitur mollis ex nisl, vitae mattis nisl consequat at. Aliquam dolor lectus, tincidunt ac nunc eu, elementum molestie lectus. Donec lacinia eget dolor a scelerisque. " +
"Aenean elementum molestie rhoncus. Duis id ornare lorem. Nam eget porta sapien. Etiam rhoncus dignissim leo, ac suscipit magna finibus eu. Curabitur hendrerit elit erat, sit amet suscipit felis condimentum ut. " +
"Nullam semper tempor mi, nec semper quam fringilla eu. Aenean sit amet pretium augue, in posuere ante. Aenean convallis porttitor purus, et posuere velit dictum eu.";
}
}
115 changes: 115 additions & 0 deletions Benchmarks/Text/TextFormatter/StripCRLF.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;

namespace Terminal.Gui.Benchmarks.Text.TextFormatter;

/// <summary>
/// Benchmarks for <see cref="Tui.TextFormatter.StripCRLF"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.TextFormatter))]
public class StripCRLF
{
/// <summary>
/// Benchmark for previous implementation.
/// </summary>
/// <param name="str"></param>
/// <param name="keepNewLine"></param>
/// <returns></returns>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public string Previous (string str, bool keepNewLine)
{
return RuneListToString (str, keepNewLine);
}

/// <summary>
/// Benchmark for current implementation with StringBuilder and char span index of search.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public string Current (string str, bool keepNewLine)
{
return Tui.TextFormatter.StripCRLF (str, keepNewLine);
}

/// <summary>
/// Previous implementation with intermediate rune list.
/// </summary>
private static string RuneListToString (string str, bool keepNewLine = false)
{
List<Rune> runes = str.ToRuneList ();

for (var i = 0; i < runes.Count; i++)
{
switch ((char)runes [i].Value)
{
case '\n':
if (!keepNewLine)
{
runes.RemoveAt (i);
}

break;

case '\r':
if (i + 1 < runes.Count && runes [i + 1].Value == '\n')
{
runes.RemoveAt (i);

if (!keepNewLine)
{
runes.RemoveAt (i);
}

i++;
}
else
{
if (!keepNewLine)
{
runes.RemoveAt (i);
}
}

break;
}
}

return Tui.StringExtensions.ToString (runes);
}

public IEnumerable<object []> DataSource ()
{
string[] textPermutations = [
// Extreme newline scenario
"E\r\nx\r\nt\r\nr\r\ne\r\nm\r\ne\r\nn\r\ne\r\nw\r\nl\r\ni\r\nn\r\ne\r\ns\r\nc\r\ne\r\nn\r\na\r\nr\r\ni\r\no\r\n",
// Long text with few line endings
"""
Ĺόŕéḿ íṕśúḿ d́όĺόŕ śít́ áḿét́, ćόńśéćt́ét́úŕ ád́íṕíśćíńǵ éĺít́. Ṕŕáéśéńt́ q́úíś ĺúćt́úś éĺít́. Íńt́éǵéŕ út́ áŕćú éǵét́ d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć ét́ d́íáḿ.
Ṕéĺĺéńt́éśq́úé śéd́ d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Út́ q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś íd́, t́éḿṕúś ńéq́úé.
Ṕŕáéśéńt́ śáṕíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś át́, v́áŕíúś śúśćíṕít́ áńt́é. Út́ ṕúĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé ṕéńát́íb́úś ét́ ḿáǵńíś d́íś ṕáŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé át́ éx́ b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ ét́, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śít́ áḿét́ áŕćú út́ áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śít́ áḿét́ ḿáx́íḿúś d́íáḿ. Ńáḿ éx́ ĺéό, ṕh́áŕét́ŕá éú ĺόb́όŕt́íś át́, t́ŕíśt́íq́úé út́ f́éĺíś.
"""
// Consistent line endings between systems for more consistent performance evaluation.
.ReplaceLineEndings ("\r\n"),
// Long text without line endings
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla sed euismod metus. Phasellus lectus metus, ultricies a commodo quis, facilisis vitae nulla. " +
"Curabitur mollis ex nisl, vitae mattis nisl consequat at. Aliquam dolor lectus, tincidunt ac nunc eu, elementum molestie lectus. Donec lacinia eget dolor a scelerisque. " +
"Aenean elementum molestie rhoncus. Duis id ornare lorem. Nam eget porta sapien. Etiam rhoncus dignissim leo, ac suscipit magna finibus eu. Curabitur hendrerit elit erat, sit amet suscipit felis condimentum ut. " +
"Nullam semper tempor mi, nec semper quam fringilla eu. Aenean sit amet pretium augue, in posuere ante. Aenean convallis porttitor purus, et posuere velit dictum eu."
];

bool[] newLinePermutations = [true, false];

foreach (string text in textPermutations)
{
foreach (bool keepNewLine in newLinePermutations)
{
yield return [text, keepNewLine];
}
}
}
}
Loading
Loading