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

Speed up font-width computation in most cases #1390

Merged
merged 21 commits into from
Dec 27, 2017
Merged

Speed up font-width computation in most cases #1390

merged 21 commits into from
Dec 27, 2017

Conversation

paulmelnikow
Copy link
Member

@paulmelnikow paulmelnikow commented Dec 24, 2017

Ref: #1379

This takes a naive approach to font-width computation, the most compute-intensive part of rendering badges.

  1. Add the widths of the individual characters.
    • These widths are measured on startup using PDFKit.
  2. For each character pair, add a kerning adjustment
    • The difference between the width of each character pair, and the sum of the characters' separate widths.
    • These are computed for each character pair on startup using PDFKit.
  3. For a string with characters outside the printable ASCII character set, fall back to PDFKit.

This branch averaged 0.049 ms in makeBadge, compared to 0.182 ms on master, a speedup of 73%. That was on a test of 10,000 consecutive requests (using the method in #1379).

The speedup applies to badges containing exclusively printable ASCII characters. It wouldn't be as dramatic on non-ASCII text. Though, we could add some frequently used non-ASCII characters to the cached set.

@paulmelnikow paulmelnikow added core Server, BaseService, GitHub auth, Shared helpers performance-improvement Related to performance or throughput of the badge servers labels Dec 24, 2017
@shields-ci
Copy link

shields-ci commented Dec 24, 2017

Warnings
⚠️

This PR modified the server but none of the service tests. That's okay so long as it's refactoring existing code.

Messages
📖

✨ Thanks for your contribution to Shields, @paulmelnikow!

Generated by 🚫 dangerJS

@paulmelnikow
Copy link
Member Author

I can replicate this failure locally. It must have crept in as I was cleaning up the code.

I think the problem is due to global state and test ordering. They used to pass locally regardless of whether I chose Verdana or DejaVu Sans. Now they pass locally only with Verdana.

Copy link
Member

@espadrine espadrine left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot! This is solid work!

I can confirm that I replicate your findings related to the benchmark with a modification of this patch (going from 0.197ms to 0.048ms).

module.exports = {
PDFKitTextMeasurer,
QuickTextMeasurer,
// measure: defaultMeasurer.measure.bind(defaultMeasurer),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked having the measure function as the default export.

Also, I see no benefit to using promises for server initialization procedures.
Could we simply have initialization be synchronous?

(As a result, the distinction between new TextMeasurer() and TextMeasurer.create() is not that useful.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I liked having the measure function as the default export.

Will try to restore that. Was in the middle of cleaning up a gnarly module state bug, though if the initialization can all be done synchronously that will help a lot. Building the measurer is a little slow, so it would probably be good to avoid doing it when it's not necessary. Will think about a way to do that.

Could we simply have initialization be synchronous?

Totally! I completely missed that. loadFont was using a callback before and I robotically ported it to promises, not realizing all the work was synchronous. That's great. It'll simplify this a lot.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up removing this, because the quick measurer takes a while to generate, and it was slowing down the CLI and the CLI tests. Plus it makes running exclusive tests longer, as the cache needs to be built even if it's not used by the tests being run.

@@ -152,6 +152,5 @@ function makeBadge (data) {
}

module.exports = makeBadge;
module.exports.loadFont = measureTextWidth.loadFont;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was that umused?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, I think so. It might have been part of the gh-badges library usage. If it is I'll make sure the docs are updated as part of #1388.

}
}

loadFont(path.join(__dirname, '..', 'Verdana.ttf'), function (err) {
if (err && process.env.FALLBACK_FONT_PATH) {
loadFont(process.env.FALLBACK_FONT_PATH);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you plan on keeping supporting FALLBACK_FONT_PATH and using FONT_PATH?

If I am reading the patch correctly, you currently no longer use any of them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea, sorry, that bit was WIP. I'll get it fixed up.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

loadFont(process.env.FALLBACK_FONT_PATH);
class QuickTextMeasurer {
constructor(baseMeasurer) {
Object.assign(this, { baseMeasurer });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we benefit from the generality this is meant to create?
Shouldn't we directly use PDFKitTextMeasurer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Injecting it makes testing a little easier, because I can place the spy on the measurer instance. Caching code is notoriously difficult to test so I feel it's important to test that the cache object doesn't call through to the base object when it's not being used. I'll take a look though; maybe that is easily changed now. When this is all synchronous it'll be way simpler.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working just fine by mocking the method on the prototype instead!

@paulmelnikow paulmelnikow changed the title Speed up font-width computation in most cases [WIP] Speed up font-width computation in most cases Dec 25, 2017
@paulmelnikow
Copy link
Member Author

Thanks for the review! Will pick this up today and respond to your comments.

@paulmelnikow paulmelnikow changed the title [WIP] Speed up font-width computation in most cases Speed up font-width computation in most cases Dec 26, 2017
@paulmelnikow
Copy link
Member Author

I re-ran the benchmark on the last commit and the 73% is holding up. I got 0.041 vs 0.144 in master.

@paulmelnikow paulmelnikow merged commit cc9a6db into badges:master Dec 27, 2017
@paulmelnikow paulmelnikow deleted the font-width-perf branch December 27, 2017 04:57
paulmelnikow added a commit that referenced this pull request Nov 15, 2018
This simplifies and further optimizes text-width computation by computing the entire width table in advance, and serializing it in the style of QuickTextMeasurer (#1390). This entirely removes the need for PDFKit at runtime. This has the advantage of fixing #1305 – more generally: producing the same result everywhere – without having to deploy a copy of Verdana.

The lifting is delegated to these three libraries, which are housed in a monorepo: https://github.com/metabolize/anafanafo

I'd be happy to move it into the badges org if folks want to collaborate on maintaining them.

QuickTextMeasurer took kerning pairs into account, whereas this implementation does not. I was thinking kerning would be a necessary refinement, though this seems to work well enough.

I dropped in a binary-search package to traverse the data structure, in part to conserve space. This causes a moderate performance regression, though there is ample room for improving on that: #2311 (comment)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
core Server, BaseService, GitHub auth, Shared helpers performance-improvement Related to performance or throughput of the badge servers
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants