Skip to content

Commit d3a60e0

Browse files
committed
Merge branch 'develop' of https://github.com/bitfoundation/bitplatform into feat/12366-aspire-azure-docker-switch
2 parents 3ddedb1 + c39961b commit d3a60e0

7 files changed

Lines changed: 622 additions & 53 deletions

File tree

src/BlazorUI/Bit.BlazorUI/Components/Inputs/Dropdown/BitDropdown.razor.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,6 +1707,8 @@ protected override async ValueTask DisposeAsync(bool disposing)
17071707
{
17081708
if (IsDisposed || disposing is false) return;
17091709

1710+
OnValueChanged -= HandleOnValueChanged;
1711+
17101712
await base.DisposeAsync(disposing);
17111713

17121714
try
@@ -1715,5 +1717,9 @@ protected override async ValueTask DisposeAsync(bool disposing)
17151717
await _js.BitSwipesDispose(_calloutId);
17161718
}
17171719
catch (JSDisconnectedException) { } // we can ignore this exception here
1720+
finally
1721+
{
1722+
_dotnetObj?.Dispose();
1723+
}
17181724
}
17191725
}

src/BlazorUI/Tests/Performance/Bit.BlazorUI.Tests.Performance.TestHost/Components/Pages/BitActionButtonPerf.razor

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
private int RenderedCount { get; set; }
4444
private double RenderTimeMs { get; set; }
4545
private double ReRenderTimeMs { get; set; }
46-
private string Status { get; set; } = "Ready";
46+
private string Status { get; set; } = "Loading";
4747
private bool _isRendered;
4848
private bool _isEnabled = true;
4949
private System.Diagnostics.Stopwatch _stopwatch = new();
@@ -66,6 +66,13 @@
6666

6767
protected override void OnAfterRender(bool firstRender)
6868
{
69+
if (firstRender)
70+
{
71+
Status = "Ready";
72+
StateHasChanged();
73+
return;
74+
}
75+
6976
if (_stopwatch.IsRunning && _isRendered)
7077
{
7178
_stopwatch.Stop();
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
@page "/perf/dropdown"
2+
@page "/perf/dropdown/{Count:int}"
3+
4+
<PageTitle>BitDropdown Performance Test</PageTitle>
5+
6+
<div id="test-controls" style="padding: 20px; background: #f0f0f0; margin-bottom: 20px;">
7+
<h3>BitDropdown Performance Test</h3>
8+
<div style="margin-bottom: 10px;">
9+
<label>Component Count: </label>
10+
<input type="number" @bind="Count" @bind:event="oninput" min="1" max="1000" style="width: 100px;" />
11+
<button @onclick="Render" id="btn-render">Render</button>
12+
<button @onclick="Clear" id="btn-clear">Clear</button>
13+
<button @onclick="ReRender" id="btn-rerender">Re-render (Toggle IsEnabled)</button>
14+
<button @onclick="ToggleResponsive" id="btn-toggle-responsive">
15+
Responsive: @(_responsive ? "ON" : "OFF")
16+
</button>
17+
</div>
18+
<div id="metrics" style="font-family: monospace;">
19+
<p>Render Time: <span id="render-time">@_renderTimeMs</span> ms</p>
20+
<p>Re-render Time: <span id="rerender-time">@_reRenderTimeMs</span> ms</p>
21+
<p>Component Count: <span id="component-count">@_renderedCount</span></p>
22+
<p>Responsive Mode: <span id="responsive-mode">@(_responsive ? "true" : "false")</span></p>
23+
<p>Status: <span id="status">@_status</span></p>
24+
</div>
25+
</div>
26+
27+
<div id="test-container" style="display: flex; flex-wrap: wrap; gap: 8px; padding: 10px;">
28+
@if (_isRendered)
29+
{
30+
@for (int i = 0; i < _renderedCount; i++)
31+
{
32+
var index = i;
33+
<BitDropdown TItem="BitDropdownItem<string>"
34+
TValue="string"
35+
Items="_items"
36+
Placeholder="@($"Dropdown {index}")"
37+
IsEnabled="@_isEnabled"
38+
Responsive="@_responsive"
39+
Style="width: 180px;" />
40+
}
41+
}
42+
</div>
43+
44+
@code {
45+
[Parameter]
46+
public int Count { get; set; } = 50;
47+
48+
private int _renderedCount;
49+
private double _renderTimeMs;
50+
private double _reRenderTimeMs;
51+
// "Loading" is shown in the SSR pre-rendered HTML before the SignalR circuit connects.
52+
// Only OnAfterRenderAsync(firstRender=true) — which never runs during SSR — transitions
53+
// this to "Ready", giving Playwright a reliable "Blazor interactive circuit is up" signal.
54+
private string _status = "Loading";
55+
private bool _isRendered;
56+
private bool _isEnabled = true;
57+
private bool _responsive;
58+
private readonly System.Diagnostics.Stopwatch _stopwatch = new();
59+
60+
// Shared static item list — same items are reused across all dropdown instances
61+
// to keep allocations focused on component lifecycle rather than data.
62+
private static readonly List<BitDropdownItem<string>> _items =
63+
Enumerable.Range(1, 10)
64+
.Select(i => new BitDropdownItem<string> { Text = $"Option {i}", Value = $"val-{i}" })
65+
.ToList();
66+
67+
private void Render()
68+
{
69+
_stopwatch.Restart();
70+
_isRendered = true;
71+
_renderedCount = Count;
72+
_status = "Rendering...";
73+
}
74+
75+
protected override void OnAfterRender(bool firstRender)
76+
{
77+
if (firstRender)
78+
{
79+
// Runs only in interactive mode (never during SSR pre-render).
80+
// Signals to the test that the Blazor circuit is ready to handle events.
81+
_status = "Ready";
82+
StateHasChanged();
83+
return;
84+
}
85+
86+
if (_stopwatch.IsRunning && _isRendered)
87+
{
88+
_stopwatch.Stop();
89+
90+
if (_status == "Rendering...")
91+
{
92+
_renderTimeMs = _stopwatch.Elapsed.TotalMilliseconds;
93+
_status = "Rendered";
94+
}
95+
else if (_status == "Re-rendering...")
96+
{
97+
_reRenderTimeMs = _stopwatch.Elapsed.TotalMilliseconds;
98+
_status = "Re-rendered";
99+
}
100+
101+
StateHasChanged();
102+
}
103+
}
104+
105+
private void Clear()
106+
{
107+
_isRendered = false;
108+
_renderedCount = 0;
109+
_renderTimeMs = 0;
110+
_reRenderTimeMs = 0;
111+
_status = "Cleared";
112+
}
113+
114+
private void ReRender()
115+
{
116+
if (!_isRendered) return;
117+
118+
_stopwatch.Restart();
119+
_isEnabled = !_isEnabled;
120+
_status = "Re-rendering...";
121+
}
122+
123+
private void ToggleResponsive()
124+
{
125+
_responsive = !_responsive;
126+
}
127+
}

src/BlazorUI/Tests/Performance/Bit.BlazorUI.Tests.Performance.TestHost/Components/Pages/Home.razor

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
<li><a href="/perf/action-button/100">BitActionButton - 100 components</a></li>
1414
<li><a href="/perf/action-button/500">BitActionButton - 500 components</a></li>
1515
<li><a href="/perf/action-button/1000">BitActionButton - 1000 components</a></li>
16+
<li><a href="/perf/dropdown">BitDropdown Performance Test</a></li>
17+
<li><a href="/perf/dropdown/10">BitDropdown - 10 components</a></li>
18+
<li><a href="/perf/dropdown/50">BitDropdown - 50 components (memory leak benchmark)</a></li>
19+
<li><a href="/perf/dropdown/100">BitDropdown - 100 components</a></li>
20+
<li><a href="/perf/dropdown/500">BitDropdown - 500 components</a></li>
1621
</ul>
1722

1823
<h2>API Endpoints</h2>

src/BlazorUI/Tests/Performance/Bit.BlazorUI.Tests.Performance.TestHost/Program.cs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,23 @@
2020
app.MapRazorComponents<Bit.BlazorUI.Tests.Performance.TestHost.Components.App>()
2121
.AddInteractiveServerRenderMode();
2222

23+
// Endpoint used by performance tests to force a server-side GC and report heap size.
24+
// This allows tests to verify that server memory returns to near-baseline after
25+
// disposing components, confirming that leaks (e.g. undisposed DotNetObjectReference
26+
// or unsubscribed event handlers) have been resolved.
27+
//
28+
// Two collections are performed — the first queues finalizers, the second reclaims
29+
// them — so the reported figure converges on the steady-state heap and isn't skewed
30+
// by recently-finalized objects.
31+
app.MapGet("/api/gc-info", () =>
32+
{
33+
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true);
34+
GC.WaitForPendingFinalizers();
35+
GC.Collect(GC.MaxGeneration, GCCollectionMode.Forced, blocking: true);
36+
var allocatedMB = GC.GetTotalMemory(forceFullCollection: false) / (1024.0 * 1024.0);
37+
return Results.Ok(new { allocatedMB });
38+
});
39+
2340
app.Run();
2441

2542
// Make Program class accessible for WebApplicationFactory

0 commit comments

Comments
 (0)