Repository: denoland/deno
Pull request: perf(ext/web): optimize URLPattern ops to reduce serde overhead and GC pressure

## Summary

- **`op_urlpattern_process_match_input`**: Replace `#[serde] Option<(MatchInput, Inputs)>` return with `#[string] Option<String>` + `#[buffer] &mut [u32]`. The 8 URL component values are concatenated into a single string with offsets written to a shared `Uint32Array(9)` buffer. The `Inputs` return (which just echoed back the caller's arguments) is eliminated entirely and reconstructed on the JS side.

- **`op_urlpattern_parse`**: Return a lean struct that excludes the unused `matcher` field (containing `prefix`, `suffix`, and `InnerMatcher` sub-objects) from each `UrlPatternComponent`.

- **JS side**: Components stored as indexed array instead of keyed object. Match values accessed by index. Inner loop variable shadowing fixed.

## Benchmark

| Benchmark | Baseline | Patched | Speedup |
|---|---|---|---|
| **test()** | | | |
| test(string) cached | 3.96M ops/s | 5.20M ops/s | **+31%** |
| test(string) rotating 1k | 1.90M ops/s | 2.65M ops/s | **+39%** |
| test(string) no match | 5.40M ops/s | 6.59M ops/s | **+22%** |
| test(string, baseURL) | 676K ops/s | 942K ops/s | **+39%** |
| test(init) | 257K ops/s | 342K ops/s | **+33%** |
| test(string) complex | 2.22M ops/s | 3.18M ops/s | **+43%** |
| **exec()** | | | |
| exec(string) cached | 1.22M ops/s | 1.96M ops/s | **+60%** |
| exec(string) rotating 1k | 933K ops/s | 1.47M ops/s | **+58%** |
| exec(string) no match | 2.84M ops/s | 4.92M ops/s | **+73%** |
| exec(string, baseURL) | 481K ops/s | 713K ops/s | **+48%** |
| exec(init) | 198K ops/s | 289K ops/s | **+46%** |
| exec(string) complex | 1.03M ops/s | 1.46M ops/s | **+42%** |
| **Property getters** | | | |
| read all 8 properties | 7.81M ops/s | 11.63M ops/s | **+49%** |

## Test plan

- [x] `./x test-unit urlpattern` passes
- [x] WPT urlpattern suite: 347 passed, 0 failed, 7 expected failures (unchanged)
- [x] Manual sanity test of `test()`, `exec()`, property getters, string/init/baseURL inputs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/web/01_urlpattern.js
patch: @@ -22,9 +22,11 @@ const {
   RegExpPrototypeTest,
   SafeMap,
   SafeRegExp,
+  StringPrototypeSlice,
   Symbol,
   SymbolFor,
   TypeError,
+  Uint32Array,
 } = primordials;
 
 import * as webidl from "ext:deno_webidl/00_webidl.js";
@@ -138,10 +140,54 @@ class SampledLRUCache {
 
 const matchInputCache = new SampledLRUCache(4096);
 
+/**
+ * Shared buffer for receiving URL component offsets from the
+ * op_urlpattern_process_match_input op.
+ *
+ * Layout: buf[0..8] = cumulative start offsets, buf[8] = total length.
+ * Component i's value = concat.slice(buf[i], buf[i+1]).
+ */
+const matchBuf = new Uint32Array(9);
+
+/**
+ * Calls the op and extracts the 8 component values from the concatenated
+ * string + offset buffer. Returns a flat 8-element string array, or null.
+ * @param {string | object} input
+ * @param {string | undefined} baseURL
+ * @returns {string[] | null}
+ */
+function processMatchInput(input, baseURL) {
+  const concat = op_urlpattern_process_match_input(
+    input,
+    baseURL ?? null,
+    matchBuf,
+  );
+  if (concat === null) return null;
+  return [
+    StringPrototypeSlice(concat, matchBuf[0], matchBuf[1]),
+    StringPrototypeSlice(concat, matchBuf[1], matchBuf[2]),
+    StringPrototypeSlice(concat, matchBuf[2], matchBuf[3]),
+    StringPrototypeSlice(concat, matchBuf[3], matchBuf[4]),
+    StringPrototypeSlice(concat, matchBuf[4], matchBuf[5]),
+    StringPrototypeSlice(concat, matchBuf[5], matchBuf[6]),
+    StringPrototypeSlice(concat, matchBuf[6], matchBuf[7]),
+    StringPrototypeSlice(concat, matchBuf[7], matchBuf[8]),
+  ];
+}
+
+/**
+ * Cache-compatible factory: calls processMatchInput with no baseURL.
+ * @param {string | object} input
+ * @returns {string[] | null}
+ */
+function processMatchInputCached(input) {
+  return processMatchInput(input, undefined);
+}
+
 const _hasRegExpGroups = Symbol("[[hasRegExpGroups]]");
 
 class URLPattern {
-  /** @type {Components} */
+  /** @type {Component[]} */
   [_components];
   [_hasRegExpGroups];
 
@@ -188,61 +234,71 @@ class URLPattern {
       );
     }
 
-    const components = op_urlpattern_parse(input, baseURL, options);
-    this[_hasRegExpGroups] = components.hasRegexpGroups;
-
-    for (let i = 0; i < COMPONENTS_KEYS.length; ++i) {
+    const parsed = op_urlpattern_parse(input, baseURL, options);
+    this[_hasRegExpGroups] = parsed.hasRegexpGroups;
+
+    const flags = options.ignoreCase ? "ui" : "u";
+    const components = [
+      undefined,
+      undefined,
+      undefined,
+      undefined,
+      undefined,
+      undefined,
+      undefined,
+      undefined,
+    ];
+    for (let i = 0; i < 8; ++i) {
       const key = COMPONENTS_KEYS[i];
+      const c = parsed[key];
       try {
-        components[key].regexp = new SafeRegExp(
-          components[key].regexpString,
-          options.ignoreCase ? "ui" : "u",
-        );
+        c.regexp = new SafeRegExp(c.regexpString, flags);
       } catch (e) {
         throw new TypeError(`${prefix}: ${key} is invalid; ${e.message}`);
       }
+      components[i] = c;
     }
     this[_components] = components;
   }
 
   get protocol() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].protocol.patternString;
+    return this[_components][0].patternString;
   }
 
   get username() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].username.patternString;
+    return this[_components][1].patternString;
   }
 
   get password() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].password.patternString;
+    return this[_components][2].patternString;
   }
 
   get hostname() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].hostname.patternString;
+    return this[_components][3].patternString;
   }
 
   get port() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].port.patternString;
+    return this[_components][4].patternString;
   }
 
   get pathname() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].pathname.patternString;
+    return this[_components][5].patternString;
   }
 
   get search() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].search.patternString;
+    return this[_components][6].patternString;
   }
 
   get hash() {
     webidl.assertBranded(this, URLPatternPrototype);
-    return this[_components].hash.patternString;
+    return this[_components][7].patternString;
   }
 
   get hasRegExpGroups() {
@@ -264,27 +320,22 @@ class URLPattern {
       baseURL = webidl.converters.USVString(baseURL, prefix, "Argument 2");
     }
 
-    const res = baseURL === undefined
-      ? matchInputCache.getOrInsert(
-        input,
-        op_urlpattern_process_match_input,
-      )
-      : op_urlpattern_process_match_input(input, baseURL);
-    if (res === null) return false;
+    const values = baseURL === undefined
+      ? matchInputCache.getOrInsert(input, processMatchInputCached)
+      : processMatchInput(input, baseURL);
+    if (values === null) return false;
 
-    const values = res[0];
-
-    for (let i = 0; i < COMPONENTS_KEYS.length; ++i) {
-      const key = COMPONENTS_KEYS[i];
-      const component = this[_components][key];
+    const components = this[_components];
+    for (let i = 0; i < 8; ++i) {
+      const component = components[i];
       switch (component.regexpString) {
         case "^$":
-          if (values[key] !== "") return false;
+          if (values[i] !== "") return false;
           break;
         case "^(.*)$":
           break;
         default: {
-          if (!RegExpPrototypeTest(component.regexp, values[key])) return false;
+          if (!RegExpPrototypeTest(component.regexp, values[i])) return false;
         }
       }
     }
@@ -306,18 +357,13 @@ class URLPattern {
       baseURL = webidl.converters.USVString(baseURL, prefix, "Argument 2");
     }
 
-    const res = baseURL === undefined
-      ? matchInputCache.getOrInsert(
-        input,
-        op_urlpattern_process_match_input,
-      )
-      : op_urlpattern_process_match_input(input, baseURL);
-    if (res === null) {
+    const values = baseURL === undefined
+      ? matchInputCache.getOrInsert(input, processMatchInputCached)
+      : processMatchInput(input, baseURL);
+    if (values === null) {
       return null;
     }
 
-    const { 0: values, 1: inputs } = res; /** @type {URLPatternResult} */
-
     // globalThis.allocAttempt++;
     this.#reusedResult ??= { inputs: [undefined] };
     const result = this.#reusedResult;
@@ -326,47 +372,48 @@ class URLPattern {
 
     const components = this[_components];
 
-    for (let i = 0; i < COMPONENTS_KEYS.length; ++i) {
+    for (let i = 0; i < 8; ++i) {
       const key = COMPONENTS_KEYS[i];
       /** @type {Component} */
-      const component = components[key];
+      const component = components[i];
 
       const res = result[key] ??= {
-        input: values[key],
-        groups: component.regexpString === "^(.*)$" ? { "0": values[key] } : {},
+        input: values[i],
+        groups: component.regexpString === "^(.*)$" ? { "0": values[i] } : {},
       };
 
       switch (component.regexpString) {
         case "^$":
-          if (values[key] !== "") return null;
+          if (values[i] !== "") return null;
           break;
         case "^(.*)$":
-          res.groups["0"] = values[key];
+          res.groups["0"] = values[i];
           break;
         default: {
-          const match = RegExpPrototypeExec(component.regexp, values[key]);
+          const match = RegExpPrototypeExec(component.regexp, values[i]);
           if (match === null) return null;
           const groupList = component.groupNameList;
           const groups = res.groups;
-          for (let i = 0; i < groupList.length; ++i) {
+          for (let j = 0; j < groupList.length; ++j) {
             // TODO(lucacasonato): this is vulnerable to override mistake
             if (urlPatternSettings.groupStringFallback) {
-              groups[groupList[i]] = match[i + 1] ?? "";
+              groups[groupList[j]] = match[j + 1] ?? "";
             } else {
-              groups[groupList[i]] = match[i + 1];
+              groups[groupList[j]] = match[j + 1];
             }
           }
           break;
         }
       }
-      res.input = values[key];
+      res.input = values[i];
     }
 
-    // Now populate result.inputs
-    result.inputs[0] = typeof inputs[0] === "string"
-      ? inputs[0]
-      : ObjectAssign(ObjectCreate(null), inputs[0]);
-    if (inputs[1] !== null) ArrayPrototypePush(result.inputs, inputs[1]);
+    // Reconstruct inputs from the original arguments (the op no longer
+    // returns them -- they were a pass-through of the caller's arguments).
+    result.inputs[0] = typeof input === "string"
+      ? input
+      : ObjectAssign(ObjectCreate(null), input);
+    if (baseURL !== undefined) ArrayPrototypePush(result.inputs, baseURL);
 
     this.#reusedResult = undefined;
     return result;

filename: ext/web/urlpattern.rs
patch: @@ -1,40 +1,124 @@
 // Copyright 2018-2026 the Deno authors. MIT license.
 
 use deno_core::op2;
+use serde::Serialize;
 use urlpattern::quirks;
-use urlpattern::quirks::MatchInput;
 use urlpattern::quirks::StringOrInit;
-use urlpattern::quirks::UrlPattern;
 
 deno_error::js_error_wrapper!(urlpattern::Error, UrlPatternError, "TypeError");
 
+/// Lean version of UrlPatternComponent that excludes the unused `matcher` field.
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct UrlPatternComponent {
+  pattern_string: String,
+  regexp_string: String,
+  group_name_list: Vec<String>,
+}
+
+impl From<urlpattern::quirks::UrlPatternComponent> for UrlPatternComponent {
+  fn from(c: urlpattern::quirks::UrlPatternComponent) -> Self {
+    Self {
+      pattern_string: c.pattern_string,
+      regexp_string: c.regexp_string,
+      group_name_list: c.group_name_list,
+    }
+  }
+}
+
+/// Lean version of UrlPattern that uses our trimmed UrlPatternComponent.
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+struct UrlPatternResult {
+  protocol: UrlPatternComponent,
+  username: UrlPatternComponent,
+  password: UrlPatternComponent,
+  hostname: UrlPatternComponent,
+  port: UrlPatternComponent,
+  pathname: UrlPatternComponent,
+  search: UrlPatternComponent,
+  hash: UrlPatternComponent,
+  has_regexp_groups: bool,
+}
+
 #[op2]
 #[serde]
 pub fn op_urlpattern_parse(
   #[serde] input: StringOrInit,
   #[string] base_url: Option<String>,
   #[serde] options: urlpattern::UrlPatternOptions,
-) -> Result<UrlPattern, UrlPatternError> {
+) -> Result<UrlPatternResult, UrlPatternError> {
   let init =
     quirks::process_construct_pattern_input(input, base_url.as_deref())?;
 
   let pattern = quirks::parse_pattern(init, options)?;
 
-  Ok(pattern)
+  Ok(UrlPatternResult {
+    protocol: pattern.protocol.into(),
+    username: pattern.username.into(),
+    password: pattern.password.into(),
+    hostname: pattern.hostname.into(),
+    port: pattern.port.into(),
+    pathname: pattern.pathname.into(),
+    search: pattern.search.into(),
+    hash: pattern.hash.into(),
+    has_regexp_groups: pattern.has_regexp_groups,
+  })
 }
 
+/// Processes match input and returns a concatenated string of all 8 URL
+/// component values, writing their offsets into `buf`.
+///
+/// Returns `None` if the input doesn't parse as a valid URL.
+///
+/// Buffer layout (9 u32 values):
+///   buf[0..8] = cumulative start offset for each component
+///   buf[8]    = total byte length (end offset of last component)
+///
+/// The returned string is the concatenation of:
+///   protocol + username + password + hostname + port + pathname + search + hash
+///
+/// To extract component `i`: `str.slice(buf[i], buf[i+1])`
 #[op2]
-#[serde]
+#[string]
 pub fn op_urlpattern_process_match_input(
   #[serde] input: StringOrInit,
   #[string] base_url: Option<String>,
-) -> Result<Option<(MatchInput, quirks::Inputs)>, UrlPatternError> {
+  #[buffer] buf: &mut [u32],
+) -> Result<Option<String>, UrlPatternError> {
   let res = quirks::process_match_input(input, base_url.as_deref())?;
 
-  let (input, inputs) = match res {
+  let (input, _inputs) = match res {
     Some((input, inputs)) => (input, inputs),
     None => return Ok(None),
   };
 
-  Ok(quirks::parse_match_input(input).map(|input| (input, inputs)))
+  let match_input = match quirks::parse_match_input(input) {
+    Some(mi) => mi,
+    None => return Ok(None),
+  };
+
+  let fields = [
+    &match_input.protocol,
+    &match_input.username,
+    &match_input.password,
+    &match_input.hostname,
+    &match_input.port,
+    &match_input.pathname,
+    &match_input.search,
+    &match_input.hash,
+  ];
+
+  let total_len: usize = fields.iter().map(|f| f.len()).sum();
+  let mut concat = String::with_capacity(total_len);
+  let mut offset = 0u32;
+
+  for (i, field) in fields.iter().enumerate() {
+    buf[i] = offset;
+    offset += field.len() as u32;
+    concat.push_str(field);
+  }
+  buf[8] = offset;
+
+  Ok(Some(concat))
 }