Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/validation/file-reader-single-task-20260921.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Single-task FileReader completion — 21 September 2026

## Product boundary

The exact unchanged Code OSS package at WebScene `62c4c617`, AppScene
`cd0a02ec`, and Code OSS `645f29cc` resolves the selected remote root in
1,219.824 ms, but paints its first Explorer child in 2,530.115 ms. The complete
functional oracle passes: exact selected root, registered provider, five
resolved children, five Explorer model children, five visible rows, and 64
Explorer nodes.

The remaining trace records 90 FileReader operations over 11,833,296 bytes.
Native `Blob.arrayBuffer()` work takes at most 0.0393 ms, while FileReader
call-to-`loadend` takes up to 1,114.299 ms. The compatibility implementation
scheduled one file-reading task for `loadstart`, then a second task for
`progress`, `load`, and `loadend`. Both tasks closed independent
microtask/style batches.

## Focused change

One asynchronous file-reading task now owns `loadstart`, the Blob promise
continuation, and the ordered terminal events. The promise continuation runs at
the browser task's required microtask checkpoint, before the task closes its
style batch. This preserves asynchronous observation, event order, cancellation,
errors, immediate chained reads, and the bounded dedicated FileReader queue,
while removing one task and one style boundary per protocol frame.

## Focused gates

- Node 24.18.1 FileReader compatibility: 8/8 pass.
- Native WebSocket/FileReader gate: 100 socket cycles and 20 product-shaped
protocol cycles pass.
- Product-shaped native FileReader p95: 0.0253 ms.
- 64-timer backlog: one timer runs before FileReader; completion takes 0.314 ms.
- Native protocol gate reports stable settled V8 heap and bounded queue high
water marks of one.

The exact installed SDK/package rerun remains the promotion gate for the
two-second Explorer first-paint budget in WebScene #280 and #252.
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,19 @@ inline constexpr std::string_view fileReaderCompatibilityScript = R"JS(
result = decoder.decode(bytes);
} else if (format === 'data') result = 'data:' + (blob.type || 'application/octet-stream') + ';base64,' + base64(bytes);
else if (format === 'binary') result = bytesToBinary(bytes);
schedule(() => {
if (!active()) return;
this._emit('progress', bytes.byteLength, blob.size);
if (!active()) return;
this._result = result; this._state = 2;
this._emit('load', bytes.byteLength, blob.size);
if (operation === this._operation && this._state === 2) this._emit('loadend', bytes.byteLength, blob.size);
});
}).catch(error => schedule(() => {
if (!active()) return;
this._emit('progress', bytes.byteLength, blob.size);
if (!active()) return;
this._result = result; this._state = 2;
this._emit('load', bytes.byteLength, blob.size);
if (operation === this._operation && this._state === 2) this._emit('loadend', bytes.byteLength, blob.size);
}).catch(error => {
if (!active()) return;
this._error = error instanceof DOMException ? error : failure(String(error?.message || error), 'NotReadableError');
this._state = 2; this._result = null;
this._emit('error', 0, blob.size);
if (operation === this._operation && this._state === 2) this._emit('loadend', 0, blob.size);
}));
});
});
}
readAsArrayBuffer(blob) { this._read(blob, 'array'); }
Expand Down
7 changes: 3 additions & 4 deletions tests/RuntimeCompatibility/file-reader.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ test('actual Blob bytes are read asynchronously with one ordered event delivery'
assert.equal(reader.error, null);
});

test('native file-reading task source is used ahead of the timer fallback', async () => {
test('one native file-reading task owns the complete ordered read', async () => {
const scheduled = [];
const FileReader = setup({
__webSceneQueueFileReadingTask(callback) {
Expand All @@ -80,12 +80,11 @@ test('native file-reading task source is used ahead of the timer fallback', asyn
reader.readAsArrayBuffer(new Blob([Uint8Array.of(7, 8, 9)]));
assert.equal(scheduled.length, 1);
scheduled.shift()();
for (let attempt = 0; attempt < 8 && scheduled.length === 0; attempt++) {
for (let attempt = 0; attempt < 8 && events.at(-1) !== 'loadend'; attempt++) {
await Promise.resolve();
}
assert.equal(scheduled.length, 1);
scheduled.shift()();
await loaded;
assert.equal(scheduled.length, 0);
assert.deepEqual(events, ['loadstart', 'progress', 'load', 'loadend']);
assert.deepEqual(Array.from(new Uint8Array(reader.result)), [7, 8, 9]);
});
Expand Down
Loading