mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
fix(durable): encode base64 with btoa, without retaining concatenation trees
toBase64 grew each 8 KiB chunk a character at a time; V8 kept every concatenation node until the final join, about 138 MB of heap for a 3 MiB image, beyond a Durable Object's 128 MB limit. closes #10923
This commit is contained in:
parent
7efebff189
commit
b042997178
3 changed files with 28 additions and 20 deletions
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
|
||||
- `toBase64`, used by `read` and the Photon image processor, no longer takes about 140 MB of heap to encode a 3 MiB image, which exceeded the 128 MB memory limit of a Cloudflare Durable Object; it now builds a binary string 8 KiB at a time and encodes it with `btoa` ([#10923](https://github.com/earendil-works/pi/issues/10923)).
|
||||
|
||||
## [1.2.0] - 2026-10-12
|
||||
|
||||
### Breaking Changes
|
||||
|
|
|
|||
|
|
@ -39,28 +39,17 @@ export type ImageProcessor = {
|
|||
/** Formats every provider takes inline; others, such as BMP, need converting. */
|
||||
export const INLINE_IMAGE_TYPES: ReadonlySet<string> = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
|
||||
|
||||
const BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
|
||||
/** Standard base64 of `bytes`, without platform APIs such as `Buffer` or `btoa`. */
|
||||
/**
|
||||
* Standard base64 of `bytes`. `btoa` takes a binary string, built 8 KiB at a time: a single `String.fromCharCode`
|
||||
* call with megabytes of arguments overflows the stack.
|
||||
*/
|
||||
export function toBase64(bytes: Uint8Array): string {
|
||||
const parts: string[] = [];
|
||||
let chunk = "";
|
||||
for (let i = 0; i < bytes.length; i += 3) {
|
||||
const a = bytes[i]!;
|
||||
const b = bytes[i + 1];
|
||||
const c = bytes[i + 2];
|
||||
chunk += BASE64[a >> 2]!;
|
||||
chunk += BASE64[((a & 3) << 4) | ((b ?? 0) >> 4)]!;
|
||||
chunk += b === undefined ? "=" : BASE64[((b & 15) << 2) | ((c ?? 0) >> 6)]!;
|
||||
chunk += c === undefined ? "=" : BASE64[c & 63]!;
|
||||
// Join in pieces: one string grown a character at a time is slow for megabytes.
|
||||
if (chunk.length >= 8192) {
|
||||
parts.push(chunk);
|
||||
chunk = "";
|
||||
}
|
||||
for (let i = 0; i < bytes.length; i += 8192) {
|
||||
// `apply` takes any array-like, though its type asks for an array.
|
||||
parts.push(String.fromCharCode.apply(null, bytes.subarray(i, i + 8192) as unknown as number[]));
|
||||
}
|
||||
parts.push(chunk);
|
||||
return parts.join("");
|
||||
return btoa(parts.join(""));
|
||||
}
|
||||
|
||||
/** The length of the base64 of `byteLength` bytes. */
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import { tmpdir } from "node:os";
|
||||
|
|
@ -486,11 +487,25 @@ describe("imageDimensions", () => {
|
|||
|
||||
describe("toBase64", () => {
|
||||
it("matches Buffer for every remainder length", () => {
|
||||
for (const length of [0, 1, 2, 3, 4, 5, 10_000]) {
|
||||
for (const length of [0, 1, 2, 3, 4, 5, 8191, 8192, 8193, 10_000, 16_385]) {
|
||||
const bytes = Uint8Array.from({ length }, (_, index) => (index * 37) % 256);
|
||||
expect(toBase64(bytes)).toBe(Buffer.from(bytes).toString("base64"));
|
||||
}
|
||||
});
|
||||
|
||||
// #10923: building the output a character at a time took 138 MB of heap for a 3 MiB image.
|
||||
it("encodes a 3 MiB image within a 128 MB heap", () => {
|
||||
const source = new URL("../src/tools/image-processor.ts", import.meta.url).href;
|
||||
const script = `
|
||||
const { toBase64 } = await import(${JSON.stringify(source)});
|
||||
const bytes = Uint8Array.from({ length: 3147295 }, (_, index) => (index * 37) % 256);
|
||||
if (toBase64(bytes) !== Buffer.from(bytes).toString("base64")) throw new Error("mismatch");
|
||||
`;
|
||||
const args = ["--experimental-strip-types", "--max-heap-size=128", "--input-type=module", "-e", script];
|
||||
const result = spawnSync(process.execPath, args, { encoding: "utf8" });
|
||||
expect(result.stderr).not.toMatch(/heap out of memory|mismatch/);
|
||||
expect(result.status).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createNodePhotonImages", () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue