Files
scriptc/benchmarks/scaling/strings.ts
Malte Ubl 1c5dfda937 Add a scaling probe suite and strace tools for pathology hunting
benchmarks/scaling: 128 probes of common operations (arrays, strings,
Map/Set/records, JSON, closures, exceptions, regex, dates, promises,
timers, console, fs) run at 3-4 sizes under scriptc and Node with
identical-stdout checks; run.mjs flags superlinear growth and >3x Node,
--strace-only reports syscalls per operation, strace-workloads.mjs
straces the runtime benchmark suite. Results and the full probe table:
results/h40-pathology-hunt/REPORT.md.

Pathologies fixed on this branch (probe ms at 1e3/1e4/1e5/1e6, Linux):
  i in number[]        base 43/5291/timeout   -> 0.03/0.20/1.5/15   (Node 0.19/0.73/6.8/39)
  k in Record<string>  base 4.8/436/timeout   -> 0.15/1.5/20/319    (Node 0.96/8.9/100/1452)
  re.exec /g loop      base 1.1/70/7910/t.o.  -> 0.49/4.1/40/416    (Node 0.47/4.6/43/398)
  clearTimeout n live  base 1.3/10/1014/t.o.  -> 1.3/3.9/38/740     (Node 3.8/9.4/46/527)
  new RegExp repeat    base 3.9/38/368/3811   -> 0.13/1.1/11/106    (Node 0.49/2.9/23/174)
  console.error        2 writes/line          -> 1 write/line

Suite A/B, Linux x86_64 sandbox, candidate vs perf/stack/06 (--layouts=4
--runs=16), all workloads ok:

| workload | baseline ms | candidate ms | change | 95% CI | verdict | size | node ms | candidate/node |
| --- | ---: | ---: | ---: | --- | --- | ---: | ---: | ---: |
| json-records | 202.3 | 201.9 | -0.2% | -7.0% .. +3.8% | neutral | +0.0% | 458.29 | 0.44x |
| regex-logs | 166.8 | 166.4 | -0.2% | -1.6% .. +1.1% | neutral | +0.1% | 239.97 | 0.69x |
| ast-interp | 268.6 | 269.9 | +0.5% | -4.6% .. +3.8% | neutral | +0.0% | 242.7 | 1.11x |
| records-sort | 648.7 | 670.4 | +3.3% | -4.7% .. +10.0% | neutral | +0.0% | 222.69 | 3.01x |
| template-render | 69.6 | 70.7 | +1.7% | -0.2% .. +2.6% | neutral | +0.0% | 187.3 | 0.38x |
| functional-pipeline | 221.8 | 214.9 | -3.1% | -7.3% .. +0.7% | neutral | +0.0% | 249.06 | 0.86x |
| async-pipeline | 197.3 | 198.2 | +0.4% | -1.2% .. +3.0% | neutral | +0.2% | 89.25 | 2.22x |
| word-graph | 259.2 | 256.0 | -1.3% | -6.3% .. +3.0% | neutral | +0.0% | 469.48 | 0.55x |
| alloc-trees | 318.8 | 316.8 | -0.6% | -3.0% .. +1.2% | neutral | +0.0% | 127.68 | 2.48x |
| numeric-kernels | 46.4 | 46.5 | +0.1% | -1.6% .. +3.9% | neutral | +0.0% | 133.42 | 0.35x |
| validate-errors | 176.5 | 178.1 | +0.9% | -3.1% .. +4.7% | neutral | +0.0% | 702.45 | 0.25x |
| csv-numbers | 336.2 | 336.8 | +0.2% | -2.0% .. +1.2% | neutral | +0.0% | 500.14 | 0.67x |
| log-summary | 165.7 | 164.6 | -0.6% | -5.9% .. +1.9% | neutral | +0.0% | 290.81 | 0.57x |
| inventory-report | 321.9 | 320.1 | -0.6% | -6.7% .. +9.5% | neutral | +0.0% | 431.4 | 0.74x |

geomean change: +0.0%
geomean candidate/node: 0.768x (lower is better; goal: well below 1)

macOS lane not run (all measurement in the sandbox per task instructions).
2026-10-08 15:48:28 -05:00

252 lines
7.8 KiB
TypeScript

// Scaling probes: string operations. Usage: strings <case> <n>
let seed = 12345;
function rnd(): number {
seed = (Math.imul(seed, 1103515245) + 12345) & 0x7fffffff;
return seed / 0x7fffffff;
}
function words(n: number): string {
const parts: string[] = [];
for (let i = 0; i < n; i++) parts.push("w" + (i % 977));
return parts.join(" ");
}
function concatLoop(n: number): string {
let s = "";
for (let i = 0; i < n; i++) s += "x";
return `${s.length}`;
}
function concatNum(n: number): string {
let s = "";
for (let i = 0; i < n; i++) s += i + ",";
return `${s.length}`;
}
function concatRead(n: number): string {
// Append and read the tail every iteration (rope flatten pathology).
let s = "";
let c = 0;
for (let i = 0; i < n; i++) {
s += String.fromCharCode(97 + (i % 26));
c += s.charCodeAt(s.length - 1);
}
return `${s.length} ${c}`;
}
function concatThenScan(n: number): string {
let s = "";
for (let i = 0; i < n; i++) s += String.fromCharCode(97 + (i % 26));
let c = 0;
for (let i = 0; i < s.length; i++) c += s.charCodeAt(i);
return `${c}`;
}
function concatPrepend(n: number): string {
let s = "";
for (let i = 0; i < n; i++) s = "y" + s;
return `${s.length}`;
}
function template(n: number): string {
let total = 0;
for (let i = 0; i < n; i++) total += `item-${i}: ${i * 2} (${i % 7})`.length;
return `${total}`;
}
function splitJoin(n: number): string {
const s = words(n);
const p = s.split(" ");
return `${p.length} ${p.join("-").length}`;
}
function splitChars(n: number): string {
const s = "abcdefghij".repeat(n / 10);
return `${s.split("").length}`;
}
function replaceOne(n: number): string {
const s = words(n);
let t = 0;
for (let k = 0; k < 10; k++) t += s.replace("w5", "Q").length;
return `${t}`;
}
function replaceAll(n: number): string {
const s = words(n);
return `${s.replaceAll(" ", "_").length} ${s.replaceAll("w1", "ZZZ").length}`;
}
function replaceRegex(n: number): string {
const s = words(n);
return `${s.replace(/w(\d)/g, "v$1").length}`;
}
function replaceFn(n: number): string {
const s = words(n);
return `${s.replace(/\d+/g, (m) => String(Number(m) + 1)).length}`;
}
function sliceLoop(n: number): string {
const s = words(n);
let t = 0;
for (let i = 0; i + 5 < s.length && i < n * 4; i++) t += s.slice(i, i + 5).length;
return `${t}`;
}
function substringLoop(n: number): string {
const s = words(n);
let t = 0;
for (let i = 0; i + 5 < s.length && i < n * 4; i++) if (s.substring(i, i + 2) === "w1") t++;
return `${t}`;
}
function indexOfAll(n: number): string {
const s = words(n);
let c = 0;
let pos = s.indexOf("w12");
while (pos !== -1) {
c++;
pos = s.indexOf("w12", pos + 1);
}
return `${c}`;
}
function indexOfMiss(n: number): string {
const s = words(n);
let c = 0;
for (let k = 0; k < 20; k++) c += s.indexOf("zz" + k);
return `${c}`;
}
function includesLoop(n: number): string {
const s = words(n);
let c = 0;
for (let k = 0; k < 20; k++) if (s.includes("w" + (970 + k))) c++;
return `${c}`;
}
function padStart(n: number): string {
let t = 0;
for (let i = 0; i < n; i++) t += String(i).padStart(10, "0").length;
return `${t}`;
}
function repeat(n: number): string {
let t = 0;
for (let k = 0; k < 10; k++) t += "ab".repeat(n).length;
return `${t}`;
}
function upperLower(n: number): string {
const s = words(n);
return `${s.toUpperCase().length} ${s.toLowerCase().length}`;
}
function charAtLoop(n: number): string {
const s = words(n);
let c = 0;
for (let i = 0; i < s.length; i++) if (s.charAt(i) === "w") c++;
return `${c}`;
}
function indexLoop(n: number): string {
const s = words(n);
let c = 0;
for (let i = 0; i < s.length; i++) if (s[i] === " ") c++;
return `${c}`;
}
function forOfChars(n: number): string {
const s = words(n);
let c = 0;
for (const ch of s) if (ch === "1") c++;
return `${c}`;
}
function nonAsciiConcat(n: number): string {
let s = "";
for (let i = 0; i < n; i++) s += i % 10 === 0 ? "é" : "a";
let c = 0;
for (let i = 0; i < s.length; i += 7) c += s.charCodeAt(i);
return `${s.length} ${c}`;
}
function nonAsciiIndex(n: number): string {
const parts: string[] = [];
for (let i = 0; i < n; i++) parts.push(i % 10 === 0 ? "ü" : "w");
const s = parts.join("");
let c = 0;
for (let i = 0; i < s.length; i++) c += s.charCodeAt(i);
return `${c}`;
}
function nonAsciiSlice(n: number): string {
const parts: string[] = [];
for (let i = 0; i < n; i++) parts.push(i % 10 === 0 ? "ü" : "w");
const s = parts.join("");
let c = 0;
for (let i = 0; i + 3 < s.length; i++) c += s.slice(i, i + 3).length;
return `${c}`;
}
function strCompare(n: number): string {
const a: string[] = [];
for (let i = 0; i < n; i++) a.push("key" + Math.floor(rnd() * 1e6));
let c = 0;
for (let i = 1; i < n; i++) if (a[i]! < a[i - 1]!) c++;
return `${c}`;
}
function trimLines(n: number): string {
let t = 0;
for (let i = 0; i < n; i++) t += (" line " + i + " ").trim().length;
return `${t}`;
}
function linesSplit(n: number): string {
const lines: string[] = [];
for (let i = 0; i < n; i++) lines.push(`${i},name${i},${i * 3}`);
const text = lines.join("\n");
let s = 0;
for (const line of text.split("\n")) s += Number(line.split(",")[2]);
return `${s}`;
}
function numToString(n: number): string {
let t = 0;
for (let i = 0; i < n; i++) t += String(i * 1.5).length + (i * 0.1).toFixed(2).length;
return `${t}`;
}
function parseNums(n: number): string {
let s = 0;
for (let i = 0; i < n; i++) s += parseInt("" + i, 10) + parseFloat(i + ".5");
return `${s}`;
}
function startsWith(n: number): string {
const s = words(n);
const p = s.split(" ");
let c = 0;
for (const w of p) if (w.startsWith("w9") || w.endsWith("7")) c++;
return `${c}`;
}
function localeCompare(n: number): string {
const a: string[] = [];
for (let i = 0; i < n; i++) a.push("k" + Math.floor(rnd() * 1e6));
a.sort((x, y) => x.localeCompare(y));
return `${a[0]} ${a[n - 1]}`;
}
const which = process.argv[2] ?? "";
const n = Number(process.argv[3] ?? "1000");
const t0 = performance.now();
let out = "";
switch (which) {
case "concat-loop": out = concatLoop(n); break;
case "concat-num": out = concatNum(n); break;
case "concat-read": out = concatRead(n); break;
case "concat-then-scan": out = concatThenScan(n); break;
case "concat-prepend": out = concatPrepend(n); break;
case "template": out = template(n); break;
case "split-join": out = splitJoin(n); break;
case "split-chars": out = splitChars(n); break;
case "replace-one": out = replaceOne(n); break;
case "replace-all": out = replaceAll(n); break;
case "replace-regex": out = replaceRegex(n); break;
case "replace-fn": out = replaceFn(n); break;
case "slice-loop": out = sliceLoop(n); break;
case "substring-loop": out = substringLoop(n); break;
case "indexOf-all": out = indexOfAll(n); break;
case "indexOf-miss": out = indexOfMiss(n); break;
case "includes": out = includesLoop(n); break;
case "padStart": out = padStart(n); break;
case "repeat": out = repeat(n); break;
case "upper-lower": out = upperLower(n); break;
case "charAt-loop": out = charAtLoop(n); break;
case "index-loop": out = indexLoop(n); break;
case "for-of-chars": out = forOfChars(n); break;
case "nonascii-concat": out = nonAsciiConcat(n); break;
case "nonascii-index": out = nonAsciiIndex(n); break;
case "nonascii-slice": out = nonAsciiSlice(n); break;
case "str-compare": out = strCompare(n); break;
case "trim": out = trimLines(n); break;
case "lines-split": out = linesSplit(n); break;
case "num-to-string": out = numToString(n); break;
case "parse-nums": out = parseNums(n); break;
case "starts-with": out = startsWith(n); break;
case "locale-compare": out = localeCompare(n); break;
default: out = "unknown case";
}
const t1 = performance.now();
console.log(which, n, out);
console.error("T=" + (t1 - t0).toFixed(3));