markdown-it / markdown-it/linkify-it
`test()` with `fuzzyLink: true` got ~80x slower on plain text in 6.0.0
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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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