Fixes #4046 - Moves examples into ./Examples and fixes ./Tests (#4047)

* touching publish.yml

* Moved Examples into ./Examples

* Moved Benchmarks into ./Tests

* Moved Benchmarks into ./Tests

* Moved UICatalog into ./Examples

* Moved UICatalog into ./Examples 2

* Moved tests into ./Tests

* Updated nuget
This commit is contained in:
Tig
2025-04-25 09:49:33 -06:00
committed by GitHub
parent dca3923491
commit 0baa881dc5
199 changed files with 149 additions and 142 deletions

View File

@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
<Nullable>enable</Nullable>
<RootNamespace>Terminal.Gui.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Terminal.Gui\Terminal.Gui.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,48 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
/// <summary>
/// Compares the Set and Append implementations in combination.
/// </summary>
/// <remarks>
/// A bit misleading because *CursorPosition is called very seldom compared to the other operations
/// but they are very similar in performance because they do very similar things.
/// </remarks>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.EscSeqUtils))]
// Hide useless empty column from results.
[HideColumns ("stringBuilder")]
public class CSI_SetVsAppend
{
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (StringBuilderSource))]
public StringBuilder Set (StringBuilder stringBuilder)
{
stringBuilder.Append (Tui.EscSeqUtils.CSI_SetBackgroundColorRGB (1, 2, 3));
stringBuilder.Append (Tui.EscSeqUtils.CSI_SetForegroundColorRGB (3, 2, 1));
stringBuilder.Append (Tui.EscSeqUtils.CSI_SetCursorPosition (4, 2));
// Clear to prevent out of memory exception from consecutive iterations.
stringBuilder.Clear ();
return stringBuilder;
}
[Benchmark]
[ArgumentsSource (nameof (StringBuilderSource))]
public StringBuilder Append (StringBuilder stringBuilder)
{
Tui.EscSeqUtils.CSI_AppendBackgroundColorRGB (stringBuilder, 1, 2, 3);
Tui.EscSeqUtils.CSI_AppendForegroundColorRGB (stringBuilder, 3, 2, 1);
Tui.EscSeqUtils.CSI_AppendCursorPosition (stringBuilder, 4, 2);
// Clear to prevent out of memory exception from consecutive iterations.
stringBuilder.Clear ();
return stringBuilder;
}
public static IEnumerable<object> StringBuilderSource ()
{
return [new StringBuilder ()];
}
}

View File

@@ -0,0 +1,31 @@
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.ConsoleDrivers.EscSeqUtils;
[MemoryDiagnoser]
// Hide useless column from results.
[HideColumns ("writer")]
public class CSI_SetVsWrite
{
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (TextWriterSource))]
public TextWriter Set (TextWriter writer)
{
writer.Write (Tui.EscSeqUtils.CSI_SetCursorPosition (1, 1));
return writer;
}
[Benchmark]
[ArgumentsSource (nameof (TextWriterSource))]
public TextWriter Write (TextWriter writer)
{
Tui.EscSeqUtils.CSI_WriteCursorPosition (writer, 1, 1);
return writer;
}
public static IEnumerable<object> TextWriterSource ()
{
return [StringWriter.Null];
}
}

View File

@@ -0,0 +1,20 @@
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
namespace Terminal.Gui.Benchmarks;
class Program
{
static void Main (string [] args)
{
var config = DefaultConfig.Instance;
// Uncomment for faster but less accurate intermediate iteration.
// Final benchmarks should be run with at least the default run length.
//config = config.AddJob (BenchmarkDotNet.Jobs.Job.ShortRun);
BenchmarkSwitcher
.FromAssembly (typeof (Program).Assembly)
.Run(args, config);
}
}

View File

@@ -0,0 +1,66 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
/// <summary>
/// Benchmarks for <see cref="Tui.RuneExtensions.DecodeSurrogatePair"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
public class DecodeSurrogatePair
{
/// <summary>
/// Benchmark for previous implementation.
/// </summary>
/// <param name="rune"></param>
/// <returns></returns>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public char []? Previous (Rune rune)
{
_ = RuneToStringToCharArray (rune, out char []? chars);
return chars;
}
/// <summary>
/// Benchmark for current implementation.
///
/// Utilizes Rune methods that take Span argument avoiding intermediate heap array allocation when combined with stack allocated intermediate buffer.
/// When rune is not surrogate pair there will be no heap allocation.
///
/// Final surrogate pair array allocation cannot be avoided due to the current method signature design.
/// Changing the method signature, or providing an alternative method, to take a destination Span would allow further optimizations by allowing caller to reuse buffer for consecutive calls.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public char []? Current (Rune rune)
{
_ = Tui.RuneExtensions.DecodeSurrogatePair (rune, out char []? chars);
return chars;
}
/// <summary>
/// Previous implementation with intermediate string allocation.
///
/// The IsSurrogatePair implementation at the time had hidden extra string allocation so there were intermediate heap allocations even if rune is not surrogate pair.
/// </summary>
private static bool RuneToStringToCharArray (Rune rune, out char []? chars)
{
if (rune.IsSurrogatePair ())
{
chars = rune.ToString ().ToCharArray ();
return true;
}
chars = null;
return false;
}
public static IEnumerable<object> DataSource ()
{
yield return new Rune ('a');
yield return "𝔹".EnumerateRunes ().Single ();
}
}

View File

@@ -0,0 +1,72 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
/// <summary>
/// Benchmarks for <see cref="Tui.RuneExtensions.Encode"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
public class Encode
{
/// <summary>
/// Benchmark for previous implementation.
/// </summary>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public byte [] Previous (Rune rune, byte [] destination, int start, int count)
{
_ = StringEncodingGetBytes (rune, destination, start, count);
return destination;
}
/// <summary>
/// Benchmark for current implementation.
///
/// Avoids intermediate heap allocations with stack allocated intermediate buffer.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public byte [] Current (Rune rune, byte [] destination, int start, int count)
{
_ = Tui.RuneExtensions.Encode (rune, destination, start, count);
return destination;
}
/// <summary>
/// Previous implementation with intermediate byte array and string allocation.
/// </summary>
private static int StringEncodingGetBytes (Rune rune, byte [] dest, int start = 0, int count = -1)
{
byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
var length = 0;
for (var i = 0; i < (count == -1 ? bytes.Length : count); i++)
{
if (bytes [i] == 0)
{
break;
}
dest [start + i] = bytes [i];
length++;
}
return length;
}
public static IEnumerable<object []> DataSource ()
{
Rune[] runes = [ new Rune ('a'),"𝔞".EnumerateRunes().Single() ];
foreach (var rune in runes)
{
yield return new object [] { rune, new byte [16], 0, -1 };
yield return new object [] { rune, new byte [16], 8, -1 };
// Does not work in original implementation
//yield return new object [] { rune, new byte [16], 8, 8 };
}
}
}

View File

@@ -0,0 +1,36 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
/// <summary>
/// Benchmarks for <see cref="Tui.RuneExtensions.EncodeSurrogatePair"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
public class EncodeSurrogatePair
{
/// <summary>
/// Benchmark for current implementation.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public Rune Current (char highSurrogate, char lowSurrogate)
{
_ = Tui.RuneExtensions.EncodeSurrogatePair (highSurrogate, lowSurrogate, out Rune rune);
return rune;
}
public static IEnumerable<object []> DataSource ()
{
string[] runeStrings = ["🍕", "🧠", "🌹"];
foreach (string symbol in runeStrings)
{
if (symbol is [char high, char low])
{
yield return [high, low];
}
}
}
}

View File

@@ -0,0 +1,74 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
/// <summary>
/// Benchmarks for <see cref="Tui.RuneExtensions.GetEncodingLength"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
public class GetEncodingLength
{
/// <summary>
/// Benchmark for previous implementation.
/// </summary>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public int Previous (Rune rune, PrettyPrintedEncoding encoding)
{
return WithEncodingGetBytesArray (rune, encoding);
}
/// <summary>
/// Benchmark for current implementation.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public int Current (Rune rune, PrettyPrintedEncoding encoding)
{
return Tui.RuneExtensions.GetEncodingLength (rune, encoding);
}
/// <summary>
/// Previous implementation with intermediate byte array, string, and char array allocation.
/// </summary>
private static int WithEncodingGetBytesArray (Rune rune, Encoding? encoding = null)
{
encoding ??= Encoding.UTF8;
byte [] bytes = encoding.GetBytes (rune.ToString ().ToCharArray ());
var offset = 0;
if (bytes [^1] == 0)
{
offset++;
}
return bytes.Length - offset;
}
public static IEnumerable<object []> DataSource ()
{
PrettyPrintedEncoding[] encodings = [ new(Encoding.UTF8), new(Encoding.Unicode), new(Encoding.UTF32) ];
Rune[] runes = [ new Rune ('a'), "𝔹".EnumerateRunes ().Single () ];
foreach (var encoding in encodings)
{
foreach (Rune rune in runes)
{
yield return [rune, encoding];
}
}
}
/// <summary>
/// <see cref="System.Text.Encoding"/> wrapper to display proper encoding name in benchmark results.
/// </summary>
public record PrettyPrintedEncoding (Encoding Encoding)
{
public static implicit operator Encoding (PrettyPrintedEncoding ppe) => ppe.Encoding;
public override string ToString () => Encoding.HeaderName;
}
}

View File

@@ -0,0 +1,50 @@
using System.Text;
using BenchmarkDotNet.Attributes;
using Tui = Terminal.Gui;
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
/// <summary>
/// Benchmarks for <see cref="Tui.RuneExtensions.IsSurrogatePair"/> performance fine-tuning.
/// </summary>
[MemoryDiagnoser]
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
public class IsSurrogatePair
{
/// <summary>
/// Benchmark for previous implementation.
/// </summary>
/// <param name="rune"></param>
[Benchmark]
[ArgumentsSource (nameof (DataSource))]
public bool Previous (Rune rune)
{
return WithToString (rune);
}
/// <summary>
/// Benchmark for current implementation.
///
/// Avoids intermediate heap allocations by using stack allocated buffer.
/// </summary>
[Benchmark (Baseline = true)]
[ArgumentsSource (nameof (DataSource))]
public bool Current (Rune rune)
{
return Tui.RuneExtensions.IsSurrogatePair (rune);
}
/// <summary>
/// Previous implementation with intermediate string allocation.
/// </summary>
private static bool WithToString (Rune rune)
{
return char.IsSurrogatePair (rune.ToString (), 0);
}
public static IEnumerable<object> DataSource ()
{
yield return new Rune ('a');
yield return "𝔹".EnumerateRunes ().Single ();
}
}

View File

@@ -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́úŕ ád́íṕíśćíńǵ éĺí. ŕáéśéń q́úíś ĺúćt́úś éĺí. Íńt́éǵéŕ ú áŕćú éǵé d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć é d́íáḿ.
éĺĺéńt́éśq́úé śé d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Ú q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś í, t́éḿṕúś ńéq́úé.
ŕáéśéń śáíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś á, v́áŕíúś śúśćíí áńt́é. Ú úĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé éńát́íb́úś é ḿáǵńíś d́íś áŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé á é b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ é, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śí áḿé áŕćú ú áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śí áḿé ḿáx́íḿúś d́íáḿ. Ńáḿ é ĺéό, h́áŕét́ŕá éú ĺόb́όŕt́íś á, t́ŕíśt́íq́úé ú 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;
}
}

View File

@@ -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];
}
}

View File

@@ -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́úŕ ád́íṕíśćíńǵ éĺí. ŕáéśéń q́úíś ĺúćt́úś éĺí. Íńt́éǵéŕ ú áŕćú éǵé d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć é d́íáḿ.
éĺĺéńt́éśq́úé śé d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Ú q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś í, t́éḿṕúś ńéq́úé.
ŕáéśéń śáíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś á, v́áŕíúś śúśćíí áńt́é. Ú úĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé éńát́íb́úś é ḿáǵńíś d́íś áŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé á é b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ é, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śí áḿé áŕćú ú áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śí áḿé ḿáx́íḿúś d́íáḿ. Ńáḿ é ĺéό, h́áŕét́ŕá éú ĺόb́όŕt́íś á, t́ŕíśt́íq́úé ú 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.";
}
}

View File

@@ -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́úŕ ád́íṕíśćíńǵ éĺí. ŕáéśéń q́úíś ĺúćt́úś éĺí. Íńt́éǵéŕ ú áŕćú éǵé d́όĺόŕ śćéĺéŕíśq́úé ḿát́t́íś áć é d́íáḿ.
éĺĺéńt́éśq́úé śé d́áṕíb́úś ḿáśśá, v́éĺ t́ŕíśt́íq́úé d́úí. Śéd́ v́ít́áé ńéq́úé éú v́éĺít́ όŕńáŕé áĺíq́úét́. Ú q́úíś όŕćí t́éḿṕόŕ, t́éḿṕόŕ t́úŕṕíś í, t́éḿṕúś ńéq́úé.
ŕáéśéń śáíéń t́úŕṕíś, όŕńáŕé v́éĺ ḿáúŕíś á, v́áŕíúś śúśćíí áńt́é. Ú úĺv́íńáŕ t́úŕṕíś ḿáśśá, q́úíś ćúŕśúś áŕćú f́áúćíb́úś íń.
Óŕćí v́áŕíúś ńát́όq́úé éńát́íb́úś é ḿáǵńíś d́íś áŕt́úŕíéńt́ ḿόńt́éś, ńáśćét́úŕ ŕíd́íćúĺúś ḿúś. F́úśćé á é b́ĺáńd́ít́, ćόńv́áĺĺíś q́úáḿ é, v́úĺṕút́át́é ĺáćúś.
Śúśṕéńd́íśśé śí áḿé áŕćú ú áŕćú f́áúćíb́úś v́áŕíúś. V́ív́áḿúś śí áḿé ḿáx́íḿúś d́íáḿ. Ńáḿ é ĺéό, h́áŕét́ŕá éú ĺόb́όŕt́íś á, t́ŕíśt́íq́úé ú 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];
}
}
}
}