Skip to content

⚡️ Faster fc.webPath/fc.webUrl on generate#7048

Merged
dubzzz merged 2 commits into
mainfrom
claude/perf-webPath-fuse-segments
Jun 5, 2026
Merged

⚡️ Faster fc.webPath/fc.webUrl on generate#7048
dubzzz merged 2 commits into
mainfrom
claude/perf-webPath-fuse-segments

Conversation

@dubzzz

@dubzzz dubzzz commented Jun 4, 2026

Copy link
Copy Markdown
Owner

Description

AI-agent disclosure: this PR was authored by an automated agent (Claude Code) and has not been line-by-line reviewed by a human before submission.

This is a performance-only change to the path-assembly mapper shared by fc.webPath and fc.webUrl. Generated values are unchanged — only the speed of generate improves. No public API change, no behavior change for end users.

Measured improvement (interleaved separate-process A/B, median of medians):

  • webPath(): ~−10% ns/op (≈1376 → ≈1236 ns/op) — the direct beneficiary, and webPath is itself public API.
  • webUrl(): ~−4% ns/op (≈9515 → ≈9123 ns/op) — a smaller relative gain because most of webUrl's cost is in authority/domain generation; the path-assembly saving is real but a smaller share of the whole.

Why

segmentsToPathMapper assembled the path as:

return safeJoin(safeMap(segments, (v) => `/${v}`), '');

That allocates an intermediate array of /segment strings and invokes a per-element arrow closure for every segment, then joins. Fusing it into a single accumulating loop removes both the intermediate array and the map callback:

let path = '';
for (let index = 0; index !== segments.length; ++index) {
  path += '/' + segments[index];
}
return path;

Scope, correctness and impact

  • Impact: patch. A changeset (fast-check: patch, ⚡️ Faster fc.webPath/fc.webUrl on generate) is included.
  • Single concern: one file (_internals/mappers/SegmentsToPath.ts) plus the changeset. Only the forward mapper changed; the unmapper is untouched.
  • Blast radius: segmentsToPathMapper is used solely by buildUriPathArbitrarywebPathwebUrl. It does not touch domain/authority/email code paths.
  • Tests: webUrl, webPath, PartsToUrl, UriPath/SegmentsToPath, and emailAddress specs all pass (25 tests). It is a pure string-assembly refactor producing byte-identical output, so no new test is needed.

Checklist

Don't delete this checklist and make sure you do the following before opening the PR

  • I have a full understanding of every line in this PR — whether the code was hand-written, AI-generated, copied from external sources or produced by any other tool
  • I flagged the impact of my change (minor / patch / major) either by running pnpm run bump or by following the instructions from the changeset bot
  • I kept this PR focused on a single concern and did not bundle unrelated changes
  • I followed the gitmoji specification for the name of the PR, including the package scope (e.g. 🐛(vitest) Something...) when the change targets a package other than fast-check
  • I added relevant tests and they would have failed without my PR (when applicable)

Generated by Claude Code

`segmentsToPathMapper` built the path with
`join(map(segments, (v) => `/${v}`), '')`, which allocates an
intermediate array of `/segment` strings and invokes a per-element
arrow closure before joining. Fuse it into a single accumulating loop
that appends `'/' + segment` directly, removing the intermediate array
and the map callback. Pure string-assembly refactor; the unmapper and
generated values are unchanged.

https://claude.ai/code/session_01SRx4yxfDXbBWd5JDy7nrtz
@changeset-bot

changeset-bot Bot commented Jun 4, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 31878c9

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
fast-check Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Jun 4, 2026

Copy link
Copy Markdown
@fast-check/ava

npm i https://pkg.pr.new/@fast-check/ava@7048

fast-check

npm i https://pkg.pr.new/fast-check@7048

@fast-check/jest

npm i https://pkg.pr.new/@fast-check/jest@7048

@fast-check/packaged

npm i https://pkg.pr.new/@fast-check/packaged@7048

@fast-check/poisoning

npm i https://pkg.pr.new/@fast-check/poisoning@7048

@fast-check/vitest

npm i https://pkg.pr.new/@fast-check/vitest@7048

@fast-check/worker

npm i https://pkg.pr.new/@fast-check/worker@7048

commit: 31878c9

@github-actions

github-actions Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

⏱️ Benchmark Results

Click to expand

 RUN  v4.1.5 /home/runner/work/fast-check/fast-check


 ✓  fast-check  test/bench/arbitraries.bench.ts > generate 45033ms
     name                                                         hz     min      max    mean     p75     p99    p995     p999      rme  samples
   · boolean()                                          6,893,596.10  0.0001   0.2341  0.0001  0.0001  0.0002  0.0003   0.0005   ±0.42%  3446799  [0.75x] ⇓
     boolean()                                          9,237,910.12  0.0001   0.3650  0.0001  0.0001  0.0001  0.0002   0.0004   ±0.39%  4618956  (baseline)
   · integer()                                          7,265,399.96  0.0001   0.2844  0.0001  0.0002  0.0002  0.0003   0.0003   ±0.19%  3632700  [0.77x] ⇓
     integer()                                          9,414,483.12  0.0001   0.1674  0.0001  0.0001  0.0002  0.0002   0.0002   ±0.14%  4707242  (baseline)
   · maxSafeInteger()                                   5,460,044.38  0.0001   0.2649  0.0002  0.0002  0.0003  0.0003   0.0005   ±0.21%  2730023  [0.83x] ⇓
     maxSafeInteger()                                   6,591,260.18  0.0001   0.2590  0.0002  0.0002  0.0002  0.0003   0.0004   ±0.20%  3295631  (baseline)
   · bigInt()                                           1,170,805.50  0.0006   0.8037  0.0009  0.0012  0.0018  0.0020   0.0105   ±0.52%   585403  [0.78x] ⇓
     bigInt()                                           1,492,725.56  0.0005   0.3452  0.0007  0.0009  0.0010  0.0013   0.0019   ±0.32%   746363  (baseline)
   · float()                                            4,391,443.44  0.0001   0.5598  0.0002  0.0003  0.0004  0.0004   0.0008   ±0.33%  2195722  [0.78x] ⇓
     float()                                            5,603,710.99  0.0001   0.2136  0.0002  0.0002  0.0002  0.0003   0.0006   ±0.19%  2801856  (baseline)
   · double()                                           1,707,575.20  0.0004   0.2770  0.0006  0.0007  0.0009  0.0011   0.0019   ±0.32%   853788  [0.81x] ⇓
     double()                                           2,112,153.06  0.0003   0.3724  0.0005  0.0006  0.0009  0.0011   0.0016   ±0.31%  1056077  (baseline)
   · date()                                             3,090,685.31  0.0003   0.5328  0.0003  0.0003  0.0004  0.0005   0.0013   ±0.35%  1545343  [0.77x] ⇓
     date()                                             4,021,749.86  0.0002   0.2145  0.0002  0.0003  0.0003  0.0004   0.0009   ±0.19%  2010875  (baseline)
   · string()                                           1,019,559.81  0.0002   0.5278  0.0010  0.0012  0.0022  0.0031   0.0113   ±0.47%   509780  [0.78x] ⇓
     string()                                           1,301,159.32  0.0002   0.3495  0.0008  0.0010  0.0016  0.0017   0.0029   ±0.36%   650580  (baseline)
   · string({ maxLength: 100, size: 'max' })              181,566.91  0.0003   0.4342  0.0055  0.0079  0.0159  0.0171   0.0249   ±0.62%    90785  [0.81x] ⇓
     string({ maxLength: 100, size: 'max' })              224,217.25  0.0002   0.3106  0.0045  0.0065  0.0125  0.0132   0.0196   ±0.52%   112110  (baseline)
   · string({ unit: 'grapheme' })                         653,624.11  0.0002   0.4912  0.0015  0.0020  0.0036  0.0041   0.0129   ±0.41%   326813  [0.77x] ⇓
     string({ unit: 'grapheme' })                         852,087.41  0.0002   0.3546  0.0012  0.0016  0.0026  0.0028   0.0098   ±0.36%   426044  (baseline)
   · base64String()                                       962,207.26  0.0003   0.4886  0.0010  0.0013  0.0023  0.0033   0.0117   ±0.48%   481104  [0.76x] ⇓
     base64String()                                     1,259,338.58  0.0002   0.3380  0.0008  0.0010  0.0016  0.0017   0.0039   ±0.40%   629670  (baseline)
   · array(integer())                                   1,682,661.21  0.0002   0.4518  0.0006  0.0006  0.0015  0.0024   0.0045   ±0.47%   841331  [0.79x] ⇓
     array(integer())                                   2,131,831.98  0.0002   0.8271  0.0005  0.0005  0.0011  0.0012   0.0016   ±0.47%  1065916  (baseline)
   · array(integer(), { maxLength: 100, size: 'max' })    342,438.86  0.0002   0.4386  0.0029  0.0039  0.0101  0.0108   0.0191   ±0.64%   171221  [0.84x] ⇓
     array(integer(), { maxLength: 100, size: 'max' })    407,212.39  0.0002   0.3100  0.0025  0.0034  0.0081  0.0087   0.0148   ±0.49%   203607  (baseline)
   · uniqueArray(integer())                             1,149,383.92  0.0002  23.0058  0.0009  0.0010  0.0020  0.0023   0.0051   ±9.25%   574692  [0.72x] ⇓
     uniqueArray(integer())                             1,587,879.34  0.0002   0.3798  0.0006  0.0008  0.0015  0.0016   0.0027   ±0.36%   793940  (baseline)
   · set(integer())                                       980,377.97  0.0003   0.4964  0.0010  0.0013  0.0023  0.0025   0.0116   ±0.45%   490190  [0.78x] ⇓
     set(integer())                                     1,255,229.08  0.0003   0.2321  0.0008  0.0010  0.0019  0.0025   0.0054   ±0.36%   627615  (baseline)
   · tuple(integer())                                   3,647,392.16  0.0002   0.4609  0.0003  0.0003  0.0004  0.0005   0.0013   ±0.53%  1823697  [0.78x] ⇓
     tuple(integer())                                   4,666,657.59  0.0002   0.3399  0.0002  0.0002  0.0003  0.0004   0.0009   ±0.50%  2333329  (baseline)
   · record({ a: integer() })                           1,778,694.85  0.0004   0.4193  0.0006  0.0006  0.0009  0.0013   0.0018   ±0.53%   889348  [0.80x] ⇓
     record({ a: integer() })                           2,217,923.81  0.0003   0.3772  0.0005  0.0005  0.0008  0.0010   0.0014   ±0.45%  1108962  (baseline)
   · dictionary(string(), integer())                      110,415.64  0.0005   4.4398  0.0091  0.0130  0.0228  0.0253   0.0349   ±2.52%    55209  [0.89x] ⇓
     dictionary(string(), integer())                      124,703.94  0.0004  19.7837  0.0080  0.0106  0.0263  0.0307   0.0420   ±8.08%    62352  (baseline)
   · map(string(), integer())                             170,979.29  0.0003   0.4687  0.0058  0.0087  0.0149  0.0186   0.0269   ±0.70%    85490  [0.82x] ⇓
     map(string(), integer())                             208,501.73  0.0002   0.3379  0.0048  0.0071  0.0149  0.0172   0.0253   ±0.59%   104252  (baseline)
   · constantFrom(...)                                  7,621,921.57  0.0001   0.6228  0.0001  0.0001  0.0002  0.0002   0.0003   ±0.27%  3810961  [0.78x] ⇓
     constantFrom(...)                                  9,799,033.31  0.0001   0.3876  0.0001  0.0001  0.0001  0.0001   0.0002   ±0.17%  4899517  (baseline)
   · oneof(integer(), integer())                        3,929,101.10  0.0002   0.6324  0.0003  0.0003  0.0004  0.0005   0.0013   ±0.33%  1964551  [0.77x] ⇓
     oneof(integer(), integer())                        5,076,050.39  0.0001   0.4850  0.0002  0.0002  0.0003  0.0003   0.0007   ±0.22%  2538026  (baseline)
   · oneof({ weight, arbitrary }, ...)                  4,111,210.96  0.0002   0.0623  0.0002  0.0003  0.0004  0.0004   0.0009   ±0.12%  2055606  [0.79x] ⇓
     oneof({ weight, arbitrary }, ...)                  5,209,841.17  0.0001   0.0376  0.0002  0.0002  0.0003  0.0003   0.0006   ±0.09%  2604921  (baseline)
   · option(integer())                                  4,245,041.75  0.0001   0.4807  0.0002  0.0003  0.0003  0.0004   0.0011   ±0.38%  2122521  [0.80x] ⇓
     option(integer())                                  5,282,137.38  0.0001   0.3746  0.0002  0.0002  0.0003  0.0003   0.0007   ±0.33%  2641069  (baseline)
   · subarray([1, 2, 3, 4, 5])                          1,896,897.70  0.0002   0.5590  0.0005  0.0007  0.0010  0.0013   0.0021   ±0.59%   948450  [0.75x] ⇓
     subarray([1, 2, 3, 4, 5])                          2,544,851.34  0.0002   0.3507  0.0004  0.0005  0.0007  0.0009   0.0014   ±0.37%  1272426  (baseline)
   · letrec(tree)                                         775,351.66  0.0002   0.4790  0.0013  0.0019  0.0055  0.0062   0.0134   ±0.62%   387676  [0.78x] ⇓
     letrec(tree)                                         999,553.56  0.0001   0.3415  0.0010  0.0015  0.0042  0.0048   0.0096   ±0.47%   499779  (baseline)
   · memo(tree)                                           808,138.93  0.0002   0.5758  0.0012  0.0021  0.0041  0.0044   0.0134   ±0.65%   404070  [0.79x] ⇓
     memo(tree)                                         1,029,136.86  0.0001   0.3450  0.0010  0.0017  0.0033  0.0034   0.0087   ±0.47%   514569  (baseline)
   · anything()                                            80,435.55  0.0003   2.0794  0.0124  0.0206  0.0641  0.0738   0.1024   ±2.24%    40218  [0.79x] ⇓
     anything()                                           102,065.99  0.0002   4.0333  0.0098  0.0163  0.0508  0.0578   0.0781   ±2.38%    51035  (baseline)
   · json()                                                74,863.99  0.0003   4.8250  0.0134  0.0221  0.0671  0.0763   0.1026   ±2.71%    37433  [0.79x] ⇓
     json()                                                94,859.62  0.0002   1.0154  0.0105  0.0178  0.0540  0.0610   0.0784   ±1.72%    47431  (baseline)
   · entityGraph(node -> node)                              7,127.19  0.0079  26.9414  0.1403  0.1260  0.3264  0.3842  10.5653  ±15.95%     3613  [0.76x] ⇓
     entityGraph(node -> node)                              9,320.55  0.0062   8.7051  0.1073  0.0982  0.1726  0.2077   8.2832  ±12.15%     4661  (baseline)
   · emailAddress()                                        92,165.22  0.0025   0.4729  0.0109  0.0139  0.0216  0.0260   0.0323   ±0.65%    46084  [0.79x] ⇓
     emailAddress()                                       116,543.95  0.0019   0.5560  0.0086  0.0109  0.0169  0.0202   0.0281   ±0.76%    58272  (baseline)
   · webUrl()                                             171,229.50  0.0023   0.5369  0.0058  0.0068  0.0106  0.0164   0.0217   ±0.64%    85615  [0.79x] ⇓
     webUrl()                                             216,091.74  0.0018   0.4520  0.0046  0.0054  0.0086  0.0120   0.0181   ±0.73%   108046  (baseline)
   · ipV4()                                             1,248,317.35  0.0006   0.5511  0.0008  0.0008  0.0011  0.0015   0.0032   ±0.51%   624159  [0.77x] ⇓
     ipV4()                                             1,618,471.83  0.0005   0.5645  0.0006  0.0006  0.0008  0.0011   0.0016   ±0.63%   809236  (baseline)
   · ipV6()                                               236,199.81  0.0005   0.5302  0.0042  0.0051  0.0075  0.0115   0.0184   ±0.60%   118101  [0.82x] ⇓
     ipV6()                                               289,615.99  0.0004  18.2143  0.0035  0.0039  0.0098  0.0120   0.0159   ±7.16%   144808  (baseline)
   · uuid()                                               701,345.62  0.0010   0.5572  0.0014  0.0015  0.0022  0.0025   0.0123   ±0.50%   350673  [0.77x] ⇓
     uuid()                                               915,498.10  0.0008   0.3776  0.0011  0.0011  0.0016  0.0018   0.0096   ±0.40%   457750  (baseline)
   · stringMatching(/^[a-zA-Z0-9]+$/)                     502,037.41  0.0008   0.5727  0.0020  0.0025  0.0038  0.0041   0.0140   ±0.65%   251019  [0.78x] ⇓
     stringMatching(/^[a-zA-Z0-9]+$/)                     640,823.71  0.0006   8.2633  0.0016  0.0019  0.0029  0.0030   0.0110   ±3.27%   320413  (baseline)
   · mixedCase(string())                                  519,167.78  0.0005   0.5629  0.0019  0.0023  0.0039  0.0064   0.0194   ±0.61%   259584  [0.75x] ⇓
     mixedCase(string())                                  692,609.26  0.0004   0.3828  0.0014  0.0018  0.0027  0.0028   0.0105   ±0.46%   346306  (baseline)
   · integer().map(.)                                   5,131,867.35  0.0001   0.6963  0.0002  0.0002  0.0003  0.0003   0.0007   ±0.63%  2565934  [0.77x] ⇓
     integer().map(.)                                   6,646,346.52  0.0001   0.2883  0.0002  0.0002  0.0002  0.0002   0.0005   ±0.47%  3323174  (baseline)
   · integer().chain(.)                                 2,934,753.18  0.0002   0.5151  0.0003  0.0004  0.0005  0.0006   0.0013   ±0.65%  1467377  [0.78x] ⇓
     integer().chain(.)                                 3,783,300.93  0.0002   0.4241  0.0003  0.0003  0.0004  0.0004   0.0010   ±0.53%  1891651  (baseline)
   · integer().filter(.)                                2,909,880.39  0.0001  14.4772  0.0003  0.0004  0.0010  0.0011   0.0016   ±5.68%  1454941  [0.76x] ⇓
     integer().filter(.)                                3,822,610.63  0.0001  10.9131  0.0003  0.0003  0.0008  0.0009   0.0012   ±4.28%  1911306  (baseline)

 ✓  fast-check  test/bench/runners.bench.ts > runner 2429ms
     name                                                     hz     min     max    mean     p75     p99    p995    p999     rme  samples
   · assert on sync predicate not returning anything   12,419.55  0.0772  0.3121  0.0805  0.0785  0.1613  0.1733  0.2100  ±0.38%     6210  [0.77x] ⇓
     assert on sync predicate not returning anything   16,067.29  0.0600  0.2371  0.0622  0.0607  0.1192  0.1269  0.1487  ±0.31%     8034  (baseline)
   · assert on sync predicate returning true           12,339.48  0.0770  0.3443  0.0810  0.0784  0.1660  0.1825  0.2670  ±0.47%     6170  [0.76x] ⇓
     assert on sync predicate returning true           16,154.28  0.0599  0.2651  0.0619  0.0605  0.1184  0.1254  0.1770  ±0.32%     8078  (baseline)
   · assert on async predicate not returning anything   8,408.45  0.1117  0.3960  0.1189  0.1152  0.2047  0.2257  0.2696  ±0.47%     4205  [0.77x] ⇓
     assert on async predicate not returning anything  10,898.55  0.0874  0.2729  0.0918  0.0892  0.1501  0.1543  0.1737  ±0.34%     5450  (baseline)
   · check on sync predicate not returning anything    12,289.47  0.0771  0.3822  0.0814  0.0785  0.1630  0.1779  0.2396  ±0.47%     6145  [0.79x] ⇓
     check on sync predicate not returning anything    15,632.62  0.0601  0.9559  0.0640  0.0620  0.1143  0.1353  0.2222  ±0.53%     7817  (baseline)

 BENCH  Summary

   fast-check  constantFrom(...) - test/bench/arbitraries.bench.ts > generate
    1.05x faster than integer()
    1.11x faster than boolean()
    1.40x faster than maxSafeInteger()
    1.49x faster than integer().map(.)
    1.74x faster than float()
    1.80x faster than option(integer())
    1.85x faster than oneof({ weight, arbitrary }, ...)
    1.94x faster than oneof(integer(), integer())
    2.09x faster than tuple(integer())
    2.47x faster than date()
    2.60x faster than integer().chain(.)
    2.62x faster than integer().filter(.)
    4.02x faster than subarray([1, 2, 3, 4, 5])
    4.29x faster than record({ a: integer() })
    4.46x faster than double()
    4.53x faster than array(integer())
    6.11x faster than ipV4()
    6.51x faster than bigInt()
    6.63x faster than uniqueArray(integer())
    7.48x faster than string()
    7.77x faster than set(integer())
    7.92x faster than base64String()
    9.43x faster than memo(tree)
    9.83x faster than letrec(tree)
    10.87x faster than uuid()
    11.66x faster than string({ unit: 'grapheme' })
    14.68x faster than mixedCase(string())
    15.18x faster than stringMatching(/^[a-zA-Z0-9]+$/)
    22.26x faster than array(integer(), { maxLength: 100, size: 'max' })
    32.27x faster than ipV6()
    41.98x faster than string({ maxLength: 100, size: 'max' })
    44.51x faster than webUrl()
    44.58x faster than map(string(), integer())
    69.03x faster than dictionary(string(), integer())
    82.70x faster than emailAddress()
    94.76x faster than anything()
    101.81x faster than json()
    1069.41x faster than entityGraph(node -> node)

   fast-check  assert on sync predicate not returning anything - test/bench/runners.bench.ts > runner
    1.01x faster than assert on sync predicate returning true
    1.01x faster than check on sync predicate not returning anything
    1.48x faster than assert on async predicate not returning anything

Benchmark report written to /home/runner/work/fast-check/fast-check/benchmark.json

Commit: 31878c9 (merge: 0e887e9)

@codecov

codecov Bot commented Jun 4, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.87%. Comparing base (dc9f32a) to head (31878c9).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7048      +/-   ##
==========================================
+ Coverage   94.86%   94.87%   +0.01%     
==========================================
  Files         212      212              
  Lines        5884     5896      +12     
  Branches     1543     1548       +5     
==========================================
+ Hits         5582     5594      +12     
  Misses        294      294              
  Partials        8        8              
Flag Coverage Δ
tests 94.87% <100.00%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dubzzz
dubzzz enabled auto-merge (squash) June 5, 2026 03:15
@dubzzz
dubzzz merged commit 3b2a942 into main Jun 5, 2026
58 checks passed
@dubzzz
dubzzz deleted the claude/perf-webPath-fuse-segments branch June 5, 2026 03:22
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.

2 participants