markdown-it / markdown-it/linkify-it

`test()` with `fuzzyLink: true` got ~80x slower on plain text in 6.0.0

Open
#126 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
739
Forks
72
PR merge metrics
No merged PRs in 30d

Description

We recently updated markdown-it to 15 in Discourse and with it linkify-it to 6. Since fuzzy links are off by default now, we turned them back on to keep the old behavior. After that, parsing our posts became noticeably slower (about a third more CPU time over 1.5 million posts). I profiled it and most of the extra time was in linkify-it, so I tried to reproduce it without markdown-it.

This is test() on a few paragraphs of plain text without any link in it. Node 22.23.1, Linux, i9-13900H, median of 5 runs:

linkify-it 5.0.2  fuzzyLink: false   1350 bytes      1.1 µs
linkify-it 5.0.2  fuzzyLink: false   5400 bytes      4.2 µs
linkify-it 5.0.2  fuzzyLink: false  10800 bytes      8.2 µs
linkify-it 5.0.2  fuzzyLink: true    1350 bytes      1.8 µs
linkify-it 5.0.2  fuzzyLink: true    5400 bytes      7.0 µs
linkify-it 5.0.2  fuzzyLink: true   10800 bytes     14.0 µs
linkify-it 6.1.0  fuzzyLink: false   1350 bytes      1.1 µs
linkify-it 6.1.0  fuzzyLink: false   5400 bytes      4.3 µs
linkify-it 6.1.0  fuzzyLink: false  10800 bytes      8.7 µs
linkify-it 6.1.0  fuzzyLink: true    1350 bytes    141.4 µs
linkify-it 6.1.0  fuzzyLink: true    5400 bytes    564.9 µs
linkify-it 6.1.0  fuzzyLink: true   10800 bytes   1127.1 µs

Without fuzzy links both versions are the same. With fuzzy links, 6.1.0 is around 80 times slower than 5.0.2, and it grows with the length of the text (about 100 ns per byte). It makes no difference if the text has punctuation or not.

I think I found the reason. In 5.x, test() first did a cheap search for something that looks like host.tld and only ran the big fuzzy regex when that found something:

if (text.search(this.re.host_fuzzy_test) >= 0) {
  if (text.match(this.__opts__.fuzzyIP ? this.re.link_fuzzy : this.re.link_no_ip_fuzzy) !== null) {

In 6.x that check is gone and the fuzzy regex always runs over the whole text:

if (this.__opts__.fuzzyLink && this.__schemas__["http:"]) {
  re = this.re.get_fuzzy_link_search();
  re.lastIndex = 0;
  if (re.exec(text) !== null) return true;
}

Timing the parts separately on the 5400 byte text:

5.0.2 host_fuzzy_test search                        2.8 µs
5.0.2 link_no_ip_fuzzy match                       41.9 µs
6.1.0 fuzzy_link_search exec                      557.3 µs
6.1.0 test()                                      567.3 µs
6.1.0 test() with the old pre-check in front        2.6 µs

So the regex in 6.x is also slower on its own, but the pre-check is what kept it away from normal text before.

It's easy to not notice this, because with the default options test() never gets to that code. But markdown-it 15 tells people to set fuzzyLink: true if they want the old behavior, and it also calls test() instead of pretest() for every text token now. So everybody who does that pays this on every paragraph.

The benchmark script is attached (linkify-fuzzy-test-bench.js). It needs both versions installed side by side:

npm i li5@npm:linkify-it@5.0.2 li6@npm:linkify-it@6.1.0
node linkify-fuzzy-test-bench.js li5
node linkify-fuzzy-test-bench.js li6

I haven't tried to fix it properly. Putting a search for the host/TLD part in front of the fuzzy regex, like 5.x did, brings the numbers back to where they were in my test. Happy to send a PR if you want to go that way.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with linkify-it's test() fuzzyLink path and compare it with the 5.x host_fuzzy_test pre-check described in the issue. Run linkify-fuzzy-test-bench.js with the li5 and li6 installs, then verify that plain-text fuzzy-link tests avoid the expensive regex while preserving detection for fuzzy links.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
performance
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
68/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.