src/MudBlazor.Benchmarks/ARCHITECTURE_ANALYSIS.md
This document provides an architectural analysis of the MudBlazor ParameterState framework, focusing on potential performance bottlenecks and optimization opportunities in the core classes.
File: src/MudBlazor/State/ParameterStateInternalOfT.cs
// Called during SetValueAsync (user-initiated changes)
public override Task SetValueAsync(T value)
{
if (!_comparer.Equals(_value, value)) // Equality check
{
_value = value;
var eventCallback = _eventCallbackFunc(); // Delegate invocation
if (eventCallback.HasDelegate)
{
return eventCallback.InvokeAsync(value);
}
}
return Task.CompletedTask;
}
// Called on EVERY render (OnParametersSet lifecycle)
public void OnParametersSet()
{
var currentParameterValue = _getParameterValueFunc(); // Delegate invocation
if (!_comparer.Equals(_lastValue, currentParameterValue)) // Equality check
{
_isChildOriginatedChange = _comparer.Equals(_value, currentParameterValue);
_value = currentParameterValue;
_lastValue = currentParameterValue;
}
}
ā Good Patterns:
Task.CompletedTask reuse (no allocations)ā ļø Potential Issues:
_getParameterValueFunc() + _eventCallbackFunc() + potentially _comparer.UnderlyingComparer()_lastValue vs currentParameterValue, another for child origin detection_eventCallbackFunc() to check HasDelegateLOW PRIORITY: Delegate invocations are cheap (1-2ns). Not worth optimizing unless profiling shows otherwise.
File: src/MudBlazor/State/ParameterScopeContainer.cs
private readonly Lazy<FrozenDictionary<string, IParameterComponentLifeCycle>> _parameters;
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out IParameterComponentLifeCycle parameterComponentLifeCycle)
{
return _parameters.Value.TryGetValue(parameterName, out parameterComponentLifeCycle);
}
ā Good Patterns:
ā ļø You mentioned "frozen set is slow" - Let me clarify:
FrozenDictionary Performance (since .NET 8):
The FrozenDictionary choice is CORRECT here because:
ā Actual Architectural Issue: The lock mechanism via IsLocked is good, but there's a subtle issue:
private FrozenDictionary<string, IParameterComponentLifeCycle> ParametersFactory()
{
IsLocked = true; // Lock BEFORE creating dictionary
var parameters = _parameterStatesReader.ReadParameters();
var dictionary = parameters.ToFrozenDictionary(...); // LINQ materialization + freezing
_parameterStatesReader.Complete();
return dictionary;
}
Problem: ToFrozenDictionary does:
ReadParameters() (LINQ)Better approach: Use FrozenDictionary.ToFrozenDictionary directly if parameters are already enumerable, or pre-size:
// If you know parameter count, you can optimize:
var parameters = _parameterStatesReader.ReadParameters();
var dictionary = parameters.ToFrozenDictionary(
parameter => parameter.Metadata.ParameterName,
parameter => parameter,
StringComparer.Ordinal); // Add explicit comparer to avoid default
MEDIUM PRIORITY:
File: src/MudBlazor/State/ParameterContainer.cs
public async Task SetParametersAsync(Func<ParameterView, Task> baseSetParametersAsync, ParameterView parameters)
{
// ... snip ...
var parametersHandlerShouldFire = _parameterScopeContainers.SelectMany(parameter => parameter)
.Where(parameter => parameter.HasHandler && parameter.HasParameterChanged(parameters))
.Select(x => x.CreateInvocationSnapshot())
.ToHashSet(ParameterHandlerUniquenessComparer.Default); // ā ALLOCATION
await baseSetParametersAsync(parameters);
foreach (var parameterHandlerShouldFire in parametersHandlerShouldFire)
{
await parameterHandlerShouldFire.ParameterChangeHandleAsync();
}
}
Problem: LINQ chain + ToHashSet() on EVERY RENDER
This allocates:
Impact: For a component with 50 parameters and 5 that have handlers:
Option 1: Pre-allocate or use ArrayPool
// Use a List instead of HashSet if uniqueness isn't critical
var parametersHandlerShouldFire = new List<IParameterStateInvocationSnapshot>();
foreach (var scopeContainer in _parameterScopeContainers)
{
foreach (var parameter in scopeContainer)
{
if (parameter.HasHandler && parameter.HasParameterChanged(parameters))
{
parametersHandlerShouldFire.Add(parameter.CreateInvocationSnapshot());
}
}
}
Option 2: Fast path for no handlers
// Early return if no parameters have handlers (common for display-only components)
if (_parameterScopeContainers.All(scope => scope.All(p => !p.HasHandler)))
{
await baseSetParametersAsync(parameters);
return;
}
Option 3: Cache handlers count
private int _handlerCount; // Set during initialization
public async Task SetParametersAsync(...)
{
if (_handlerCount == 0)
{
// Fast path: no change handlers exist
await baseSetParametersAsync(parameters);
return;
}
// ... existing logic
}
File: src/MudBlazor/Extensions/ComponentBaseWithStateExtensions.cs
public static T GetState<T>(this ComponentBaseWithState component, string propertyName)
{
if (component.ParameterContainer.TryGetValue(propertyName, out var lifeCycle))
{
if (lifeCycle is ParameterStateInternal<T> parameterState)
{
return parameterState.Value;
}
}
throw new KeyNotFoundException($"ParameterState<{typeof(T).Name}> with {propertyName} was not found!");
}
Which calls:
// ParameterContainer.cs
public bool TryGetValue(string parameterName, [MaybeNullWhen(false)] out IParameterComponentLifeCycle parameterComponentLifeCycle)
{
foreach (var parameterSet in _parameterScopeContainers) // ā LINEAR SEARCH
{
if (parameterSet.TryGetValue(parameterName, out parameterComponentLifeCycle))
{
return true;
}
}
parameterComponentLifeCycle = null;
return false;
}
Problem: Linear search through multiple scopes on EVERY GetState call
Scenario: Component with 3 scopes (e.g., inherited from base classes):
GetState("MyParameter") in Scope 3:
3 dictionary lookups instead of 1!
Option 1: Flatten to single FrozenDictionary on first access
private Lazy<FrozenDictionary<string, IParameterComponentLifeCycle>> _flattenedParameters;
public ParameterContainer()
{
_flattenedParameters = new Lazy<FrozenDictionary<string, IParameterComponentLifeCycle>>(FlattenParameters);
}
private FrozenDictionary<string, IParameterComponentLifeCycle> FlattenParameters()
{
return _parameterScopeContainers
.SelectMany(scope => scope)
.ToFrozenDictionary(p => p.Metadata.ParameterName, p => p, StringComparer.Ordinal);
}
public bool TryGetValue(string parameterName, ...)
{
return _flattenedParameters.Value.TryGetValue(parameterName, out parameterComponentLifeCycle);
}
Trade-offs:
This is likely worth it because:
File: src/MudBlazor/State/Rule/ParameterMetadataRules.cs
private static readonly IExclusion[] _exclusions =
[
new HandlerLambdaExclusion(),
new ComparerParameterLambdaExclusion()
];
public static ParameterMetadata Morph(ParameterMetadata originalMetadata)
{
var currentMetaData = originalMetadata;
foreach (var exclusion in _exclusions) // Only 2 items
{
if (exclusion.IsExclusion(originalMetadata, out var newMetadata))
{
currentMetaData = newMetadata;
}
}
return currentMetaData;
}
ā Good Pattern:
No optimization needed - this is fine.
ParameterContainer.TryGetValue: Flatten scopes to single FrozenDictionary
ParameterContainer.SetParametersAsync: Eliminate LINQ + ToHashSet allocation
Add fast path for components without change handlers
These optimizations address actual architectural issues rather than micro-optimizations that don't matter.