Skip to content

Commit ffddf2d

Browse files
Merge pull request #486 from SixLabors/js/fix-484
Use threadsafe pooling for TrueType Interpreter
2 parents e831f53 + 64eee53 commit ffddf2d

File tree

6 files changed

+244
-27
lines changed

6 files changed

+244
-27
lines changed

src/SixLabors.Fonts/Buffer{T}.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public Buffer(int length)
2727
this.length = length;
2828

2929
using ByteMemoryManager<T> manager = new(this.buffer);
30-
this.Memory = manager.Memory.Slice(0, this.length);
30+
this.Memory = manager.Memory[..this.length];
3131
this.span = this.Memory.Span;
3232

3333
this.isDisposed = false;
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
using System.Collections.Concurrent;
5+
6+
namespace SixLabors.Fonts;
7+
8+
/// <summary>
9+
/// A pool for reusing objects of type <typeparamref name="T"/>.
10+
/// </summary>
11+
/// <typeparam name="T">The type to pool objects for.</typeparam>
12+
/// <remarks>
13+
/// This implementation keeps a cache of retained objects.
14+
/// This means that if objects are returned when the pool has already reached "maximumRetained" objects they will be available to be Garbage Collected.
15+
/// </remarks>
16+
internal sealed class ObjectPool<T>
17+
where T : class
18+
{
19+
private readonly Func<T> createFunc;
20+
private readonly Func<T, bool> returnFunc;
21+
private readonly int maxCapacity;
22+
private int numItems;
23+
24+
private readonly ConcurrentQueue<T> items = new();
25+
private T? fastItem;
26+
27+
/// <summary>
28+
/// Initializes a new instance of the <see cref="ObjectPool{T}"/> class.
29+
/// </summary>
30+
/// <param name="policy">The pooling policy to use.</param>
31+
public ObjectPool(IPooledObjectPolicy<T> policy)
32+
: this(policy, Environment.ProcessorCount * 2)
33+
{
34+
}
35+
36+
/// <summary>
37+
/// Initializes a new instance of the <see cref="ObjectPool{T}"/> class.
38+
/// </summary>
39+
/// <param name="policy">The pooling policy to use.</param>
40+
/// <param name="maximumRetained">The maximum number of objects to retain in the pool.</param>
41+
public ObjectPool(IPooledObjectPolicy<T> policy, int maximumRetained)
42+
{
43+
// cache the target interface methods, to avoid interface lookup overhead
44+
this.createFunc = policy.Create;
45+
this.returnFunc = policy.Return;
46+
this.maxCapacity = maximumRetained - 1; // -1 to account for fastItem
47+
}
48+
49+
/// <summary>
50+
/// Gets an object from the pool if one is available, otherwise creates one.
51+
/// </summary>
52+
/// <returns>A <typeparamref name="T"/>.</returns>
53+
public T Get()
54+
{
55+
T? item = this.fastItem;
56+
if (item == null || Interlocked.CompareExchange(ref this.fastItem, null, item) != item)
57+
{
58+
if (this.items.TryDequeue(out item))
59+
{
60+
_ = Interlocked.Decrement(ref this.numItems);
61+
return item;
62+
}
63+
64+
// no object available, so go get a brand new one
65+
return this.createFunc();
66+
}
67+
68+
return item;
69+
}
70+
71+
/// <summary>
72+
/// Return an object to the pool.
73+
/// </summary>
74+
/// <param name="obj">The object to add to the pool.</param>
75+
public void Return(T obj) => this.ReturnCore(obj);
76+
77+
/// <summary>
78+
/// Returns an object to the pool.
79+
/// </summary>
80+
/// <returns>true if the object was returned to the pool</returns>
81+
private bool ReturnCore(T obj)
82+
{
83+
if (!this.returnFunc(obj))
84+
{
85+
// policy says to drop this object
86+
return false;
87+
}
88+
89+
if (this.fastItem != null || Interlocked.CompareExchange(ref this.fastItem, obj, null) != null)
90+
{
91+
if (Interlocked.Increment(ref this.numItems) <= this.maxCapacity)
92+
{
93+
this.items.Enqueue(obj);
94+
return true;
95+
}
96+
97+
// no room, clean up the count and drop the object on the floor
98+
_ = Interlocked.Decrement(ref this.numItems);
99+
return false;
100+
}
101+
102+
return true;
103+
}
104+
}
105+
106+
/// <summary>
107+
/// Represents a policy for managing pooled objects.
108+
/// </summary>
109+
/// <typeparam name="T">The type of object which is being pooled.</typeparam>
110+
#pragma warning disable SA1201 // Elements should appear in the correct order
111+
internal interface IPooledObjectPolicy<T>
112+
#pragma warning restore SA1201 // Elements should appear in the correct order
113+
where T : notnull
114+
{
115+
/// <summary>
116+
/// Create a <typeparamref name="T"/>.
117+
/// </summary>
118+
/// <returns>The <typeparamref name="T"/> which was created.</returns>
119+
public T Create();
120+
121+
/// <summary>
122+
/// Runs some processing when an object was returned to the pool. Can be used to reset the state of an object and indicate if the object should be returned to the pool.
123+
/// </summary>
124+
/// <param name="obj">The object to return to the pool.</param>
125+
/// <returns><see langword="true" /> if the object should be returned to the pool. <see langword="false" /> if it's not possible/desirable for the pool to keep the object.</returns>
126+
public bool Return(T obj);
127+
}

src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,30 @@ namespace SixLabors.Fonts;
2222
/// </content>
2323
internal partial class StreamFontMetrics
2424
{
25-
private TrueTypeInterpreter? interpreter;
25+
// Bounded pool of interpreters shared across threads.
26+
// Size tied to logical CPU count.
27+
private readonly ObjectPool<TrueTypeInterpreter>? interpreterPool;
28+
29+
private TrueTypeInterpreter CreateInterpreter()
30+
{
31+
TrueTypeFontTables tables = this.trueTypeFontTables!;
32+
MaximumProfileTable maxp = tables.Maxp;
33+
34+
TrueTypeInterpreter interpreter = new(
35+
maxp.MaxStackElements,
36+
maxp.MaxStorage,
37+
maxp.MaxFunctionDefs,
38+
maxp.MaxInstructionDefs,
39+
maxp.MaxTwilightPoints);
40+
41+
FpgmTable? fpgm = tables.Fpgm;
42+
if (fpgm is not null)
43+
{
44+
interpreter.InitializeFunctionDefs(fpgm.Instructions);
45+
}
46+
47+
return interpreter;
48+
}
2649

2750
internal void ApplyTrueTypeHinting(HintingMode hintingMode, GlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float pixelSize)
2851
{
@@ -31,37 +54,34 @@ internal void ApplyTrueTypeHinting(HintingMode hintingMode, GlyphMetrics metrics
3154
return;
3255
}
3356

34-
TrueTypeFontTables tables = this.trueTypeFontTables!;
35-
if (this.interpreter == null)
57+
if (this.trueTypeFontTables is null || this.interpreterPool is null)
3658
{
37-
MaximumProfileTable maxp = tables.Maxp;
38-
this.interpreter = new TrueTypeInterpreter(
39-
maxp.MaxStackElements,
40-
maxp.MaxStorage,
41-
maxp.MaxFunctionDefs,
42-
maxp.MaxInstructionDefs,
43-
maxp.MaxTwilightPoints);
44-
45-
FpgmTable? fpgm = tables.Fpgm;
46-
if (fpgm is not null)
47-
{
48-
this.interpreter.InitializeFunctionDefs(fpgm.Instructions);
49-
}
59+
return;
5060
}
5161

52-
CvtTable? cvt = tables.Cvt;
53-
PrepTable? prep = tables.Prep;
54-
float scaleFactor = pixelSize / this.UnitsPerEm;
55-
this.interpreter.SetControlValueTable(cvt?.ControlValues, scaleFactor, pixelSize, prep?.Instructions);
62+
TrueTypeFontTables tables = this.trueTypeFontTables;
63+
TrueTypeInterpreter interpreter = this.interpreterPool.Get();
5664

57-
Bounds bounds = glyphVector.Bounds;
65+
try
66+
{
67+
CvtTable? cvt = tables.Cvt;
68+
PrepTable? prep = tables.Prep;
69+
float hintingScaleFactor = pixelSize / this.UnitsPerEm;
70+
interpreter.SetControlValueTable(cvt?.ControlValues, hintingScaleFactor, pixelSize, prep?.Instructions);
71+
72+
Bounds bounds = glyphVector.Bounds;
5873

59-
Vector2 pp1 = new(MathF.Round(bounds.Min.X - (metrics.LeftSideBearing * scaleXY.X)), 0);
60-
Vector2 pp2 = new(MathF.Round(pp1.X + (metrics.AdvanceWidth * scaleXY.X)), 0);
61-
Vector2 pp3 = new(0, MathF.Round(bounds.Max.Y + (metrics.TopSideBearing * scaleXY.Y)));
62-
Vector2 pp4 = new(0, MathF.Round(pp3.Y - (metrics.AdvanceHeight * scaleXY.Y)));
74+
Vector2 pp1 = new(MathF.Round(bounds.Min.X - (metrics.LeftSideBearing * scaleXY.X)), 0);
75+
Vector2 pp2 = new(MathF.Round(pp1.X + (metrics.AdvanceWidth * scaleXY.X)), 0);
76+
Vector2 pp3 = new(0, MathF.Round(bounds.Max.Y + (metrics.TopSideBearing * scaleXY.Y)));
77+
Vector2 pp4 = new(0, MathF.Round(pp3.Y - (metrics.AdvanceHeight * scaleXY.Y)));
6378

64-
GlyphVector.Hint(hintingMode, ref glyphVector, this.interpreter, pp1, pp2, pp3, pp4);
79+
GlyphVector.Hint(hintingMode, ref glyphVector, interpreter, pp1, pp2, pp3, pp4);
80+
}
81+
finally
82+
{
83+
this.interpreterPool.Return(interpreter);
84+
}
6585
}
6686

6787
private static StreamFontMetrics LoadTrueTypeFont(FontReader reader)
@@ -224,4 +244,19 @@ private GlyphMetrics CreateTrueTypeGlyphMetrics(
224244
textDecorations,
225245
glyphType);
226246
}
247+
248+
private sealed class TrueTypeInterpreterPooledObjectPolicy
249+
: IPooledObjectPolicy<TrueTypeInterpreter>
250+
{
251+
private readonly StreamFontMetrics owner;
252+
253+
public TrueTypeInterpreterPooledObjectPolicy(StreamFontMetrics owner)
254+
=> this.owner = owner;
255+
256+
public TrueTypeInterpreter Create()
257+
=> this.owner.CreateInterpreter();
258+
259+
public bool Return(TrueTypeInterpreter interpreter)
260+
=> true; // Always accept returned instances.
261+
}
227262
}

src/SixLabors.Fonts/StreamFontMetrics.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using SixLabors.Fonts.Tables.General.Kern;
1212
using SixLabors.Fonts.Tables.General.Post;
1313
using SixLabors.Fonts.Tables.TrueType;
14+
using SixLabors.Fonts.Tables.TrueType.Hinting;
1415
using SixLabors.Fonts.Unicode;
1516

1617
namespace SixLabors.Fonts;
@@ -66,6 +67,8 @@ internal StreamFontMetrics(TrueTypeFontTables tables)
6667
(HorizontalMetrics HorizontalMetrics, VerticalMetrics VerticalMetrics) metrics = this.Initialize(tables);
6768
this.horizontalMetrics = metrics.HorizontalMetrics;
6869
this.verticalMetrics = metrics.VerticalMetrics;
70+
71+
this.interpreterPool = new ObjectPool<TrueTypeInterpreter>(new TrueTypeInterpreterPooledObjectPolicy(this));
6972
}
7073

7174
/// <summary>

src/SixLabors.Fonts/Tables/TrueType/Hinting/TrueTypeInterpreter.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public void SetControlValueTable(short[]? cvt, float scale, float ppem, byte[]?
7676
this.controlValueTable = new float[cvt.Length];
7777
}
7878

79+
// TODO: How about SIMD here? Will the JIT vectorize this?
7980
for (int i = 0; i < cvt.Length; i++)
8081
{
8182
this.controlValueTable[i] = cvt[i] * scale;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright (c) Six Labors.
2+
// Licensed under the Six Labors Split License.
3+
4+
namespace SixLabors.Fonts.Tests.Issues;
5+
6+
public class Issues_484
7+
{
8+
[Fact]
9+
public void Test_Issue_484()
10+
=> Parallel.For(0, 10, static _ => Test_Issue_484_Core());
11+
12+
private static void Test_Issue_484_Core()
13+
{
14+
FontCollection fontCollection = new();
15+
string arial = fontCollection.Add(TestFonts.Arial).Name;
16+
17+
FontFamily arialFamily = fontCollection.Get(arial);
18+
Font arialFont = arialFamily.CreateFont(12, FontStyle.Regular);
19+
20+
TextOptions textOptions = new(arialFont)
21+
{
22+
HintingMode = HintingMode.Standard
23+
};
24+
25+
FontRectangle advance = TextMeasurer.MeasureAdvance("Hello, World!", textOptions);
26+
Assert.NotEqual(FontRectangle.Empty, advance);
27+
}
28+
29+
[Fact]
30+
public void Test_Issue_484_B()
31+
{
32+
FontCollection fontCollection = new();
33+
string arial = fontCollection.Add(TestFonts.Arial).Name;
34+
35+
FontFamily arialFamily = fontCollection.Get(arial);
36+
Font arialFont = arialFamily.CreateFont(12, FontStyle.Regular);
37+
38+
Parallel.For(0, 10, _ => Test_Issue_484_Core_B(arialFont));
39+
}
40+
41+
private static void Test_Issue_484_Core_B(Font font)
42+
{
43+
TextOptions textOptions = new(font)
44+
{
45+
HintingMode = HintingMode.Standard
46+
};
47+
48+
FontRectangle advance = TextMeasurer.MeasureAdvance("Hello, World!", textOptions);
49+
Assert.NotEqual(FontRectangle.Empty, advance);
50+
}
51+
}

0 commit comments

Comments
 (0)