Measuring text with `TextMeasurer.Measure` is slow
Prerequisites
- [x] I have written a descriptive issue title
- [x] I have verified that I am running the latest version of Fonts
- [x] I have verified if the problem exist in both
DEBUGandRELEASEmode - [x] I have searched open and closed issues to ensure it has not already been reported
Description
I tried to use SixLabors.Fonts to compute widths of many different texts to produce a nice looking Excel file with Simplexcel. It turned out to be an unusable solution because it was too slow. I tracked the slowness down to the SixLabors.Fonts.TextMeasurer.Measure function.
I ran some benchmarks, comparing SixLabors.Fonts to SkiaSharp and here are the results.
BenchmarkDotNet=v0.13.1, OS=macOS Catalina 10.15.7 (19H1615) [Darwin 19.6.0]
Intel Core i9-9980HK CPU 2.40GHz, 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.300
[Host] : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
MediumRun : .NET 6.0.5 (6.0.522.21309), X64 RyuJIT
Job=MediumRun IterationCount=15 LaunchCount=2
WarmupCount=10
| Method | Text | Mean | Error | StdDev |
|---|---|---|---|---|
| SixLaborsFonts | Hello world | 31,270.4 ns | 621.99 ns | 871.94 ns |
| SkiaSharp | Hello world | 539.0 ns | 8.13 ns | 11.92 ns |
| SixLaborsFonts | The q(...)y dog [43] | 110,846.6 ns | 4,511.26 ns | 6,752.24 ns |
| SkiaSharp | The q(...)y dog [43] | 1,421.4 ns | 38.72 ns | 55.54 ns |
| SixLaborsFonts | a | 7,025.6 ns | 349.17 ns | 511.81 ns |
| SkiaSharp | a | 353.3 ns | 11.29 ns | 16.90 ns |
I eventually used SkiaSharp instead of SixLabors.Fonts and did not investigate further why measuring text is slow.
The benchmark below could help as a starting point, should someone tackle this issue (myself included if I ever find the time).
Steps to Reproduce
Run the following project with dotnet run -c Release
MeasureTextBenchmarks.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.1" />
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
<PackageReference Include="SkiaSharp.NativeAssets.Linux" Version="2.88.0" />
</ItemGroup>
</Project>
Program.cs
using System;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using SixLabors.Fonts;
using SkiaSharp;
BenchmarkRunner.Run<MeasureTextBenchmark>();
[MediumRunJob]
public class MeasureTextBenchmark : IDisposable
{
private readonly TextOptions _textOptions;
private readonly SKTypeface _arialTypeface;
private readonly SKFont _font;
private readonly SKPaint _paint;
public MeasureTextBenchmark()
{
const string fontFamilyName = "Arial";
const int fontSize = 16;
_textOptions = new TextOptions(SystemFonts.Get(fontFamilyName).CreateFont(fontSize, FontStyle.Regular));
_arialTypeface = SKTypeface.FromFamilyName(fontFamilyName, SKFontStyle.Normal);
_font = new SKFont(_arialTypeface, fontSize);
_paint = new SKPaint(_font);
}
public void Dispose()
{
_arialTypeface.Dispose();
_font.Dispose();
_paint.Dispose();
}
[Params("a", "Hello world", "The quick brown fox jumps over the lazy dog")]
public string Text { get; set; } = "";
[Benchmark]
public void SixLaborsFonts() => TextMeasurer.Measure(Text, _textOptions);
[Benchmark]
public void SkiaSharp() => _paint.MeasureText(Text);
}
System Configuration
- Fonts version: 1.0.0-beta17
- Other Six Labors packages and versions: none
- Environment (Operating system, version and so on): macOS 10.15.7
- .NET Framework version: .NET 6.0.5
- Additional information: N/A
SkiaSharp doesn’t have a proper text layout engine though does it?
https://github.com/mono/SkiaSharp/issues/692#issuecomment-443295319
We do layout and shaping as part of the measuring process so I don’t think this is a fair comparison.
SkiaSharp doesn’t have a proper text layout engine though does it?
This can be proven using the following string.
"\u1112\u1172\u1100\u1161\u0020\u1100\u1161\u002D\u002D\u0020\u0028\u110B\u1169\u002D\u002D\u0029"
This is a decomposed Hangul script that requires composing in order to measure the text properly.
The correct output should be. (Note this is actually a paste of the decomposed format, GitHub automatically composes it.)
휴가 가-- (오--)
SkiaSharp doesn't do any shaping and simply renders the raw Unicode codepoints.

Whereas we do the correct thing.

MeasureText in SkiaSharp seems to be a very naïve implementation and as such really cannot be trusted for anything other than the most simple cases. There doesn't appear to be any bidi, shaping, or proper linebreak support when measuring and rendering text.
That doesn't mean we can't speed things up. We've done a lot of work though while building the library though to keep performance in mind so I'm not sure how much low hanging fruit there is.
We might be able to create a closer comparison using example code from here.
https://social.msdn.microsoft.com/Forums/en-US/ddb1c505-e7a2-473e-a768-a3be568fc632/text-measuring-when-dealing-with-rtl-harfbuzz?forum=xamarinlibraries
I did some profiling and added your benchmark to the solution in another branch.
After removing the bounds checks from the Unicode trie lookups I'm mostly left with the lazy loading of the Unicode data. There's no way I can think of to speed that up as I'm depending heavily on runtime components there.