Skip to content

Commit 0b4068b

Browse files
tigTheTonttuBDisp
authored
Merge v2_develop into v2_release (#3929)
* Moved scripts * testing versions * Rune extensions micro-optimizations (#3910) * Add benchmarks for potentially optimizable RuneExtensions * Add new RuneExtensions.DecodeSurrogatePair benchmark implementation Avoids intermediate heap array allocations which is especially nice when the rune is not surrogate pair because then array heap allocations are completely avoided. * Enable nullable reference types in RuneExtensions * Make RuneExtensions.MaxUnicodeCodePoint readonly Makes sure no one can accidentally change the value. Ideally would be const value. * Optimize RuneExtensions.DecodeSurrogatePair * Remove duplicate Rune.GetUnicodeCategory call * Add new RuneExtensions.IsSurrogatePair benchmark implementation Avoids intermediate heap allocations by using stack allocated buffer. * Optimize RuneExtensions.IsSurrogatePair * Add RuneExtensions.GetEncodingLength tests * Optimize RuneExtensions.GetEncodingLength * Optimize RuneExtensions.Encode * Print encoding name in benchmark results * Rename variable to better match return description * Add RuneExtensions.EncodeSurrogatePair benchmark --------- Co-authored-by: Tig <[email protected]> * Reduce func allocations (#3919) * Replace Region.Contains LINQ lambdas with foreach loop Removes the lambda func allocations caused by captured outer variables. * Replace LineCanvas.Has LINQ lambda with foreach loop * Fix LineCanvas.GetMap intersects array nullability It should be enough to add null-forgiving operator somewhere in the LINQ query to make the final result non-null. No need to shove the nullability further down the line to complicate things. :) * Replace LineCanvas.All LINQ lambda with foreach loop * Replace Region.Intersect LINQ lambdas and list allocation with foreach loop and rented array * Use stackalloc buffer in Region.Intersect when max 8 rectangles * Fix LineCanvas.GetCellMap intersects array nullability * Remove leftover LineCanvas.GetRuneForIntersects null-conditional operators * Remove leftover IntersectionRuneResolver.GetRuneForIntersects null-conditional operators * PosAlign.CalculateMinDimension: calculate sum during loop No need to first put the dimensions in a list and then sum the list when you can just add to sum while looping through dimensions. * PosAlign.CalculateMinDimension: Remove intermediate list and related filter func allocation * TextFormatter.GetRuneWidth: Remove intermediate list and related sum func allocation * ReadOnlySpan refactor preparation for GetCellMap rewrite * LineCanvas.GetCellMap: Reuse intersection list outside nested loops GetCellMap would not benefit much from array pool because IntersectionDefinition is not struct. This change eliminates majority of the rest of Func<,> allocations. As a bonus IntersectionDefinition[] allocations dropped nicely. * Refactor local method UseRounded * Wrap too long list of method parameters * Region: Consistent location for #nullable enable --------- Co-authored-by: Tig <[email protected]> * Fixes #3918 and #3913 - `Accepting` behavior (#3921) * Fixed #3905, #3918 * Tweaked Generic * Label code cleanup * Clean up. * Clean up. * Clean up2. * Fixes #3839, #3922 - CM Glyphs not working (#3923) * fixed * Moved Glyphs to ThemeScope * Removed test code * Fixed nav (#3926) * Fixes #3881. PositionCursor broke with recent ConsoleDriver changes. (#3927) * Reduce IntersectionType[] allocations (#3924) * Eliminate LineCanvas.Has params array allocation Inline ReadOnlySpan arguments do not incur heap allocation compared to regular arrays. * Allocate once LineCanvas.Exactly corner intersection arrays --------- Co-authored-by: Tig <[email protected]> * API doc updates (#3928) --------- Co-authored-by: Tonttu <[email protected]> Co-authored-by: BDisp <[email protected]>
1 parent 396b5ea commit 0b4068b

File tree

79 files changed

+1951
-1192
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+1951
-1192
lines changed

Benchmarks/Benchmarks.csproj

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>net8.0</TargetFramework>
6+
<IsPackable>false</IsPackable>
7+
<ImplicitUsings>enable</ImplicitUsings>
8+
<Nullable>enable</Nullable>
9+
<RootNamespace>Terminal.Gui.$(MSBuildProjectName.Replace(" ", "_"))</RootNamespace>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
14+
</ItemGroup>
15+
16+
<ItemGroup>
17+
<ProjectReference Include="..\Terminal.Gui\Terminal.Gui.csproj" />
18+
</ItemGroup>
19+
20+
</Project>

Benchmarks/Program.cs

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using BenchmarkDotNet.Configs;
2+
using BenchmarkDotNet.Running;
3+
4+
namespace Terminal.Gui.Benchmarks;
5+
6+
class Program
7+
{
8+
static void Main (string [] args)
9+
{
10+
var config = DefaultConfig.Instance;
11+
12+
// Uncomment for faster but less accurate intermediate iteration.
13+
// Final benchmarks should be run with at least the default run length.
14+
//config = config.AddJob (BenchmarkDotNet.Jobs.Job.ShortRun);
15+
16+
BenchmarkSwitcher
17+
.FromAssembly (typeof (Program).Assembly)
18+
.Run(args, config);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using System.Text;
2+
using BenchmarkDotNet.Attributes;
3+
using Tui = Terminal.Gui;
4+
5+
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
6+
7+
/// <summary>
8+
/// Benchmarks for <see cref="Tui.RuneExtensions.DecodeSurrogatePair"/> performance fine-tuning.
9+
/// </summary>
10+
[MemoryDiagnoser]
11+
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
12+
public class DecodeSurrogatePair
13+
{
14+
/// <summary>
15+
/// Benchmark for previous implementation.
16+
/// </summary>
17+
/// <param name="rune"></param>
18+
/// <returns></returns>
19+
[Benchmark]
20+
[ArgumentsSource (nameof (DataSource))]
21+
public char []? Previous (Rune rune)
22+
{
23+
_ = RuneToStringToCharArray (rune, out char []? chars);
24+
return chars;
25+
}
26+
27+
/// <summary>
28+
/// Benchmark for current implementation.
29+
///
30+
/// Utilizes Rune methods that take Span argument avoiding intermediate heap array allocation when combined with stack allocated intermediate buffer.
31+
/// When rune is not surrogate pair there will be no heap allocation.
32+
///
33+
/// Final surrogate pair array allocation cannot be avoided due to the current method signature design.
34+
/// 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.
35+
/// </summary>
36+
[Benchmark (Baseline = true)]
37+
[ArgumentsSource (nameof (DataSource))]
38+
public char []? Current (Rune rune)
39+
{
40+
_ = Tui.RuneExtensions.DecodeSurrogatePair (rune, out char []? chars);
41+
return chars;
42+
}
43+
44+
/// <summary>
45+
/// Previous implementation with intermediate string allocation.
46+
///
47+
/// The IsSurrogatePair implementation at the time had hidden extra string allocation so there were intermediate heap allocations even if rune is not surrogate pair.
48+
/// </summary>
49+
private static bool RuneToStringToCharArray (Rune rune, out char []? chars)
50+
{
51+
if (rune.IsSurrogatePair ())
52+
{
53+
chars = rune.ToString ().ToCharArray ();
54+
return true;
55+
}
56+
57+
chars = null;
58+
return false;
59+
}
60+
61+
public static IEnumerable<object> DataSource ()
62+
{
63+
yield return new Rune ('a');
64+
yield return "𝔹".EnumerateRunes ().Single ();
65+
}
66+
}
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using System.Text;
2+
using BenchmarkDotNet.Attributes;
3+
using Tui = Terminal.Gui;
4+
5+
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
6+
7+
/// <summary>
8+
/// Benchmarks for <see cref="Tui.RuneExtensions.Encode"/> performance fine-tuning.
9+
/// </summary>
10+
[MemoryDiagnoser]
11+
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
12+
public class Encode
13+
{
14+
/// <summary>
15+
/// Benchmark for previous implementation.
16+
/// </summary>
17+
[Benchmark]
18+
[ArgumentsSource (nameof (DataSource))]
19+
public byte [] Previous (Rune rune, byte [] destination, int start, int count)
20+
{
21+
_ = StringEncodingGetBytes (rune, destination, start, count);
22+
return destination;
23+
}
24+
25+
/// <summary>
26+
/// Benchmark for current implementation.
27+
///
28+
/// Avoids intermediate heap allocations with stack allocated intermediate buffer.
29+
/// </summary>
30+
[Benchmark (Baseline = true)]
31+
[ArgumentsSource (nameof (DataSource))]
32+
public byte [] Current (Rune rune, byte [] destination, int start, int count)
33+
{
34+
_ = Tui.RuneExtensions.Encode (rune, destination, start, count);
35+
return destination;
36+
}
37+
38+
/// <summary>
39+
/// Previous implementation with intermediate byte array and string allocation.
40+
/// </summary>
41+
private static int StringEncodingGetBytes (Rune rune, byte [] dest, int start = 0, int count = -1)
42+
{
43+
byte [] bytes = Encoding.UTF8.GetBytes (rune.ToString ());
44+
var length = 0;
45+
46+
for (var i = 0; i < (count == -1 ? bytes.Length : count); i++)
47+
{
48+
if (bytes [i] == 0)
49+
{
50+
break;
51+
}
52+
53+
dest [start + i] = bytes [i];
54+
length++;
55+
}
56+
57+
return length;
58+
}
59+
60+
public static IEnumerable<object []> DataSource ()
61+
{
62+
Rune[] runes = [ new Rune ('a'),"𝔞".EnumerateRunes().Single() ];
63+
64+
foreach (var rune in runes)
65+
{
66+
yield return new object [] { rune, new byte [16], 0, -1 };
67+
yield return new object [] { rune, new byte [16], 8, -1 };
68+
// Does not work in original implementation
69+
//yield return new object [] { rune, new byte [16], 8, 8 };
70+
}
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Text;
2+
using BenchmarkDotNet.Attributes;
3+
using Tui = Terminal.Gui;
4+
5+
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
6+
7+
/// <summary>
8+
/// Benchmarks for <see cref="Tui.RuneExtensions.EncodeSurrogatePair"/> performance fine-tuning.
9+
/// </summary>
10+
[MemoryDiagnoser]
11+
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
12+
public class EncodeSurrogatePair
13+
{
14+
/// <summary>
15+
/// Benchmark for current implementation.
16+
/// </summary>
17+
[Benchmark (Baseline = true)]
18+
[ArgumentsSource (nameof (DataSource))]
19+
public Rune Current (char highSurrogate, char lowSurrogate)
20+
{
21+
_ = Tui.RuneExtensions.EncodeSurrogatePair (highSurrogate, lowSurrogate, out Rune rune);
22+
return rune;
23+
}
24+
25+
public static IEnumerable<object []> DataSource ()
26+
{
27+
string[] runeStrings = ["🍕", "🧠", "🌹"];
28+
foreach (string symbol in runeStrings)
29+
{
30+
if (symbol is [char high, char low])
31+
{
32+
yield return [high, low];
33+
}
34+
}
35+
}
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
using System.Text;
2+
using BenchmarkDotNet.Attributes;
3+
using Tui = Terminal.Gui;
4+
5+
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
6+
7+
/// <summary>
8+
/// Benchmarks for <see cref="Tui.RuneExtensions.GetEncodingLength"/> performance fine-tuning.
9+
/// </summary>
10+
[MemoryDiagnoser]
11+
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
12+
public class GetEncodingLength
13+
{
14+
/// <summary>
15+
/// Benchmark for previous implementation.
16+
/// </summary>
17+
[Benchmark]
18+
[ArgumentsSource (nameof (DataSource))]
19+
public int Previous (Rune rune, PrettyPrintedEncoding encoding)
20+
{
21+
return WithEncodingGetBytesArray (rune, encoding);
22+
}
23+
24+
/// <summary>
25+
/// Benchmark for current implementation.
26+
/// </summary>
27+
[Benchmark (Baseline = true)]
28+
[ArgumentsSource (nameof (DataSource))]
29+
public int Current (Rune rune, PrettyPrintedEncoding encoding)
30+
{
31+
return Tui.RuneExtensions.GetEncodingLength (rune, encoding);
32+
}
33+
34+
/// <summary>
35+
/// Previous implementation with intermediate byte array, string, and char array allocation.
36+
/// </summary>
37+
private static int WithEncodingGetBytesArray (Rune rune, Encoding? encoding = null)
38+
{
39+
encoding ??= Encoding.UTF8;
40+
byte [] bytes = encoding.GetBytes (rune.ToString ().ToCharArray ());
41+
var offset = 0;
42+
43+
if (bytes [^1] == 0)
44+
{
45+
offset++;
46+
}
47+
48+
return bytes.Length - offset;
49+
}
50+
51+
public static IEnumerable<object []> DataSource ()
52+
{
53+
PrettyPrintedEncoding[] encodings = [ new(Encoding.UTF8), new(Encoding.Unicode), new(Encoding.UTF32) ];
54+
Rune[] runes = [ new Rune ('a'), "𝔹".EnumerateRunes ().Single () ];
55+
56+
foreach (var encoding in encodings)
57+
{
58+
foreach (Rune rune in runes)
59+
{
60+
yield return [rune, encoding];
61+
}
62+
}
63+
}
64+
65+
/// <summary>
66+
/// <see cref="System.Text.Encoding"/> wrapper to display proper encoding name in benchmark results.
67+
/// </summary>
68+
public record PrettyPrintedEncoding (Encoding Encoding)
69+
{
70+
public static implicit operator Encoding (PrettyPrintedEncoding ppe) => ppe.Encoding;
71+
72+
public override string ToString () => Encoding.HeaderName;
73+
}
74+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
using System.Text;
2+
using BenchmarkDotNet.Attributes;
3+
using Tui = Terminal.Gui;
4+
5+
namespace Terminal.Gui.Benchmarks.Text.RuneExtensions;
6+
7+
/// <summary>
8+
/// Benchmarks for <see cref="Tui.RuneExtensions.IsSurrogatePair"/> performance fine-tuning.
9+
/// </summary>
10+
[MemoryDiagnoser]
11+
[BenchmarkCategory (nameof (Tui.RuneExtensions))]
12+
public class IsSurrogatePair
13+
{
14+
/// <summary>
15+
/// Benchmark for previous implementation.
16+
/// </summary>
17+
/// <param name="rune"></param>
18+
[Benchmark]
19+
[ArgumentsSource (nameof (DataSource))]
20+
public bool Previous (Rune rune)
21+
{
22+
return WithToString (rune);
23+
}
24+
25+
/// <summary>
26+
/// Benchmark for current implementation.
27+
///
28+
/// Avoids intermediate heap allocations by using stack allocated buffer.
29+
/// </summary>
30+
[Benchmark (Baseline = true)]
31+
[ArgumentsSource (nameof (DataSource))]
32+
public bool Current (Rune rune)
33+
{
34+
return Tui.RuneExtensions.IsSurrogatePair (rune);
35+
}
36+
37+
/// <summary>
38+
/// Previous implementation with intermediate string allocation.
39+
/// </summary>
40+
private static bool WithToString (Rune rune)
41+
{
42+
return char.IsSurrogatePair (rune.ToString (), 0);
43+
}
44+
45+
public static IEnumerable<object> DataSource ()
46+
{
47+
yield return new Rune ('a');
48+
yield return "𝔹".EnumerateRunes ().Single ();
49+
}
50+
}

CommunityToolkitExample/LoginView.cs

+6-2
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@ public LoginView (LoginViewModel viewModel)
1919
{
2020
ViewModel.Password = passwordInput.Text;
2121
};
22-
loginButton.Accepting += (_, _) =>
22+
loginButton.Accepting += (_, e) =>
2323
{
2424
if (!ViewModel.CanLogin) { return; }
2525
ViewModel.LoginCommand.Execute (null);
26+
// Anytime Accepting is handled, make sure to set e.Cancel to false.
27+
e.Cancel = false;
2628
};
2729

28-
clearButton.Accepting += (_, _) =>
30+
clearButton.Accepting += (_, e) =>
2931
{
3032
ViewModel.ClearCommand.Execute (null);
33+
// Anytime Accepting is handled, make sure to set e.Cancel to false.
34+
e.Cancel = false;
3135
};
3236

3337
Initialized += (_, _) => { ViewModel.Initialized (); };

Example/Example.cs

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public ExampleWindow ()
7878
{
7979
MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
8080
}
81+
// Anytime Accepting is handled, make sure to set e.Cancel to false.
82+
e.Cancel = false;
8183
};
8284

8385
// Add the views to the Window

NativeAot/Program.cs

+2
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ public ExampleWindow ()
105105
{
106106
MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
107107
}
108+
// Anytime Accepting is handled, make sure to set e.Cancel to false.
109+
e.Cancel = false;
108110
};
109111

110112
// Add the views to the Window

Release.ps1 Scripts/Release.ps1

File renamed without changes.
File renamed without changes.

SelfContained/Program.cs

+2
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ public ExampleWindow ()
104104
{
105105
MessageBox.ErrorQuery ("Logging In", "Incorrect username or password", "Ok");
106106
}
107+
// Anytime Accepting is handled, make sure to set e.Cancel to false.
108+
e.Cancel = false;
107109
};
108110

109111
// Add the views to the Window

0 commit comments

Comments
 (0)