Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6a78856
Refactor MermaidPad version and filename from toolbar to separate sta…
udlose Dec 28, 2025
c7a27e6
Improve status text display and main content layout
udlose Dec 28, 2025
d4e1dfa
Add sequenced gate to serialize and deduplicate diagram render reques…
udlose Dec 31, 2025
4ba5ba7
Centralize object pooling, add pooling for StringBuilder, and improve…
udlose Dec 31, 2025
fb6699b
Refactor MermaidRenderer async methods for performance by reducing va…
udlose Dec 31, 2025
3dbbed6
Introduce pooled StringBuilder lease factory pattern
udlose Dec 31, 2025
0d9eef0
Refactor MermaidRenderer path constants and navigation
udlose Dec 31, 2025
549255b
Enhance Rent() docs and add JetBrains annotations
udlose Dec 31, 2025
ec600da
Add bucketed StringBuilder pooling for efficient leasing
udlose Dec 31, 2025
4823c02
Refactor MermaidRenderer to use pooled StringBuilder
udlose Dec 31, 2025
1da000c
Optimize Mermaid source escaping with SearchValues
udlose Dec 31, 2025
c76161d
Restrict service and VM classes to internal visibility
udlose Dec 31, 2025
7fdfe95
Fix SVG export to accept additional options which weren't being passe…
udlose Dec 31, 2025
d79bdd1
Mark a lot of classes as internal to hide implementations
udlose Dec 31, 2025
2d45242
Cleanup some code analysis warnings
udlose Dec 31, 2025
a0549b7
Fix minor bug with dialogs 'PlatformImpl is null, couldn't handle inp…
udlose Jan 1, 2026
b0e039d
Mark a lot of classes as internal to hide implementations
udlose Jan 1, 2026
328caaf
Refactor pooled StringBuilder management in services
udlose Jan 1, 2026
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
9 changes: 5 additions & 4 deletions App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ namespace MermaidPad;
/// running.</remarks>
public sealed partial class App : Application, IDisposable
{
private const int StringBuilderInitialCapacity = 512;
public static IServiceProvider Services { get; private set; } = null!;
private static readonly string[] _newlineCharacters = ["\r\n", "\r", "\n"];

Expand Down Expand Up @@ -377,7 +378,7 @@ private async Task ShowErrorDialogCoreAsync(Exception exception, string userMess
string fullExceptionDetails = BuildExceptionDetails(exception);

// Build user-facing summary
StringBuilder errorSummary = new StringBuilder();
StringBuilder errorSummary = new StringBuilder(StringBuilderInitialCapacity);
errorSummary.AppendLine(userMessage);
errorSummary.AppendLine();
errorSummary.AppendLine($"Error Type: {exception.GetType().FullName}");
Expand Down Expand Up @@ -786,7 +787,7 @@ static async Task SafeSetButtonContentAsync(Button btn, object? content)
/// <param name="threadContext">A string representing the logical context or name of the thread where the exception occurred - e.g. "UI Thread", "Background Thread", "ThreadPool Thread".</param>
private static void LogFatalExceptionWithContext(string message, Exception exception, string threadContext)
{
StringBuilder logEntry = new StringBuilder(256);
StringBuilder logEntry = new StringBuilder(StringBuilderInitialCapacity);
logEntry.AppendLine("---------------------------------------------------------------");
logEntry.AppendLine($"EXCEPTION: {message}");
logEntry.AppendLine($"Time: {DateTime.Now:yyyy-MM-dd HH:mm:ss.fff}");
Expand Down Expand Up @@ -822,7 +823,7 @@ private static void LogFatalExceptionWithContext(string message, Exception excep
/// and stack trace where applicable.</returns>
private static string BuildExceptionDetails(Exception exception)
{
StringBuilder details = new StringBuilder();
StringBuilder details = new StringBuilder(StringBuilderInitialCapacity);

// Handle AggregateException specially to show all inner exceptions
if (exception is AggregateException aggregateException)
Expand Down Expand Up @@ -884,7 +885,7 @@ private static string BuildExceptionDetails(Exception exception)
/// location, source assembly, target site, custom data, and optionally the stack trace.</returns>
private static string FormatSingleException(Exception exception, bool includeStackTrace)
{
StringBuilder details = new StringBuilder();
StringBuilder details = new StringBuilder(StringBuilderInitialCapacity);

details.AppendLine($"Exception Type: {exception.GetType().FullName}");
details.AppendLine($"Message: {exception.Message}");
Expand Down
4 changes: 2 additions & 2 deletions Infrastructure/DialogFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace MermaidPad.Infrastructure;
/// <summary>
/// Factory for creating dialogs with proper dependency injection
/// </summary>
public interface IDialogFactory
internal interface IDialogFactory
{
/// <summary>
/// Creates a dialog window with DI support
Expand All @@ -43,7 +43,7 @@ public interface IDialogFactory
/// <summary>
/// Implementation of dialog factory using the service provider
/// </summary>
public sealed class DialogFactory : IDialogFactory
internal sealed class DialogFactory : IDialogFactory
{
private readonly IServiceProvider _serviceProvider;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// MIT License
// Copyright (c) 2025 Dave Black
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using JetBrains.Annotations;
using System.Text;

namespace MermaidPad.Infrastructure.ObjectPooling;

/// <summary>
/// Defines a factory for obtaining pooled leases of <see cref="StringBuilder"/> instances.
/// </summary>
/// <remarks>Implementations of this interface provide a mechanism to rent and return <see cref="StringBuilder"/> objects from a
/// pool, which can help reduce memory allocations in scenarios with frequent string manipulations. The returned lease
/// typically manages the lifecycle of the pooled <see cref="StringBuilder"/>, ensuring it is returned to the pool when
/// disposed.</remarks>
internal interface IPooledStringBuilderLeaseFactory
{
/// <summary>
/// Rents a pooled <see cref="StringBuilder"/> instance wrapped in a <see cref="PooledStringBuilderLease"/>.
/// </summary>
/// <remarks>
/// <para>
/// The caller:
/// 1. Must dispose of the returned <see cref="PooledStringBuilderLease"/> to return the <see cref="StringBuilder"/> to the pool
/// as indicated by the <see cref="MustDisposeResourceAttribute"/>.
/// 2. Must avoid holding onto the <see cref="StringBuilder"/> reference beyond the scope of the lease.
/// 3. Must use the return value as indicated by the <see cref="MustUseReturnValueAttribute"/>.
/// </para>
/// </remarks>
/// <returns>A <see cref="PooledStringBuilderLease"/> that provides exclusive access to a pooled <see cref="StringBuilder"/>.
/// The lease must be disposed to return the <see cref="StringBuilder"/> to the pool.</returns>
[MustDisposeResource]
[MustUseReturnValue]
PooledStringBuilderLease Rent();

/// <summary>
/// Rents a pooled <see cref="StringBuilder"/> instance wrapped in a <see cref="PooledStringBuilderLease"/>.
/// </summary>
/// <param name="minimumCapacity">The minimum capacity the rented <see cref="StringBuilder"/> should have.</param>
/// <remarks>
/// <para>
/// The caller:
/// 1. Must dispose of the returned <see cref="PooledStringBuilderLease"/> to return the <see cref="StringBuilder"/> to the pool
/// as indicated by the <see cref="MustDisposeResourceAttribute"/>.
/// 2. Must avoid holding onto the <see cref="StringBuilder"/> reference beyond the scope of the lease.
/// 3. Must use the return value as indicated by the <see cref="MustUseReturnValueAttribute"/>.
/// </para>
/// </remarks>
/// <returns>A <see cref="PooledStringBuilderLease"/> that provides exclusive access to a pooled <see cref="StringBuilder"/>.
/// The lease must be disposed to return the <see cref="StringBuilder"/> to the pool.</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown when <paramref name="minimumCapacity"/> is less than or equal to zero.</exception>
[MustDisposeResource]
[MustUseReturnValue]
PooledStringBuilderLease Rent(int minimumCapacity);
}
103 changes: 103 additions & 0 deletions Infrastructure/ObjectPooling/ObjectPoolRegistrations.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// MIT License
// Copyright (c) 2025 Dave Black
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using MermaidPad.ObjectPoolPolicies;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using System.Text;

namespace MermaidPad.Infrastructure.ObjectPooling;

/// <summary>
/// Provides registration methods for configuring object pooling services, such as StringBuilder pooling, within a
/// dependency injection container.
/// </summary>
/// <remarks>This class is intended for internal use to centralize object pool service registrations. It is not
/// intended to be used directly by application code.</remarks>
internal static class ObjectPoolRegistrations
{
/// <summary>
/// Adds object pooling services to the specified <see cref="IServiceCollection"/>. Registers default object pool
/// providers and policies for use within the application.
/// </summary>
/// <remarks>This method registers a default <see cref="ObjectPoolProvider"/> and configures object pools
/// for commonly used types such as <see cref="HashSet{String}"/> and <see cref="StringBuilder"/>. Call this method
/// during service configuration to enable efficient object reuse and reduce memory allocations.</remarks>
/// <param name="services">The <see cref="IServiceCollection"/> to which the object pooling services are added. Cannot be null.</param>
internal static void AddObjectPooling(this IServiceCollection services)
{
// Register the default object pool provider first if one is not already registered
services.TryAddSingleton<ObjectPoolProvider, DefaultObjectPoolProvider>();
services.TryAddSingleton<ObjectPool<HashSet<string>>>(static sp =>
{
ObjectPoolProvider provider = sp.GetRequiredService<ObjectPoolProvider>();
HashSetPooledObjectPolicy policy = new HashSetPooledObjectPolicy();
return provider.Create(policy);
});

services.TryAddSingleton<IPooledStringBuilderLeaseFactory>(CreatePooledStringBuilderLeaseFactory);
}

/// <summary>
/// Creates an object pool for <see cref="StringBuilder"/> instances using the specified service provider.
/// </summary>
/// <remarks>The returned <see cref="IPooledStringBuilderLeaseFactory"/> is configured to provide a pool with
/// an initial capacity of 256 characters and a maximum retained capacity of 16,384 characters per
/// <see cref="StringBuilder"/> instance. Using a pool can improve performance by reducing allocations when working with temporary strings.</remarks>
/// <param name="serviceProvider">The service provider used to retrieve the <see cref="ObjectPoolProvider"/> required to create the pool.
/// Cannot be null.</param>
/// <returns>An <see cref="IPooledStringBuilderLeaseFactory"/> that provides access to pooled <see cref="StringBuilder"/> instances.</returns>
private static IPooledStringBuilderLeaseFactory CreatePooledStringBuilderLeaseFactory(IServiceProvider serviceProvider)
{
const int oneKiloByte = 1_024;
ObjectPoolProvider poolProvider = serviceProvider.GetRequiredService<ObjectPoolProvider>();
StringBuilderPooledObjectPolicy policy256 = new StringBuilderPooledObjectPolicy
{
InitialCapacity = 256,
MaximumRetainedCapacity = 256
};

StringBuilderPooledObjectPolicy policy1024 = new StringBuilderPooledObjectPolicy
{
InitialCapacity = oneKiloByte,
MaximumRetainedCapacity = oneKiloByte
};

StringBuilderPooledObjectPolicy policy4096 = new StringBuilderPooledObjectPolicy
{
InitialCapacity = 4 * oneKiloByte,
MaximumRetainedCapacity = 4 * oneKiloByte
};

StringBuilderPooledObjectPolicy policy16384 = new StringBuilderPooledObjectPolicy
{
InitialCapacity = 16 * oneKiloByte,
MaximumRetainedCapacity = 16 * oneKiloByte
};

ObjectPool<StringBuilder> pool256 = poolProvider.Create(policy256);
ObjectPool<StringBuilder> pool1024 = poolProvider.Create(policy1024);
ObjectPool<StringBuilder> pool4096 = poolProvider.Create(policy4096);
ObjectPool<StringBuilder> pool16384 = poolProvider.Create(policy16384);

return new PooledStringBuilderBucketedLeaseFactory(pool256, pool1024, pool4096, pool16384);
}
}
Loading