• Stars
    star
    145
  • Rank 245,007 (Top 5 %)
  • Language
    HTML
  • Created almost 4 years ago
  • Updated 11 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Proposal for resizable array buffers

In-Place Resizable and Growable ArrayBuffers

Stage: 3

Author: Shu-yu Guo (@syg)

Champion: Shu-yu Guo (@syg)

Introduction

ArrayBuffers have enabled in-memory handling of binary data and have enjoyed great success. This proposal extends the ArrayBuffer constructors to take an additional maximum length that allows in-place growth and shrinking of buffers. Similarly, SharedArrayBuffer is extended to take an additional maximum length that allows in-place growth.

Motivation and use cases

Better memory management

Growing a new buffer right now requires allocating a new buffer and copying. Not only is this inefficient, it needlessly fragments the address space on 32-bit systems.

Sync up capability with WebAssembly memory.grow

WebAssembly memory can grow. Every time it does, wasm vends a new ArrayBuffer instance and detaches the old one. Any JS-side "pointers" into wasm memory would need to be updated when a grow happens. This is an open problem and currently requires polling, which is super slow:

// The backing buffer gets detached on every grow in wasm!
let U8 = new Uint8Array(WebAssembly.Memory.buffer);

function derefPointerIntoWasmMemory(idx) {
  // Do we need to re-create U8 because memory grew, causing the old buffer
  // to detach?
  if (U8.length === 0) {
    U8 = new Uint8Array(WebAssembly.Memory.buffer);
  }
  doSomethingWith(U8[idx]);
}

It also spurred proposals such as having a signal handler-like synchronous callback on growth events for wasm's JS API, which doesn't feel great due to the issues of signal handler re-entrancy being difficult to reason about.

Having growable ArrayBuffers and auto-tracking TypedArrays would solve this problem more cleanly.

WebGPU buffers

WebGPU would like to repoint the same ArrayBuffer instances to different backing buffers. This is important for performance during animations, as remaking ArrayBuffer instances multiple times per frame of animation incurs GC pressure and pauses.

Having a resizable ArrayBuffer would let WebGPU explain repointing as a resize + overwrite. Under the hood, browsers can implement WebGPU-vended resizable ArrayBuffers as repointable without actually adding a repointable ArrayBuffer into the language.

Proposal

ArrayBuffer

class ArrayBuffer {
  // If the options parameter is not an object with a "maxByteLength"
  // property, the ArrayBuffer can neither grow nor shrink (status quo).
  // Otherwise it is resizable.
  //
  // A resizable ArrayBuffer can grow up to the provided
  // options.maxByteLength and shrink.
  //
  // If options is an object with a "maxByteLength" property,
  // - Throws a RangeError if maxByteLength is not finite.
  // - Throws a RangeError if byteLength > maxByteLength.
  constructor(byteLength [, options ]);

  // Resizes the buffer.
  //
  // Grows are designed to be implemented in-place, i.e. address space is
  // reserved up front but the pages are not committed until grown.
  //
  // Shrinks are also designed to be in-place, with a length change and
  // no realloc.
  //
  // Throws a TypeError if the this value is not resizable.
  // Throws a RangeError unless 0 <= newByteLength <= this.maxByteLength.
  //
  // Can throw OOM.
  resize(newByteLength);

  // Returns a *non*-resizable ArrayBuffer.
  slice(start, end);

  // Returns true if the `this` value is resizable `ArrayBuffer`,
  // false otherwise.
  //
  // No setter.
  get resizable();

  // If resizable, returns the maximum byte length passed in during construction.
  // If not resizable, returns the byte length.
  //
  // No setter.
  get maxByteLength();

  // No setter.
  get byteLength();
}

SharedArrayBuffer

class SharedArrayBuffer {
  // If the options parameter is not an object with a "maxByteLength"
  // property, the SharedArrayBuffer cannot grow (status quo).
  // Otherwise it is growable.
  //
  // A growable SharedArrayBuffer can only grow up to the provided
  // options.maxByteLength.
  //
  // If options is an object with a "maxByteLength" property,
  // - Throws a RangeError if options.maxByteLength is not finite.
  // - Throws a RangeError if byteLength > options.maxByteLength.
  constructor(byteLength [, options ]);

  // Grows the buffer.
  //
  // Grows are designed to be implemented in-place, i.e. address space is
  // reserved up front but the pages are not committed until grown.
  //
  // Growable SharedArrayBuffers cannot shrink because it is real scary to
  // allow for shared memory.
  //
  // Throws a TypeError if the `this` value is not a growable
  // SharedArrayBuffer.
  // Throws a RangeError unless
  // this.byteLength <= newByteLength <= this.maxByteLength.
  //
  // Can throw OOM.
  grow(newByteLength);

  // Returns a *non*-growable SharedArrayBuffer.
  slice(start, end);

  // Returns true if the `this` value is a growable SharedArrayBuffer,
  // false otherwise.
  //
  // No setter.
  get growable();

  // If resizable, returns the maximum byte length passed in during construction.
  // If not resizable, returns the byte length.
  //
  // No setter.
  get maxByteLength();

  // No setter.
  get byteLength();
}

Modifications to TypedArray

TypedArrays are extended to make use of these buffers. When a TypedArray is backed by a resizable buffer, its byte offset length may automatically change if the backing buffer is resized.

The TypedArray (buffer, [, byteOffset [, length ] ] ) constructor is modified as follows:

  • If buffer is a resizable ArrayBuffer or a growable SharedArrayBuffer, if the length is undefined, then the constructed TA automatically tracks the length of the backing buffer.

The length getter on TypedArray.prototype is modified as follows:

  • If this TA is backed by a resizable ArrayBuffer or growable SharedArrayBuffer and is automatically tracking the length of the backing buffer, then return floor((buffer byte length - byte offset) / element size).
  • If this TA is backed by a resizable ArrayBuffer and the length is out of bounds, then return 0.

All methods and internal methods that access indexed properties on TypedArrays are modified as follow:

  • If this TA is backed by a resizable ArrayBuffer and the translated byte index on the backing buffer is out of bounds, return undefined.
  • If this TA is backed by a resizable ArrayBuffer and the translated byte length is out of bounds, return 0.

This change generalizes the detachment check: if a fixed-length window on a backing buffer becomes out of bounds, either in whole or in part, due to resizing, treat it like a detached buffer.

This generalized bounds check is performed on every index access on TypedArrays backed by resizable ArrayBuffer.

Growable SharedArrayBuffers can only grow, so TAs backed by growable SharedArrayBuffers cannot go out of bounds.

An example:

let rab = new ArrayBuffer(1024, { maxByteLength: 1024 ** 2 });
// 0 offset, auto length
let U32a = new Uint32Array(rab);
assert(U32a.length === 256); // (1024 - 0) / 4
rab.resize(1024 * 2);
assert(U32a.length === 512); // (2048 - 0) / 4

// Non-0 offset, auto length
let U32b = new Uint32Array(rab, 256);
assert(U32b.length === 448); // (2048 - 256) / 4
rab.resize(1024);
assert(U32b.length === 192); // (1024 - 256) / 4

// Non-0 offset, fixed length
let U32c = new Uint32Array(rab, 128, 4);
assert(U32c.length === 4);
rab.resize(1024 * 2);
assert(U32c.length === 4);

// If a resize makes any accessible part of a TA OOB, the TA acts like
// it's been detached.
rab.resize(256);
assertThrows(() => U32b[0]);
assert(U32b.length === 0);
rab.resize(132);
// U32c can address rab[128] to rab[144]. Being partially OOB still makes
// it act like it's been detached.
assertThrows(() => U32c[0]);
assert(U32c.length === 0);
// Resizing the underlying buffer can bring a TA back into bounds.
// New memory is zeroed.
rab.resize(1024);
assert(U32b[0] === 0);
assert(U32b.length === 192);

Implementation

  • Both resizable ArrayBuffer and growable SharedArrayBuffer are designed to be direct buffers where the virtual memory is reserved for the address range but not backed by physical memory until needed.

  • TypedArrays that are backed by resizable and growable buffers have more complex, but similar-in-kind, logic to detachment checks. The performance expectation is that these TypedArrays will be slower than TypedArrays backed by fixed-size buffers. In tight loops, however, this generalized check is hoistable in the same way that the current detach check is hoistable.

  • TypedArrays that are backed by resizable and growable buffers are recommended to have a distinct hidden class from TypedArrays backed by fixed-size buffers for maintainability of security-sensitive fast paths. This unfortunately makes use sites polymorphic. The slowdown from the polymorphism needs to be benchmarked.

Security

ArrayBuffers and TypedArrays are one of the most common attack vectors for web browsers. Resizability adds non-zero security risk to the platform in that bugs in bounds checking code for resizable buffers may be easily exploited.

This security risk is intrinsic to the proposal and is not entirely eliminable. This proposal tries to mitigate with the following design choices:

  • Existing uses of ArrayBuffer and SharedArrayBuffer constructors remain fixed-length and are not retrofitted to be resizable. Internally the resizable buffer types may have different hidden classes so existing code paths can be kept separate
  • Make partially OOB TypedArrays act like their buffers are detached instead of auto-updating the length
  • Make in-place implementation always possible to limit data pointer moves

FAQ and design rationale tradeoffs

What happened to transfer()? It used to be here.

It has been separated into its own proposal to further explore the design space.

Why not retrofit all ArrayBuffers to be resizable?

Retrofitting the single-parameter ArrayBuffer instead of adding an explicit opt-in overload is hard because of both language and implementation concerns:

  1. TypedArray views have particular offsets and lengths that would need to be updated. It is messy to determine what TAs' lengths should be updated. If growing, it seems like user intention needs to be taken into account, and those with explicitly provided lengths should not be updated. If shrinking, it seems like all views need to be updated. This would not only require tracking all created views but is not clean to reason about.
  2. Browsers and VMs have battle-hardened code paths around existing TypedArrays and ArrayBuffers, as they are the most popular way to attack browsers. By introducing new types, we hope to leave those existing paths alone. Otherwise we'd need to audit all existing paths, of which there are many because of web APIs' use of buffers, to ensure they handle the possibility of growth and shrinking. This is scary and is likely a security bug farm.

Why require maximum length?

The API is designed to be implementable as an in-place growth. Non in-place growth (i.e. realloc) semantics presents more challenges for implementation as well as a bigger attack surface. In-place growth has the guarantee that the data pointer of the backing store does not move.

Under the hood, this means the backing store pointer can be made immovable. Note that this immovability of the data pointer is unobservable from within JS. For resizable ArrayBuffers, it would be conformant, but possibly undesirable, to implement growth and shrinking as realloc. For growable SharedArrayBuffers, due to memory model constraints, it is unlikely that a realloc implementation is possible.

Why can't growable SharedArrayBuffer shrink?

Shrinking shared memory is scary and seems like a bad time.

How would growable SharedArrayBuffer growth work with the memory model?

Growing a growable SharedArrayBuffer performs a SeqCst access on the buffer length. Explicit accesses to length, such as to the byteLength accessor, and built-in functions, such as slice, perform a SeqCst access on the buffer length. Bounds checks as part of indexed access, such as via ta[idx] and Atomics.load(ta, idx), perform an Unordered access on the buffer length.

This aligns with WebAssembly as well as enable more optimization opportunities for bounds checking codegen. It also means that other threads are not guaranteed to see the grown length without synchronizing on an explicit length access, such as by reading the byteLength accessor.

Open questions

Should resize(0) be allowed?

Currently a length of 0 always denotes a detached buffer. Are there use cases for resize(0)? Should it mean detach if allowed? Or should the buffer be allowed to grow again afterwards?

#22 points out that ArrayBuffer(0) is already a thing. This proposal thus allows resize(0).

History and acknowledgment

Thanks to:

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,380
star
4

proposal-pattern-matching

Pattern matching syntax for ECMAScript
HTML
5,341
star
5

proposal-optional-chaining

HTML
4,952
star
6

proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
JavaScript
4,090
star
7

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,135
star
8

proposal-observable

Observables for ECMAScript
JavaScript
3,032
star
9

proposal-signals

A proposal to add signals to JavaScript.
TypeScript
2,668
star
10

proposal-decorators

Decorators for ES6 classes
2,640
star
11

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,423
star
12

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
13

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,859
star
14

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,736
star
15

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,720
star
16

proposal-async-await

Async/await for ECMAScript
HTML
1,577
star
17

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,496
star
18

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,365
star
19

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,233
star
20

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,220
star
21

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,082
star
22

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
23

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
24

agendas

TC39 meeting agendas
JavaScript
952
star
25

proposal-binary-ast

Binary AST proposal for ECMAScript
945
star
26

proposal-built-in-modules

HTML
886
star
27

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
854
star
28

proposal-explicit-resource-management

ECMAScript Explicit Resource Management
JavaScript
671
star
29

proposal-operator-overloading

JavaScript
610
star
30

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
588
star
31

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
560
star
32

proposal-set-methods

Proposal for new Set methods in JS
HTML
557
star
33

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
538
star
34

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
536
star
35

proposal-slice-notation

HTML
515
star
36

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
509
star
37

ecma402

Status, process, and documents for ECMA 402
HTML
506
star
38

notes

TC39 meeting notes
JavaScript
496
star
39

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
40

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
464
star
41

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
462
star
42

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
43

proposal-module-expressions

HTML
424
star
44

proposal-UnambiguousJavaScriptGrammar

413
star
45

proposal-decimal

Built-in decimal datatype in JavaScript
HTML
408
star
46

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
47

proposal-async-context

Async Context for JavaScript
HTML
406
star
48

proposal-weakrefs

WeakRefs
HTML
404
star
49

proposal-error-cause

TC39 proposal for accumulating errors
HTML
378
star
50

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
376
star
51

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
52

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
53

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
350
star
54

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
55

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
344
star
56

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
327
star
57

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
320
star
58

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
317
star
59

proposal-module-declarations

JavaScript Module Declarations
HTML
314
star
60

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
61

tc39.github.io

Get involved in specifying JavaScript
HTML
313
star
62

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
63

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
308
star
64

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
65

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
297
star
66

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
278
star
67

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
268
star
68

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
262
star
69

proposal-json-modules

Proposal to import JSON files as modules
HTML
259
star
70

proposal-promise-with-resolvers

HTML
255
star
71

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
254
star
72

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
241
star
73

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
217
star
74

proposal-structs

JavaScript Structs: Fixed Layout Objects
216
star
75

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
76

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
215
star
77

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
78

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
200
star
79

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
198
star
80

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
81

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
82

proposal-defer-import-eval

A proposal for introducing a way to defer evaluate of a module
HTML
174
star
83

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
84

proposal-optional-chaining-assignment

`a?.b = c` proposal
168
star
85

proposal-array-from-async

Draft specification for a proposed Array.fromAsync method in JavaScript.
HTML
167
star
86

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
87

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
165
star
88

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
165
star
89

how-we-work

Documentation of how TC39 operates and how to participate
161
star
90

proposal-collection-methods

HTML
160
star
91

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
92

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
156
star
93

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
94

proposal-hashbang

#! for JS
HTML
148
star
95

proposal-import-meta

import.meta proposal for JavaScript
HTML
145
star
96

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
145
star
97

proposal-extensions

Extensions proposal for ECMAScript
HTML
143
star
98

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
99

proposal-intl-duration-format

141
star
100

proposal-regexp-unicode-property-escapes

Proposal to add Unicode property escapes `\p{…}` and `\P{…}` to regular expressions in ECMAScript.
HTML
134
star