Tests: Test JS typed array view cache edge cases
Add direct indexed access coverage for fixed ArrayBuffer typed array views that can use cached backing-store pointers. Cover multiple views on the same buffer, offset views, detach, transfer, truncating transfer, transferToFixedLength(), slicing, and fixed SharedArrayBuffer views. Also cover WebAssembly.Memory buffers across fixed, resizable, shared, and non-shared cases. Exercise memory.grow(), ArrayBuffer resize, SharedArrayBuffer grow, and conversions between resizable and fixed-length memory buffers.
This commit is contained in:
parent
1ab23e67e9
commit
ec0833764b
3 changed files with 446 additions and 0 deletions
|
|
@ -0,0 +1,213 @@
|
|||
const NUMERIC_TYPED_ARRAYS = [
|
||||
Uint8Array,
|
||||
Uint8ClampedArray,
|
||||
Uint16Array,
|
||||
Uint32Array,
|
||||
Int8Array,
|
||||
Int16Array,
|
||||
Int32Array,
|
||||
Float16Array,
|
||||
Float32Array,
|
||||
Float64Array,
|
||||
];
|
||||
|
||||
const BIGINT_TYPED_ARRAYS = [BigUint64Array, BigInt64Array];
|
||||
|
||||
function initialValueFor(T) {
|
||||
if (T === Float16Array || T === Float32Array || T === Float64Array) return 1.5;
|
||||
if (T === BigUint64Array || T === BigInt64Array) return 17n;
|
||||
return 17;
|
||||
}
|
||||
|
||||
function secondValueFor(T) {
|
||||
if (T === Float16Array || T === Float32Array || T === Float64Array) return 2.5;
|
||||
if (T === BigUint64Array || T === BigInt64Array) return 23n;
|
||||
return 23;
|
||||
}
|
||||
|
||||
function replacementValueFor(T) {
|
||||
if (T === Float16Array || T === Float32Array || T === Float64Array) return 3.5;
|
||||
if (T === BigUint64Array || T === BigInt64Array) return 31n;
|
||||
return 31;
|
||||
}
|
||||
|
||||
function testTypes(callback) {
|
||||
NUMERIC_TYPED_ARRAYS.forEach(callback);
|
||||
BIGINT_TYPED_ARRAYS.forEach(callback);
|
||||
}
|
||||
|
||||
test("direct indexed access stays coherent for multiple fixed views over one buffer", () => {
|
||||
testTypes(T => {
|
||||
const buffer = new ArrayBuffer(T.BYTES_PER_ELEMENT * 4);
|
||||
const wholeView = new T(buffer);
|
||||
const offsetView = new T(buffer, T.BYTES_PER_ELEMENT, 2);
|
||||
const initialValue = initialValueFor(T);
|
||||
const secondValue = secondValueFor(T);
|
||||
|
||||
wholeView[1] = initialValue;
|
||||
expect(offsetView[0]).toBe(initialValue);
|
||||
expect(Reflect.get(offsetView, 0)).toBe(initialValue);
|
||||
|
||||
offsetView[1] = secondValue;
|
||||
expect(wholeView[2]).toBe(secondValue);
|
||||
expect(Reflect.get(wholeView, 2)).toBe(secondValue);
|
||||
});
|
||||
});
|
||||
|
||||
test("detach invalidates every cached fixed view over the buffer", () => {
|
||||
testTypes(T => {
|
||||
const buffer = new ArrayBuffer(T.BYTES_PER_ELEMENT * 4);
|
||||
const wholeView = new T(buffer);
|
||||
const offsetView = new T(buffer, T.BYTES_PER_ELEMENT, 2);
|
||||
const initialValue = initialValueFor(T);
|
||||
const secondValue = secondValueFor(T);
|
||||
|
||||
wholeView[0] = initialValue;
|
||||
offsetView[0] = secondValue;
|
||||
expect(wholeView[1]).toBe(secondValue);
|
||||
|
||||
detachArrayBuffer(buffer);
|
||||
|
||||
expect(buffer.detached).toBeTrue();
|
||||
expect(wholeView.length).toBe(0);
|
||||
expect(offsetView.length).toBe(0);
|
||||
expect(wholeView[0]).toBeUndefined();
|
||||
expect(offsetView[0]).toBeUndefined();
|
||||
expect(Reflect.get(wholeView, 0)).toBeUndefined();
|
||||
expect(Reflect.get(offsetView, 0)).toBeUndefined();
|
||||
|
||||
wholeView[0] = replacementValueFor(T);
|
||||
offsetView[0] = replacementValueFor(T);
|
||||
|
||||
expect(wholeView[0]).toBeUndefined();
|
||||
expect(offsetView[0]).toBeUndefined();
|
||||
expect(Object.hasOwn(wholeView, "0")).toBeFalse();
|
||||
expect(Object.hasOwn(offsetView, "0")).toBeFalse();
|
||||
expect(Object.getOwnPropertyDescriptor(wholeView, "0")).toBeUndefined();
|
||||
expect(Object.getOwnPropertyDescriptor(offsetView, "0")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("ArrayBuffer.prototype.transfer invalidates old cached views", () => {
|
||||
testTypes(T => {
|
||||
const buffer = new ArrayBuffer(T.BYTES_PER_ELEMENT * 3);
|
||||
const oldWholeView = new T(buffer);
|
||||
const oldOffsetView = new T(buffer, T.BYTES_PER_ELEMENT, 1);
|
||||
const initialValue = initialValueFor(T);
|
||||
const secondValue = secondValueFor(T);
|
||||
const replacementValue = replacementValueFor(T);
|
||||
|
||||
oldWholeView[0] = initialValue;
|
||||
oldOffsetView[0] = secondValue;
|
||||
|
||||
const newBuffer = buffer.transfer();
|
||||
const newView = new T(newBuffer);
|
||||
|
||||
expect(buffer.detached).toBeTrue();
|
||||
expect(newBuffer.detached).toBeFalse();
|
||||
expect(oldWholeView.length).toBe(0);
|
||||
expect(oldOffsetView.length).toBe(0);
|
||||
expect(oldWholeView[0]).toBeUndefined();
|
||||
expect(oldOffsetView[0]).toBeUndefined();
|
||||
expect(newView[0]).toBe(initialValue);
|
||||
expect(newView[1]).toBe(secondValue);
|
||||
|
||||
oldWholeView[0] = replacementValue;
|
||||
oldOffsetView[0] = replacementValue;
|
||||
|
||||
expect(newView[0]).toBe(initialValue);
|
||||
expect(newView[1]).toBe(secondValue);
|
||||
});
|
||||
});
|
||||
|
||||
test("ArrayBuffer.prototype.transfer truncation invalidates offset cached views", () => {
|
||||
testTypes(T => {
|
||||
const buffer = new ArrayBuffer(T.BYTES_PER_ELEMENT * 4);
|
||||
const oldOffsetView = new T(buffer, T.BYTES_PER_ELEMENT * 2, 2);
|
||||
const initialValue = initialValueFor(T);
|
||||
const secondValue = secondValueFor(T);
|
||||
|
||||
oldOffsetView[0] = initialValue;
|
||||
oldOffsetView[1] = secondValue;
|
||||
|
||||
const newBuffer = buffer.transfer(T.BYTES_PER_ELEMENT * 3);
|
||||
const newView = new T(newBuffer);
|
||||
|
||||
expect(buffer.detached).toBeTrue();
|
||||
expect(oldOffsetView.length).toBe(0);
|
||||
expect(oldOffsetView[0]).toBeUndefined();
|
||||
expect(newView.length).toBe(3);
|
||||
expect(newView[2]).toBe(initialValue);
|
||||
});
|
||||
});
|
||||
|
||||
test("ArrayBuffer.prototype.transferToFixedLength invalidates old cached views", () => {
|
||||
testTypes(T => {
|
||||
const buffer = new ArrayBuffer(T.BYTES_PER_ELEMENT * 3);
|
||||
const oldView = new T(buffer);
|
||||
const initialValue = initialValueFor(T);
|
||||
|
||||
oldView[2] = initialValue;
|
||||
|
||||
const newBuffer = buffer.transferToFixedLength();
|
||||
const newView = new T(newBuffer);
|
||||
|
||||
expect(buffer.detached).toBeTrue();
|
||||
expect(newBuffer.detached).toBeFalse();
|
||||
expect(oldView.length).toBe(0);
|
||||
expect(oldView[2]).toBeUndefined();
|
||||
expect(newView.length).toBe(3);
|
||||
expect(newView[2]).toBe(initialValue);
|
||||
|
||||
detachArrayBuffer(newBuffer);
|
||||
|
||||
expect(newBuffer.detached).toBeTrue();
|
||||
expect(newView.length).toBe(0);
|
||||
expect(newView[0]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
test("ArrayBuffer.prototype.slice creates an independently cached fixed buffer", () => {
|
||||
testTypes(T => {
|
||||
const buffer = new ArrayBuffer(T.BYTES_PER_ELEMENT * 4);
|
||||
const originalView = new T(buffer);
|
||||
const initialValue = initialValueFor(T);
|
||||
const secondValue = secondValueFor(T);
|
||||
|
||||
originalView[1] = initialValue;
|
||||
originalView[2] = secondValue;
|
||||
|
||||
const slicedBuffer = buffer.slice(T.BYTES_PER_ELEMENT, T.BYTES_PER_ELEMENT * 3);
|
||||
const slicedView = new T(slicedBuffer);
|
||||
|
||||
expect(slicedView.length).toBe(2);
|
||||
expect(slicedView[0]).toBe(initialValue);
|
||||
expect(slicedView[1]).toBe(secondValue);
|
||||
|
||||
detachArrayBuffer(buffer);
|
||||
|
||||
expect(buffer.detached).toBeTrue();
|
||||
expect(originalView.length).toBe(0);
|
||||
expect(originalView[1]).toBeUndefined();
|
||||
expect(slicedBuffer.detached).toBeFalse();
|
||||
expect(slicedView[0]).toBe(initialValue);
|
||||
expect(slicedView[1]).toBe(secondValue);
|
||||
});
|
||||
});
|
||||
|
||||
test("fixed SharedArrayBuffer cached views stay coherent", () => {
|
||||
NUMERIC_TYPED_ARRAYS.forEach(T => {
|
||||
const buffer = new SharedArrayBuffer(T.BYTES_PER_ELEMENT * 4);
|
||||
const wholeView = new T(buffer);
|
||||
const offsetView = new T(buffer, T.BYTES_PER_ELEMENT, 2);
|
||||
const initialValue = initialValueFor(T);
|
||||
const secondValue = secondValueFor(T);
|
||||
|
||||
wholeView[1] = initialValue;
|
||||
expect(offsetView[0]).toBe(initialValue);
|
||||
|
||||
offsetView[1] = secondValue;
|
||||
expect(wholeView[2]).toBe(secondValue);
|
||||
expect(Reflect.get(wholeView, 2)).toBe(secondValue);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
non-shared fixed memory grow detaches old views
|
||||
oldBuffer.detached: true
|
||||
oldView.length: 0
|
||||
oldView[0]: undefined
|
||||
newView[0]: 0x11
|
||||
newView[PAGE_SIZE - 1]: 0x22
|
||||
newView[PAGE_SIZE]: 0x0
|
||||
non-shared resizable memory grow keeps views coherent
|
||||
buffer.detached: false
|
||||
buffer.byteLength: 131072
|
||||
trackingView.length: 131072
|
||||
fixedEndView.length: 4
|
||||
trackingView[0]: 0x51
|
||||
trackingView[PAGE_SIZE - 1]: 0x52
|
||||
trackingView[PAGE_SIZE]: 0x53
|
||||
freshView[PAGE_SIZE]: 0x53
|
||||
non-shared resizable buffer.resize keeps views coherent
|
||||
buffer.detached: false
|
||||
buffer.byteLength: 131072
|
||||
trackingView.length: 131072
|
||||
fixedEndView.length: 2
|
||||
trackingView[1]: 0x54
|
||||
trackingView[PAGE_SIZE - 1]: 0x55
|
||||
trackingView[PAGE_SIZE + 1]: 0x56
|
||||
freshView[PAGE_SIZE + 1]: 0x56
|
||||
non-shared resizable to fixed conversion detaches old resizable views
|
||||
resizableBuffer.detached: true
|
||||
trackingView.length: 0
|
||||
fixedBuffer.detached: true
|
||||
fixedView.length: 0
|
||||
newView[0]: 0x61
|
||||
shared fixed memory grow keeps old in-bounds views coherent
|
||||
firstBuffer.detached: undefined
|
||||
firstBuffer.byteLength: 65536
|
||||
secondBuffer.byteLength: 131072
|
||||
firstView.length: 65536
|
||||
firstView[0]: 0x73
|
||||
firstView[PAGE_SIZE - 1]: 0x72
|
||||
firstView[PAGE_SIZE]: undefined
|
||||
secondView[0]: 0x73
|
||||
secondView[PAGE_SIZE]: 0x74
|
||||
shared resizable memory grow keeps tracking views coherent
|
||||
buffer.detached: undefined
|
||||
buffer.byteLength: 131072
|
||||
trackingView.length: 131072
|
||||
fixedEndView.length: 4
|
||||
trackingView[0]: 0x81
|
||||
trackingView[PAGE_SIZE - 1]: 0x82
|
||||
trackingView[PAGE_SIZE]: 0x83
|
||||
freshView[PAGE_SIZE]: 0x83
|
||||
shared resizable buffer.grow keeps views coherent
|
||||
buffer.detached: undefined
|
||||
buffer.byteLength: 131072
|
||||
trackingView.length: 131072
|
||||
fixedEndView.length: 2
|
||||
trackingView[2]: 0x84
|
||||
trackingView[PAGE_SIZE - 1]: 0x85
|
||||
trackingView[PAGE_SIZE + 2]: 0x86
|
||||
freshView[PAGE_SIZE + 2]: 0x86
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
<!doctype html>
|
||||
<script src="include.js"></script>
|
||||
<script>
|
||||
const PAGE_SIZE = 65536;
|
||||
|
||||
function hex(value) {
|
||||
return value === undefined ? "undefined" : `0x${value.toString(16)}`;
|
||||
}
|
||||
|
||||
function touch(view, index, value) {
|
||||
for (let i = 0; i < 16; ++i) view[index] = value;
|
||||
return view[index];
|
||||
}
|
||||
|
||||
test(() => {
|
||||
println("non-shared fixed memory grow detaches old views");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3 });
|
||||
const oldBuffer = memory.buffer;
|
||||
const oldView = new Uint8Array(oldBuffer);
|
||||
|
||||
touch(oldView, 0, 0x11);
|
||||
touch(oldView, PAGE_SIZE - 1, 0x22);
|
||||
memory.grow(1);
|
||||
|
||||
const newView = new Uint8Array(memory.buffer);
|
||||
oldView[0] = 0x33;
|
||||
|
||||
println(`oldBuffer.detached: ${oldBuffer.detached}`);
|
||||
println(`oldView.length: ${oldView.length}`);
|
||||
println(`oldView[0]: ${hex(oldView[0])}`);
|
||||
println(`newView[0]: ${hex(newView[0])}`);
|
||||
println(`newView[PAGE_SIZE - 1]: ${hex(newView[PAGE_SIZE - 1])}`);
|
||||
println(`newView[PAGE_SIZE]: ${hex(newView[PAGE_SIZE])}`);
|
||||
}
|
||||
|
||||
println("non-shared resizable memory grow keeps views coherent");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3 });
|
||||
const buffer = memory.toResizableBuffer();
|
||||
const trackingView = new Uint8Array(buffer);
|
||||
const fixedEndView = new Uint8Array(buffer, PAGE_SIZE - 4, 4);
|
||||
|
||||
touch(trackingView, 0, 0x51);
|
||||
touch(fixedEndView, 3, 0x52);
|
||||
memory.grow(1);
|
||||
touch(trackingView, PAGE_SIZE, 0x53);
|
||||
|
||||
const freshView = new Uint8Array(memory.buffer);
|
||||
println(`buffer.detached: ${buffer.detached}`);
|
||||
println(`buffer.byteLength: ${buffer.byteLength}`);
|
||||
println(`trackingView.length: ${trackingView.length}`);
|
||||
println(`fixedEndView.length: ${fixedEndView.length}`);
|
||||
println(`trackingView[0]: ${hex(trackingView[0])}`);
|
||||
println(`trackingView[PAGE_SIZE - 1]: ${hex(trackingView[PAGE_SIZE - 1])}`);
|
||||
println(`trackingView[PAGE_SIZE]: ${hex(trackingView[PAGE_SIZE])}`);
|
||||
println(`freshView[PAGE_SIZE]: ${hex(freshView[PAGE_SIZE])}`);
|
||||
}
|
||||
|
||||
println("non-shared resizable buffer.resize keeps views coherent");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3 });
|
||||
const buffer = memory.toResizableBuffer();
|
||||
const trackingView = new Uint8Array(buffer);
|
||||
const fixedEndView = new Uint8Array(buffer, PAGE_SIZE - 2, 2);
|
||||
|
||||
touch(trackingView, 1, 0x54);
|
||||
touch(fixedEndView, 1, 0x55);
|
||||
buffer.resize(2 * PAGE_SIZE);
|
||||
touch(trackingView, PAGE_SIZE + 1, 0x56);
|
||||
|
||||
const freshView = new Uint8Array(memory.buffer);
|
||||
println(`buffer.detached: ${buffer.detached}`);
|
||||
println(`buffer.byteLength: ${buffer.byteLength}`);
|
||||
println(`trackingView.length: ${trackingView.length}`);
|
||||
println(`fixedEndView.length: ${fixedEndView.length}`);
|
||||
println(`trackingView[1]: ${hex(trackingView[1])}`);
|
||||
println(`trackingView[PAGE_SIZE - 1]: ${hex(trackingView[PAGE_SIZE - 1])}`);
|
||||
println(`trackingView[PAGE_SIZE + 1]: ${hex(trackingView[PAGE_SIZE + 1])}`);
|
||||
println(`freshView[PAGE_SIZE + 1]: ${hex(freshView[PAGE_SIZE + 1])}`);
|
||||
}
|
||||
|
||||
println("non-shared resizable to fixed conversion detaches old resizable views");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3 });
|
||||
const resizableBuffer = memory.toResizableBuffer();
|
||||
const trackingView = new Uint8Array(resizableBuffer);
|
||||
|
||||
touch(trackingView, 0, 0x61);
|
||||
const fixedBuffer = memory.toFixedLengthBuffer();
|
||||
const fixedView = new Uint8Array(fixedBuffer);
|
||||
memory.grow(1);
|
||||
|
||||
const newView = new Uint8Array(memory.buffer);
|
||||
println(`resizableBuffer.detached: ${resizableBuffer.detached}`);
|
||||
println(`trackingView.length: ${trackingView.length}`);
|
||||
println(`fixedBuffer.detached: ${fixedBuffer.detached}`);
|
||||
println(`fixedView.length: ${fixedView.length}`);
|
||||
println(`newView[0]: ${hex(newView[0])}`);
|
||||
}
|
||||
|
||||
println("shared fixed memory grow keeps old in-bounds views coherent");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3, shared: true });
|
||||
const firstBuffer = memory.buffer;
|
||||
const firstView = new Uint8Array(firstBuffer);
|
||||
|
||||
touch(firstView, 0, 0x71);
|
||||
touch(firstView, PAGE_SIZE - 1, 0x72);
|
||||
memory.grow(1);
|
||||
|
||||
const secondBuffer = memory.buffer;
|
||||
const secondView = new Uint8Array(secondBuffer);
|
||||
touch(secondView, 0, 0x73);
|
||||
touch(secondView, PAGE_SIZE, 0x74);
|
||||
|
||||
println(`firstBuffer.detached: ${firstBuffer.detached}`);
|
||||
println(`firstBuffer.byteLength: ${firstBuffer.byteLength}`);
|
||||
println(`secondBuffer.byteLength: ${secondBuffer.byteLength}`);
|
||||
println(`firstView.length: ${firstView.length}`);
|
||||
println(`firstView[0]: ${hex(firstView[0])}`);
|
||||
println(`firstView[PAGE_SIZE - 1]: ${hex(firstView[PAGE_SIZE - 1])}`);
|
||||
println(`firstView[PAGE_SIZE]: ${hex(firstView[PAGE_SIZE])}`);
|
||||
println(`secondView[0]: ${hex(secondView[0])}`);
|
||||
println(`secondView[PAGE_SIZE]: ${hex(secondView[PAGE_SIZE])}`);
|
||||
}
|
||||
|
||||
println("shared resizable memory grow keeps tracking views coherent");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3, shared: true });
|
||||
const buffer = memory.toResizableBuffer();
|
||||
const trackingView = new Uint8Array(buffer);
|
||||
const fixedEndView = new Uint8Array(buffer, PAGE_SIZE - 4, 4);
|
||||
|
||||
touch(trackingView, 0, 0x81);
|
||||
touch(fixedEndView, 3, 0x82);
|
||||
memory.grow(1);
|
||||
touch(trackingView, PAGE_SIZE, 0x83);
|
||||
|
||||
const freshView = new Uint8Array(memory.buffer);
|
||||
println(`buffer.detached: ${buffer.detached}`);
|
||||
println(`buffer.byteLength: ${buffer.byteLength}`);
|
||||
println(`trackingView.length: ${trackingView.length}`);
|
||||
println(`fixedEndView.length: ${fixedEndView.length}`);
|
||||
println(`trackingView[0]: ${hex(trackingView[0])}`);
|
||||
println(`trackingView[PAGE_SIZE - 1]: ${hex(trackingView[PAGE_SIZE - 1])}`);
|
||||
println(`trackingView[PAGE_SIZE]: ${hex(trackingView[PAGE_SIZE])}`);
|
||||
println(`freshView[PAGE_SIZE]: ${hex(freshView[PAGE_SIZE])}`);
|
||||
}
|
||||
|
||||
println("shared resizable buffer.grow keeps views coherent");
|
||||
{
|
||||
const memory = new WebAssembly.Memory({ initial: 1, maximum: 3, shared: true });
|
||||
const buffer = memory.toResizableBuffer();
|
||||
const trackingView = new Uint8Array(buffer);
|
||||
const fixedEndView = new Uint8Array(buffer, PAGE_SIZE - 2, 2);
|
||||
|
||||
touch(trackingView, 2, 0x84);
|
||||
touch(fixedEndView, 1, 0x85);
|
||||
buffer.grow(2 * PAGE_SIZE);
|
||||
touch(trackingView, PAGE_SIZE + 2, 0x86);
|
||||
|
||||
const freshView = new Uint8Array(memory.buffer);
|
||||
println(`buffer.detached: ${buffer.detached}`);
|
||||
println(`buffer.byteLength: ${buffer.byteLength}`);
|
||||
println(`trackingView.length: ${trackingView.length}`);
|
||||
println(`fixedEndView.length: ${fixedEndView.length}`);
|
||||
println(`trackingView[2]: ${hex(trackingView[2])}`);
|
||||
println(`trackingView[PAGE_SIZE - 1]: ${hex(trackingView[PAGE_SIZE - 1])}`);
|
||||
println(`trackingView[PAGE_SIZE + 2]: ${hex(trackingView[PAGE_SIZE + 2])}`);
|
||||
println(`freshView[PAGE_SIZE + 2]: ${hex(freshView[PAGE_SIZE + 2])}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
Loading…
Reference in a new issue