@sproutboat/runtime 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/package.json +1 -1
- package/src/native-fetch-prelude.js +30 -10
- package/src/utf8-decode.test.js +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @sproutboat/runtime
|
|
2
2
|
|
|
3
|
+
## 0.6.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Fix two compounding performance regressions in the `#181` fix, caught after
|
|
8
|
+
it went live and made sproutboat.com's homepage take 2.6-2.8s per request:
|
|
9
|
+
|
|
10
|
+
1. `__sbRawBodyResponse` decoded eagerly at construction, paying the cost of
|
|
11
|
+
`__sbFromUtf8` on every asset/proxied response regardless of whether the
|
|
12
|
+
handler ever called `.text()`/`.json()` on it (the common case is a
|
|
13
|
+
response handed straight back to the client, never read as text). Now
|
|
14
|
+
decodes lazily on first call and memoizes.
|
|
15
|
+
|
|
16
|
+
2. `__sbFromUtf8` built its result with repeated `out +=`. Porffor's strings
|
|
17
|
+
have no rope/cons optimization, so `+=` in a loop copies the whole
|
|
18
|
+
accumulated string on every append, making a large decode `O(n^2)`.
|
|
19
|
+
Measured directly against the real broker-sourced 43KB homepage body,
|
|
20
|
+
each successive 8000-character chunk took visibly longer than the last.
|
|
21
|
+
Now builds into an array and joins once.
|
|
22
|
+
|
|
23
|
+
Together these took the homepage from ~2.7s to ~25ms (measured via the
|
|
24
|
+
runtime's own `x-sb-cpu-ms` header, confirming real CPU time, not I/O wait).
|
|
25
|
+
Every other route was already unaffected by fix 1 alone, since they never
|
|
26
|
+
call `.text()`. Verified against a real compiled `porf native` binary with
|
|
27
|
+
the actual page content, and end to end on sproutboat-site's own deployment.
|
|
28
|
+
|
|
3
29
|
## 0.6.2
|
|
4
30
|
|
|
5
31
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -339,7 +339,15 @@ function __sbBufFrom(latin1) {
|
|
|
339
339
|
// regardless of whether it holds real Latin-1 text or raw bytes, so this
|
|
340
340
|
// decode is only safe where the caller already knows which one it has.
|
|
341
341
|
function __sbFromUtf8(bytes) {
|
|
342
|
-
|
|
342
|
+
// Collected into an array and joined once at the end, not built with
|
|
343
|
+
// repeated `out +=` -- Porffor's strings have no rope/cons optimization, so
|
|
344
|
+
// `+=` in a loop copies the whole accumulated string on every append,
|
|
345
|
+
// making the naive version O(n^2). Measured on the real 43KB homepage
|
|
346
|
+
// (dynamic, wire-sourced content, not a compile-time constant): each
|
|
347
|
+
// successive 8000-character chunk took visibly longer than the last, ~2.8s
|
|
348
|
+
// total -- a real, shipped performance regression on sproutboat.com.
|
|
349
|
+
// `Array.prototype.join` builds the result in one pass.
|
|
350
|
+
const out = [];
|
|
343
351
|
const len = bytes.length;
|
|
344
352
|
const cont = (j) => {
|
|
345
353
|
const b = bytes.charCodeAt(j) & 0xff;
|
|
@@ -348,13 +356,13 @@ function __sbFromUtf8(bytes) {
|
|
|
348
356
|
for (let i = 0; i < len; i++) {
|
|
349
357
|
const b0 = bytes.charCodeAt(i) & 0xff;
|
|
350
358
|
if (b0 < 0x80) {
|
|
351
|
-
out
|
|
359
|
+
out.push(String.fromCharCode(b0));
|
|
352
360
|
continue;
|
|
353
361
|
}
|
|
354
362
|
if ((b0 & 0xe0) == 0xc0 && i + 1 < len) {
|
|
355
363
|
const c1 = cont(i + 1);
|
|
356
364
|
if (c1 >= 0) {
|
|
357
|
-
out
|
|
365
|
+
out.push(String.fromCharCode(((b0 & 0x1f) << 6) | c1));
|
|
358
366
|
i += 1;
|
|
359
367
|
continue;
|
|
360
368
|
}
|
|
@@ -362,7 +370,7 @@ function __sbFromUtf8(bytes) {
|
|
|
362
370
|
const c1 = cont(i + 1);
|
|
363
371
|
const c2 = cont(i + 2);
|
|
364
372
|
if (c1 >= 0 && c2 >= 0) {
|
|
365
|
-
out
|
|
373
|
+
out.push(String.fromCharCode(((b0 & 0x0f) << 12) | (c1 << 6) | c2));
|
|
366
374
|
i += 2;
|
|
367
375
|
continue;
|
|
368
376
|
}
|
|
@@ -373,16 +381,16 @@ function __sbFromUtf8(bytes) {
|
|
|
373
381
|
if (c1 >= 0 && c2 >= 0 && c3 >= 0) {
|
|
374
382
|
let cp = ((b0 & 0x07) << 18) | (c1 << 12) | (c2 << 6) | c3;
|
|
375
383
|
cp -= 0x10000;
|
|
376
|
-
out
|
|
384
|
+
out.push(String.fromCharCode(0xd800 + (cp >> 10), 0xdc00 + (cp & 0x3ff)));
|
|
377
385
|
i += 3;
|
|
378
386
|
continue;
|
|
379
387
|
}
|
|
380
388
|
}
|
|
381
389
|
// no lead byte matched, or its continuation bytes weren't 10xxxxxx:
|
|
382
390
|
// pass the single byte through rather than guessing
|
|
383
|
-
out
|
|
391
|
+
out.push(String.fromCharCode(b0));
|
|
384
392
|
}
|
|
385
|
-
return out;
|
|
393
|
+
return out.join("");
|
|
386
394
|
}
|
|
387
395
|
// sproutboat #181 — wrap a Response built from raw wire bytes (the assets
|
|
388
396
|
// binding, outbound fetch(), service-binding fetch(): the three `x-sb-raw-
|
|
@@ -395,12 +403,24 @@ function __sbFromUtf8(bytes) {
|
|
|
395
403
|
// safely can't decode by default). Overriding just these three instances,
|
|
396
404
|
// which sproutboat itself builds and knows are bytes, fixes the read side
|
|
397
405
|
// without touching Porffor's generic Response/Request classes at all.
|
|
406
|
+
// Decoded lazily, on first .text()/.json() call, and memoized after that --
|
|
407
|
+
// most raw-body responses (a static asset, a proxied fetch()) are handed
|
|
408
|
+
// straight back to the client and never read as text at all, and Porffor's
|
|
409
|
+
// strings have no rope/cons optimization: `+=`-building a decoded copy costs
|
|
410
|
+
// real time proportional to the body size (measured ~116ms for a 44KB
|
|
411
|
+
// all-ASCII body). Decoding unconditionally at construction paid that cost
|
|
412
|
+
// on every asset request regardless of whether anything used it -- this was
|
|
413
|
+
// a real, shipped performance regression on sproutboat.com, caught after
|
|
414
|
+
// the fact by unexpectedly slow page loads.
|
|
398
415
|
function __sbRawBodyResponse(body, init) {
|
|
399
416
|
const resp = new Response(body == null ? "" : body, init);
|
|
400
417
|
if (body != null) {
|
|
401
|
-
|
|
402
|
-
resp.text = () =>
|
|
403
|
-
|
|
418
|
+
let decoded = null;
|
|
419
|
+
resp.text = () => {
|
|
420
|
+
if (decoded === null) decoded = __sbFromUtf8(body);
|
|
421
|
+
return decoded;
|
|
422
|
+
};
|
|
423
|
+
resp.json = () => JSON.parse(resp.text());
|
|
404
424
|
}
|
|
405
425
|
return resp;
|
|
406
426
|
}
|
package/src/utf8-decode.test.js
CHANGED
|
@@ -50,3 +50,57 @@ test("empty and pure-ASCII strings are untouched", () => {
|
|
|
50
50
|
expect(__sbFromUtf8("")).toBe("");
|
|
51
51
|
expect(__sbFromUtf8("hello world")).toBe("hello world");
|
|
52
52
|
});
|
|
53
|
+
|
|
54
|
+
// __sbFromUtf8 builds its result via an array + one join(),
|
|
55
|
+
// not repeated `out +=`. Porffor's strings have no rope/cons optimization, so
|
|
56
|
+
// `+=` in a loop copies the whole accumulated string on every append,
|
|
57
|
+
// making a large decode O(n^2) -- measured as a real ~2.6s stall decoding a
|
|
58
|
+
// 43KB page on sproutboat.com, gone after the array+join rewrite. This is a
|
|
59
|
+
// correctness check (long input still round-trips exactly), not a timing
|
|
60
|
+
// assertion -- CI timing thresholds are flaky; the real perf verification
|
|
61
|
+
// happened against a real compiled binary, documented in the fix commit.
|
|
62
|
+
test("a long, multi-chunk body round-trips exactly", () => {
|
|
63
|
+
const chunk = "porffor · src/index.js → 1.25 MB · x86_64-musl ✓ café 🌱 ";
|
|
64
|
+
const text = chunk.repeat(800); // ~46KB, comparable to the real homepage
|
|
65
|
+
expect(__sbFromUtf8(toBytestring(text))).toBe(text);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// __sbRawBodyResponse must decode lazily: eagerly decoding at construction
|
|
69
|
+
// paid the cost of __sbFromUtf8 on every asset/proxied response regardless of
|
|
70
|
+
// whether the handler ever reads it as text, a real perf regression shipped
|
|
71
|
+
// and caught on sproutboat.com (most such responses are handed straight to
|
|
72
|
+
// the client and .text()/.json() are never called).
|
|
73
|
+
const rbrStart = src.indexOf("function __sbRawBodyResponse(");
|
|
74
|
+
const rbrEnd = src.indexOf("// Constant-time compare of two latin1 byte strings.");
|
|
75
|
+
if (rbrStart === -1 || rbrEnd === -1 || rbrEnd < rbrStart) {
|
|
76
|
+
throw new Error("__sbRawBodyResponse block not found in prelude");
|
|
77
|
+
}
|
|
78
|
+
// oxlint-disable-next-line no-function-constructor -- lifting a pure block out of the text-only prelude for test
|
|
79
|
+
const loadRbr = new Function(
|
|
80
|
+
"Response",
|
|
81
|
+
`${src.slice(start, end)}\n${src.slice(rbrStart, rbrEnd)}\nreturn { __sbRawBodyResponse };`,
|
|
82
|
+
);
|
|
83
|
+
// A minimal Response stand-in: only what __sbRawBodyResponse touches (the
|
|
84
|
+
// constructor call and setting .text/.json on the instance), no Porffor
|
|
85
|
+
// bytestring typing involved.
|
|
86
|
+
class FakeResponse {
|
|
87
|
+
constructor(body, init) {
|
|
88
|
+
this.body = body;
|
|
89
|
+
this.status = (init && init.status) || 200;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const { __sbRawBodyResponse } = loadRbr(FakeResponse);
|
|
93
|
+
|
|
94
|
+
test("__sbRawBodyResponse's text()/json() decode correctly, callable more than once", () => {
|
|
95
|
+
const bytes = toBytestring("café · 🌱");
|
|
96
|
+
const resp = __sbRawBodyResponse(bytes, { status: 200 });
|
|
97
|
+
expect(resp.text()).toBe("café · 🌱");
|
|
98
|
+
expect(resp.text()).toBe("café · 🌱"); // memoized decode, not re-run per call
|
|
99
|
+
expect(resp.json).toBeInstanceOf(Function);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("__sbRawBodyResponse with a null body sets no text()/json() override", () => {
|
|
103
|
+
const resp = __sbRawBodyResponse(null, { status: 404 });
|
|
104
|
+
expect(resp.text).toBeUndefined();
|
|
105
|
+
expect(resp.json).toBeUndefined();
|
|
106
|
+
});
|