diff --git a/.gitignore b/.gitignore
index 2e5faf7..45158ca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -29,3 +29,4 @@ imagecache
/src/.vs
src/.idea/.idea.MessageFormat/.idea/workspace.xml
.DS_Store
+BenchmarkDotNet.Artifacts/
diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj b/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj
new file mode 100644
index 0000000..6315f90
--- /dev/null
+++ b/src/Jeffijoe.MessageFormat.Benchmarks/Jeffijoe.MessageFormat.Benchmarks.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+ latest
+ enable
+ enable
+ True
+ MessageFormat.snk
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk
new file mode 100644
index 0000000..ba4de3e
Binary files /dev/null and b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormat.snk differ
diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs
new file mode 100644
index 0000000..e7cdf05
--- /dev/null
+++ b/src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using BenchmarkDotNet.Attributes;
+using Jeffijoe.MessageFormat;
+
+namespace Jeffijoe.MessageFormat.Benchmarks;
+
+[MemoryDiagnoser]
+public class MessageFormatterBenchmarks
+{
+ private MessageFormatter _formatter = null!;
+
+ private readonly Dictionary _simpleArgs = new() { ["name"] = "World" };
+
+ private readonly Dictionary _pluralSimpleArgs = new() { ["count"] = 5 };
+
+ private readonly Dictionary _selectSimpleArgs = new() { ["gender"] = "male" };
+
+ private readonly Dictionary _pluralOffsetArgs = new() { ["count"] = 3 };
+
+ private readonly Dictionary _nested2Args = new() { ["gender"] = "female", ["count"] = 1 };
+
+ private readonly Dictionary _nested3Args = new()
+ {
+ ["gender"] = "male",
+ ["count"] = 2,
+ ["total"] = 10
+ };
+
+ [GlobalSetup]
+ public void Setup()
+ {
+ _formatter = new MessageFormatter();
+ }
+
+ [Benchmark]
+ public string SimpleSubstitution()
+ {
+ return _formatter.FormatMessage("{name}", _simpleArgs);
+ }
+
+ [Benchmark]
+ public string PluralSimple()
+ {
+ return _formatter.FormatMessage(
+ "{count, plural, one {1 thing} other {# things}}",
+ _pluralSimpleArgs);
+ }
+
+ [Benchmark]
+ public string SelectSimple()
+ {
+ return _formatter.FormatMessage(
+ "{gender, select, male {He} female {She} other {They}}",
+ _selectSimpleArgs);
+ }
+
+ [Benchmark]
+ public string PluralWithOffset()
+ {
+ return _formatter.FormatMessage(
+ "{count, plural, offset:1 =0 {Nobody} one {You and one other} other {You and # others}}",
+ _pluralOffsetArgs);
+ }
+
+ [Benchmark]
+ public string Nested2Levels()
+ {
+ return _formatter.FormatMessage(
+ "{gender, select, male {{count, plural, one {He has 1 item} other {He has # items}}} female {{count, plural, one {She has 1 item} other {She has # items}}} other {{count, plural, one {They have 1 item} other {They have # items}}}}",
+ _nested2Args);
+ }
+
+ [Benchmark]
+ public string Nested3Levels()
+ {
+ return _formatter.FormatMessage(
+ "{gender, select, male {{count, plural, one {He has 1 of {total} items} other {He has # of {total} items}}} female {{count, plural, one {She has 1 of {total} items} other {She has # of {total} items}}} other {{count, plural, one {They have 1 of {total} items} other {They have # of {total} items}}}}",
+ _nested3Args);
+ }
+
+ [Params(1, 2, 4, 8)]
+ public int ThreadCount { get; set; }
+
+ [Benchmark]
+ public void MultiThreadFormatMessage()
+ {
+ var args = new Dictionary { ["count"] = 5 };
+ var pattern = "{count, plural, one {1 thing} other {# things}}";
+
+ var tasks = new Task[ThreadCount];
+ for (var t = 0; t < ThreadCount; t++)
+ {
+ tasks[t] = Task.Run(() =>
+ {
+ for (var i = 0; i < 1000; i++)
+ {
+ _formatter.FormatMessage(pattern, args);
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+ }
+}
diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs b/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs
new file mode 100644
index 0000000..9948aa8
--- /dev/null
+++ b/src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs
@@ -0,0 +1,45 @@
+using System.Text;
+using BenchmarkDotNet.Attributes;
+using Jeffijoe.MessageFormat;
+
+namespace Jeffijoe.MessageFormat.Benchmarks;
+
+[MemoryDiagnoser]
+public class PoolBenchmarks
+{
+ private const int OperationsPerThread = 1000;
+
+ [Benchmark]
+ public void SingleThreadGetReturn()
+ {
+ for (var i = 0; i < OperationsPerThread; i++)
+ {
+ var sb = StringBuilderPool.Get();
+ sb.Append("test");
+ StringBuilderPool.Return(sb);
+ }
+ }
+
+ [Params(1, 2, 4, 8)]
+ public int ThreadCount { get; set; }
+
+ [Benchmark]
+ public void MultiThreadGetReturn()
+ {
+ var tasks = new Task[ThreadCount];
+ for (var t = 0; t < ThreadCount; t++)
+ {
+ tasks[t] = Task.Run(() =>
+ {
+ for (var i = 0; i < OperationsPerThread; i++)
+ {
+ var sb = StringBuilderPool.Get();
+ sb.Append("test");
+ StringBuilderPool.Return(sb);
+ }
+ });
+ }
+
+ Task.WaitAll(tasks);
+ }
+}
diff --git a/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs b/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs
new file mode 100644
index 0000000..c9a0467
--- /dev/null
+++ b/src/Jeffijoe.MessageFormat.Benchmarks/Program.cs
@@ -0,0 +1,3 @@
+using BenchmarkDotNet.Running;
+
+BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
diff --git a/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs b/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs
new file mode 100644
index 0000000..c530fd3
--- /dev/null
+++ b/src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs
@@ -0,0 +1,79 @@
+using System.Text;
+using System.Threading.Tasks;
+
+using Xunit;
+
+namespace Jeffijoe.MessageFormat.Tests;
+
+public class ObjectPoolTests
+{
+ [Fact]
+ public void Allocate_WhenPoolEmpty_ReturnsNewObject()
+ {
+ var pool = new ObjectPool(() => new StringBuilder());
+ var sb = pool.Allocate();
+ Assert.NotNull(sb);
+ }
+
+ [Fact]
+ public void Free_ThenAllocate_ReturnsSameInstance()
+ {
+ var pool = new ObjectPool(() => new StringBuilder());
+ var sb = pool.Allocate();
+ pool.Free(sb);
+ var sb2 = pool.Allocate();
+ Assert.Same(sb, sb2);
+ }
+
+ [Fact]
+ public void Allocate_BeyondPoolSize_CreatesNewObjects()
+ {
+ var pool = new ObjectPool(() => new StringBuilder(), size: 2);
+ var a = pool.Allocate();
+ var b = pool.Allocate();
+ var c = pool.Allocate();
+ Assert.NotSame(a, b);
+ Assert.NotSame(b, c);
+ Assert.NotSame(a, c);
+ }
+
+ [Fact]
+ public void Free_BeyondPoolSize_DoesNotThrow()
+ {
+ var pool = new ObjectPool(() => new StringBuilder(), size: 2);
+ var a = pool.Allocate();
+ var b = pool.Allocate();
+ var c = pool.Allocate();
+ pool.Free(a);
+ pool.Free(b);
+ pool.Free(c); // exceeds pool size, should silently discard
+ }
+
+ [Fact]
+ public async Task ConcurrentAllocateAndFree_DoesNotThrow()
+ {
+ var pool = new ObjectPool(() => new StringBuilder());
+ const int ThreadCount = 8;
+ const int Iterations = 1000;
+
+ var tasks = new Task[ThreadCount];
+ for (var t = 0; t < ThreadCount; t++)
+ {
+ tasks[t] = Task.Run(() =>
+ {
+ for (var i = 0; i < Iterations; i++)
+ {
+ var sb = pool.Allocate();
+ sb.Append("test");
+ var output = sb.ToString();
+ // Assert we didn't get a dirty builder with data still left in it.
+ Assert.Equal("test", output);
+ sb.Clear();
+ pool.Free(sb);
+ }
+ });
+ }
+
+ await Task.WhenAll(tasks);
+ }
+}
diff --git a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj
index e17ea51..793e4f7 100644
--- a/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj
+++ b/src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj
@@ -20,7 +20,6 @@
-
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -35,5 +34,9 @@
+
+
+
+
diff --git a/src/Jeffijoe.MessageFormat/ObjectPool.cs b/src/Jeffijoe.MessageFormat/ObjectPool.cs
new file mode 100644
index 0000000..6b26c26
--- /dev/null
+++ b/src/Jeffijoe.MessageFormat/ObjectPool.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+// Ported from Roslyn, see: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+namespace Jeffijoe.MessageFormat;
+
+///
+/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose
+/// is that limited number of frequently used objects can be kept in the pool for further recycling.
+///
+/// The type of objects to pool.
+internal sealed class ObjectPool(Func factory, int size)
+ where T : class
+{
+ private readonly Element[] _items = new Element[size - 1];
+ private T? _firstItem;
+
+ public ObjectPool(Func factory)
+ : this(factory, Environment.ProcessorCount * 2)
+ {
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T Allocate()
+ {
+ var item = _firstItem;
+
+ if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item))
+ {
+ item = AllocateSlow();
+ }
+
+ return item;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Free(T obj)
+ {
+ if (_firstItem is null)
+ {
+ _firstItem = obj;
+ }
+ else
+ {
+ FreeSlow(obj);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private T AllocateSlow()
+ {
+ foreach (ref var element in _items.AsSpan())
+ {
+ var instance = element.Value;
+
+ if (instance is null)
+ {
+ continue;
+ }
+
+ if (instance == Interlocked.CompareExchange(ref element.Value, null, instance))
+ {
+ return instance;
+ }
+ }
+
+
+ return factory();
+ }
+
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void FreeSlow(T obj)
+ {
+ foreach (ref var element in _items.AsSpan())
+ {
+ if (element.Value is not null)
+ {
+ continue;
+ }
+
+ element.Value = obj;
+ break;
+ }
+ }
+
+ private struct Element
+ {
+ internal T? Value;
+ }
+}
diff --git a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs
index f9ea5d5..8df6583 100644
--- a/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs
+++ b/src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs
@@ -6,3 +6,4 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")]
+[assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")]
diff --git a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs
index f91fc26..7d0a1cb 100644
--- a/src/Jeffijoe.MessageFormat/StringBuilderPool.cs
+++ b/src/Jeffijoe.MessageFormat/StringBuilderPool.cs
@@ -1,25 +1,28 @@
-using System.Text;
-using Microsoft.Extensions.ObjectPool;
+using System.Text;
namespace Jeffijoe.MessageFormat;
internal static class StringBuilderPool
{
- private static readonly ObjectPool SbPool;
+ private const int MaxBuilderCapacity = 4096;
- static StringBuilderPool()
- {
- var shared = new DefaultObjectPoolProvider();
- SbPool = shared.CreateStringBuilderPool();
- }
+ private static readonly ObjectPool SbPool = new(static () => new StringBuilder());
public static StringBuilder Get()
{
- return SbPool.Get();
+ return SbPool.Allocate();
}
public static void Return(StringBuilder sb)
{
- SbPool.Return(sb);
+ // If the builder grew too large, just let it go
+ // rather than returning it so it can get garbage-collected.
+ if (sb.Capacity > MaxBuilderCapacity)
+ {
+ return;
+ }
+
+ sb.Clear();
+ SbPool.Free(sb);
}
-}
\ No newline at end of file
+}
diff --git a/src/MessageFormat.sln b/src/MessageFormat.sln
index 3a62c07..d682f2e 100644
--- a/src/MessageFormat.sln
+++ b/src/MessageFormat.sln
@@ -9,24 +9,66 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jeffijoe.MessageFormat.Test
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.MetadataGenerator", "Jeffijoe.MessageFormat.MetadataGenerator\Jeffijoe.MessageFormat.MetadataGenerator.csproj", "{5431C848-23D1-4752-A9B0-5159E5B2F92E}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jeffijoe.MessageFormat.Benchmarks", "Jeffijoe.MessageFormat.Benchmarks\Jeffijoe.MessageFormat.Benchmarks.csproj", "{D63A7E6E-D302-44E2-A355-F72DD005AB57}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x64.Build.0 = Debug|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Debug|x86.Build.0 = Debug|Any CPU
{7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x64.ActiveCfg = Release|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x64.Build.0 = Release|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x86.ActiveCfg = Release|Any CPU
+ {7D16B114-A482-4FC4-A055-8E96573BE2A3}.Release|x86.Build.0 = Release|Any CPU
{F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x64.Build.0 = Debug|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Debug|x86.Build.0 = Debug|Any CPU
{F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x64.ActiveCfg = Release|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x64.Build.0 = Release|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x86.ActiveCfg = Release|Any CPU
+ {F1AC744E-9031-468E-A397-6F44AA19EBA1}.Release|x86.Build.0 = Release|Any CPU
{5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x64.Build.0 = Debug|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Debug|x86.Build.0 = Debug|Any CPU
{5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x64.ActiveCfg = Release|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x64.Build.0 = Release|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x86.ActiveCfg = Release|Any CPU
+ {5431C848-23D1-4752-A9B0-5159E5B2F92E}.Release|x86.Build.0 = Release|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x64.Build.0 = Debug|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Debug|x86.Build.0 = Debug|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x64.ActiveCfg = Release|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x64.Build.0 = Release|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x86.ActiveCfg = Release|Any CPU
+ {D63A7E6E-D302-44E2-A355-F72DD005AB57}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE