Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ imagecache
/src/.vs
src/.idea/.idea.MessageFormat/.idea/workspace.xml
.DS_Store
BenchmarkDotNet.Artifacts/
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>MessageFormat.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.15.8" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Jeffijoe.MessageFormat\Jeffijoe.MessageFormat.csproj" />
</ItemGroup>

</Project>
Binary file not shown.
104 changes: 104 additions & 0 deletions src/Jeffijoe.MessageFormat.Benchmarks/MessageFormatterBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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<string, object?> _simpleArgs = new() { ["name"] = "World" };

private readonly Dictionary<string, object?> _pluralSimpleArgs = new() { ["count"] = 5 };

private readonly Dictionary<string, object?> _selectSimpleArgs = new() { ["gender"] = "male" };

private readonly Dictionary<string, object?> _pluralOffsetArgs = new() { ["count"] = 3 };

private readonly Dictionary<string, object?> _nested2Args = new() { ["gender"] = "female", ["count"] = 1 };

private readonly Dictionary<string, object?> _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<string, object?> { ["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);
}
}
45 changes: 45 additions & 0 deletions src/Jeffijoe.MessageFormat.Benchmarks/PoolBenchmarks.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
3 changes: 3 additions & 0 deletions src/Jeffijoe.MessageFormat.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
using BenchmarkDotNet.Running;

BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
79 changes: 79 additions & 0 deletions src/Jeffijoe.MessageFormat.Tests/ObjectPoolTests.cs
Original file line number Diff line number Diff line change
@@ -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<StringBuilder>(() => new StringBuilder());
var sb = pool.Allocate();
Assert.NotNull(sb);
}

[Fact]
public void Free_ThenAllocate_ReturnsSameInstance()
{
var pool = new ObjectPool<StringBuilder>(() => 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<StringBuilder>(() => 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<StringBuilder>(() => 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<StringBuilder>(() => 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);
}
}
5 changes: 4 additions & 1 deletion src/Jeffijoe.MessageFormat/Jeffijoe.MessageFormat.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.ObjectPool" Version="10.0.3" />
<PackageReference Include="MinVer" Version="7.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand All @@ -35,5 +34,9 @@
<ProjectReference Include="../Jeffijoe.MessageFormat.MetadataGenerator/Jeffijoe.MessageFormat.MetadataGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<PackageReference Include="System.Memory" Version="4.6.3" />
</ItemGroup>


</Project>
96 changes: 96 additions & 0 deletions src/Jeffijoe.MessageFormat/ObjectPool.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
/// <typeparam name="T">The type of objects to pool.</typeparam>
internal sealed class ObjectPool<T>(Func<T> factory, int size)
where T : class
{
private readonly Element[] _items = new Element[size - 1];
private T? _firstItem;

public ObjectPool(Func<T> 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;
}
}
1 change: 1 addition & 0 deletions src/Jeffijoe.MessageFormat/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
using System.Runtime.CompilerServices;

[assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")]
[assembly: InternalsVisibleTo("Jeffijoe.MessageFormat.Benchmarks, PublicKey=00240000048000009400000006020000002400005253413100040000010001004f0dc10ad8873751f986416a7d3134e473ad3454a7138e26cf9760eff341709fc50e69d985b4e0ea512b5628079043a31ce1e402f4eaebffb5772eed9ef3a7a9e39f122633f19529a1e9988005289ed09a1e294abe13152afebc59835c2fef2879d8641b564daa7f511298ec2f8a3e9b322819e05cbaea0bfc73fe100615bfc7")]
Loading