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

perf(ext/url): improve URLPattern perf #21488

Merged
merged 2 commits into from Dec 8, 2023

Conversation

lucacasonato
Copy link
Member

This significantly optimizes URLPattern in the case where the same
URL is matched against many patterns (like in a router).

Also minor speedups to other use-cases.


Bench code:

const pattern = new URLPattern({ pathname: "/:bar" });
const pattern2 = new URLPattern({ pathname: "/bar" });

const urls = Array.from({ length: 100 }, () => {
  return `https://example.com/${crypto.randomUUID()}`;
});

const patterns = urls.filter((_, i) => i % Math.ceil(urls.length / 100) === 0)
  .map((url) => new URLPattern({ pathname: new URL(url).pathname }));
console.log(patterns.length);

Deno.bench("one pattern, random url, match", () => {
  const res = pattern.exec(`https://example.com/${crypto.randomUUID()}`);
});

Deno.bench("one pattern, random url, no match", () => {
  const res = pattern2.exec(`https://example.com/${crypto.randomUUID()}`);
});

Deno.bench("one pattern, same url, match", () => {
  const res = pattern.exec("https://example.com/foo");
});

Deno.bench("one pattern, same url, no match", () => {
  const res = pattern2.exec("https://example.com/foo");
});

Deno.bench("many patterns, different url, until match", () => {
  const randomUrlIndex = Math.floor(Math.random() * urls.length);
  for (const pattern of patterns) {
    const res = pattern.exec(urls[randomUrlIndex]);
    if (res !== null) return;
  }
});

v1.38.5
cpu: Apple M1
runtime: deno 1.38.5 (aarch64-apple-darwin)

file:///Users/lucacasonato/projects/github.com/denoland/deno/bench.ts
benchmark                                      time (avg)        iter/s             (min … max)       p75       p99      p995
----------------------------------------------------------------------------------------------- -----------------------------
one pattern, random url, match                  6.59 µs/iter     151,791.1   (6.08 µs … 302.54 µs)   6.42 µs  11.54 µs  14.12 µs
one pattern, random url, no match                5.7 µs/iter     175,587.0     (5.59 µs … 5.87 µs)   5.73 µs   5.87 µs   5.87 µs
one pattern, same url, match                    4.29 µs/iter     233,029.7     (4.24 µs … 4.47 µs)    4.3 µs   4.47 µs   4.47 µs
one pattern, same url, no match                 3.71 µs/iter     269,867.4     (3.64 µs … 3.94 µs)    3.7 µs   3.94 µs   3.94 µs
many patterns, different url, until match     248.63 µs/iter       4,022.1   (5.42 µs … 710.71 µs)  370.5 µs 540.71 µs 595.17 µs
main
cpu: Apple M1
runtime: deno 1.38.5 (aarch64-apple-darwin)

file:///Users/lucacasonato/projects/github.com/denoland/deno/bench.ts
benchmark                                      time (avg)        iter/s             (min … max)       p75       p99      p995
----------------------------------------------------------------------------------------------- -----------------------------
one pattern, random url, match                  6.56 µs/iter     152,369.3     (5.62 µs … 2.22 ms)   6.21 µs  10.88 µs     15 µs
one pattern, random url, no match               5.65 µs/iter     176,876.6     (5.44 µs … 5.98 µs)   5.73 µs   5.98 µs   5.98 µs
one pattern, same url, match                    1.24 µs/iter     808,545.2     (1.22 µs … 1.33 µs)   1.24 µs   1.33 µs   1.33 µs
one pattern, same url, no match               554.29 ns/iter   1,804,102.6 (540.52 ns … 577.54 ns) 558.08 ns 573.21 ns 577.54 ns
many patterns, different url, until match      32.51 µs/iter      30,759.8      (1 µs … 415.96 µs)  47.62 µs  71.71 µs  79.29 µs

This significantly optimizes URLPattern in the case where the same
URL is matched against many patterns (like in a router).

Also minor speedups to other use-cases.
Copy link
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for looking into this.

if (this.#lastUsedKey === key) return this.#lastUsedValue;
const value = this.#map.get(key);
if (value !== undefined) {
if (MathRandom() < 0.1) {
Copy link
Member

Choose a reason for hiding this comment

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

Ideally should be actually do a least recently used strategy but fine for now. Maybe rename class to RandomLruCache

Copy link
Member Author

Choose a reason for hiding this comment

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

It does do least recently used - maps are insertion ordered. See the code below this line (the re-insertion is to put the used item back to the "hot" side of the LRU), we use keys().next().value to get item at the "cold" end of the LRU.

I am intentionally using a random chance to determine whether to refresh / insert as a very cheap pseudo "young space" where if a item is not used at least 10 times on average (but not 10 times in a row), it doesn't graduate into the more permanent LRU storage. This is done because inserting and refreshing LRU items is actually comparatively very expensive (about 50% of the operation it is caching).

This is a "pseudo young space" because it is random based, and thus sometimes misses the mark. In the benchmark configurations I have run that were written based on usage experience from real websites using Fresh (like dotcom), where there are some very hot routes, and a long tail of cold routes, I think this will do well.

Copy link
Member Author

Choose a reason for hiding this comment

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

But yes, I can rename to SampledLruCache

Copy link
Member

Choose a reason for hiding this comment

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

Sounds good, thanks for explaning

@lucacasonato lucacasonato merged commit e15c735 into denoland:main Dec 8, 2023
14 checks passed
@lucacasonato lucacasonato deleted the urlpattern_fix branch December 8, 2023 11:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants