Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Measuring text with TextMeasurer.Measure is slow #268

Open
4 tasks done
0xced opened this issue May 28, 2022 · 4 comments
Open
4 tasks done

Measuring text with TextMeasurer.Measure is slow #268

0xced opened this issue May 28, 2022 · 4 comments

Comments

@0xced
Copy link
Contributor

0xced commented May 28, 2022

Prerequisites

  • I have written a descriptive issue title
  • I have verified that I am running the latest version of Fonts
  • I have verified if the problem exist in both DEBUG and RELEASE mode
  • 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
@JimBobSquarePants
Copy link
Member

SkiaSharp doesn’t have a proper text layout engine though does it?

mono/SkiaSharp#692 (comment)

We do layout and shaping as part of the measuring process so I don’t think this is a fair comparison.

@JimBobSquarePants
Copy link
Member

JimBobSquarePants commented Jun 14, 2022

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.

hangul

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.

@JimBobSquarePants
Copy link
Member

@JimBobSquarePants
Copy link
Member

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.

image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants