Skip to content

Commit 512d3ef

Browse files
committed
url: reduce search params sort buffering
URLSearchParams.sort() copies both merge runs into temporary arrays. Buffer only the left run and read the right run from the output array, removing one scratch-array allocation and the right-run copying. While left-side elements remain, the output cursor stays behind unread right-side elements. Once the left run is exhausted, the right remainder is already in place. Taking the left key on equality preserves stability. The insertion-sort path below 50 pairs is unchanged. Assisted-by: GitHub Copilot Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com>
1 parent e7fcd60 commit 512d3ef

3 files changed

Lines changed: 113 additions & 24 deletions

File tree

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const common = require('../common.js');
4+
const assert = require('node:assert');
5+
6+
const bench = common.createBenchmark(main, {
7+
n: [1e4],
8+
size: [49, 50, 65, 256, 1024],
9+
order: ['sorted', 'reversed', 'shuffled', 'duplicates'],
10+
});
11+
12+
function main({ n, size, order }) {
13+
const entries = Array.from({ length: size }, (_, index) => [
14+
`key-${String(order === 'duplicates' ? index % 7 : index).padStart(8, '0')}`,
15+
String(index),
16+
]);
17+
if (order === 'reversed') {
18+
entries.reverse();
19+
} else if (order === 'shuffled') {
20+
let seed = 12345;
21+
for (let index = entries.length - 1; index > 0; index--) {
22+
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
23+
const other = seed % (index + 1);
24+
[entries[index], entries[other]] = [entries[other], entries[index]];
25+
}
26+
}
27+
const base = new URLSearchParams(entries);
28+
const expected = entries.slice().sort(([first], [second]) =>
29+
(first < second ? -1 : first > second ? 1 : 0));
30+
31+
for (let index = 0; index < 1000; index++) {
32+
new URLSearchParams(base).sort();
33+
}
34+
let result;
35+
bench.start();
36+
for (let index = 0; index < n; index++) {
37+
result = new URLSearchParams(base);
38+
result.sort();
39+
}
40+
bench.end(n);
41+
assert.deepStrictEqual([...result], expected);
42+
assert.deepStrictEqual([...base], entries);
43+
}

‎lib/internal/url.js‎

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -738,15 +738,14 @@ class URLSearchParams {
738738
} else {
739739
// Bottom-up iterative stable merge sort
740740
const lBuffer = new Array(len);
741-
const rBuffer = new Array(len);
742741
for (let step = 2; step < len; step *= 2) {
743742
for (let start = 0; start < len - 2; start += 2 * step) {
744743
const mid = start + step;
745744
let end = mid + step;
746745
end = end < len ? end : len;
747746
if (mid > end)
748747
continue;
749-
merge(a, start, mid, end, lBuffer, rBuffer);
748+
merge(a, start, mid, end, lBuffer);
750749
}
751750
}
752751
}
@@ -1428,32 +1427,25 @@ function serializeParams(array) {
14281427
}
14291428

14301429
// for merge sort
1431-
function merge(out, start, mid, end, lBuffer, rBuffer) {
1430+
function merge(out, start, mid, end, lBuffer) {
14321431
const sizeLeft = mid - start;
1433-
const sizeRight = end - mid;
1434-
let l, r, o;
1435-
1436-
for (l = 0; l < sizeLeft; l++)
1437-
lBuffer[l] = out[start + l];
1438-
for (r = 0; r < sizeRight; r++)
1439-
rBuffer[r] = out[mid + r];
1440-
1441-
l = 0;
1442-
r = 0;
1443-
o = start;
1444-
while (l < sizeLeft && r < sizeRight) {
1445-
if (lBuffer[l] <= rBuffer[r]) {
1446-
out[o++] = lBuffer[l++];
1447-
out[o++] = lBuffer[l++];
1432+
for (let index = 0; index < sizeLeft; index++)
1433+
lBuffer[index] = out[start + index];
1434+
1435+
let left = 0;
1436+
let right = mid;
1437+
let output = start;
1438+
while (left < sizeLeft && right < end) {
1439+
if (lBuffer[left] <= out[right]) {
1440+
out[output++] = lBuffer[left++];
1441+
out[output++] = lBuffer[left++];
14481442
} else {
1449-
out[o++] = rBuffer[r++];
1450-
out[o++] = rBuffer[r++];
1443+
out[output++] = out[right++];
1444+
out[output++] = out[right++];
14511445
}
14521446
}
1453-
while (l < sizeLeft)
1454-
out[o++] = lBuffer[l++];
1455-
while (r < sizeRight)
1456-
out[o++] = rBuffer[r++];
1447+
while (left < sizeLeft)
1448+
out[output++] = lBuffer[left++];
14571449
}
14581450

14591451
function domainToASCII(domain) {
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
'use strict';
2+
3+
require('../common');
4+
const assert = require('node:assert');
5+
6+
for (const size of [0, 1, 2, 49, 50, 51, 63, 64, 65, 127, 128, 129,
7+
255, 256, 257, 1023, 1024, 1025]) {
8+
const ascending = Array.from({ length: size }, (_, index) => [
9+
String(index).padStart(8, '0'), String(index),
10+
]);
11+
const descending = ascending.slice().reverse();
12+
const duplicates = ascending.map(([, value], index) => [
13+
String(index % 7), value,
14+
]);
15+
const shuffled = ascending.slice();
16+
let seed = 12345;
17+
for (let index = size - 1; index > 0; index--) {
18+
seed = (Math.imul(seed, 1664525) + 1013904223) >>> 0;
19+
const other = seed % (index + 1);
20+
[shuffled[index], shuffled[other]] = [shuffled[other], shuffled[index]];
21+
}
22+
23+
for (const entries of [ascending, descending, duplicates, shuffled]) {
24+
const expected = entries.slice().sort(([first], [second]) =>
25+
(first < second ? -1 : first > second ? 1 : 0));
26+
const params = new URLSearchParams(entries);
27+
const iterator = params.entries();
28+
params.toString();
29+
assert.strictEqual(params.sort(), undefined);
30+
assert.deepStrictEqual([...params], expected);
31+
assert.deepStrictEqual([...iterator], expected);
32+
assert.strictEqual(params.size, size);
33+
assert.strictEqual(params.toString(), new URLSearchParams(expected).toString());
34+
params.sort();
35+
assert.deepStrictEqual([...params], expected);
36+
}
37+
}
38+
39+
{
40+
const entries = Array.from({ length: 65 }, (_, index) => [
41+
['\u00e9', 'e\u0301', '\ud83d\ude00', '\uffff', ''][index % 5],
42+
`value ${index}`,
43+
]);
44+
const expected = entries.slice().sort(([first], [second]) =>
45+
(first < second ? -1 : first > second ? 1 : 0));
46+
const url = new URL('https://example.org/');
47+
for (const [name, value] of entries) url.searchParams.append(name, value);
48+
assert.strictEqual(url.search, `?${new URLSearchParams(entries)}`);
49+
const params = url.searchParams;
50+
params.sort();
51+
assert.strictEqual(url.searchParams, params);
52+
assert.deepStrictEqual([...params], expected);
53+
assert.strictEqual(url.search, `?${new URLSearchParams(expected)}`);
54+
}

0 commit comments

Comments
 (0)