summaryrefslogtreecommitdiffstats
path: root/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts
diff options
context:
space:
mode:
Diffstat (limited to 'dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts')
-rw-r--r--dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts178
1 files changed, 178 insertions, 0 deletions
diff --git a/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts b/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts
new file mode 100644
index 0000000000..d14486ecee
--- /dev/null
+++ b/dom/webgpu/tests/cts/checkout/src/stress/memory/oom.spec.ts
@@ -0,0 +1,178 @@
+export const description = `
+Stress tests covering robustness when available VRAM is exhausted.
+`;
+
+import { makeTestGroup } from '../../common/framework/test_group.js';
+import { unreachable } from '../../common/util/util.js';
+import { GPUConst } from '../../webgpu/constants.js';
+import { GPUTest } from '../../webgpu/gpu_test.js';
+import { exhaustVramUntilUnder64MB } from '../../webgpu/util/memory.js';
+
+export const g = makeTestGroup(GPUTest);
+
+function createBufferWithMapState(
+ device: GPUDevice,
+ size: number,
+ mapState: GPUBufferMapState,
+ mode: GPUMapModeFlags,
+ mappedAtCreation: boolean
+) {
+ const mappable = mapState === 'unmapped';
+ if (!mappable && !mappedAtCreation) {
+ return device.createBuffer({
+ size,
+ usage: GPUBufferUsage.UNIFORM,
+ mappedAtCreation,
+ });
+ }
+ let buffer: GPUBuffer;
+ switch (mode) {
+ case GPUMapMode.READ:
+ buffer = device.createBuffer({
+ size,
+ usage: GPUBufferUsage.MAP_READ,
+ mappedAtCreation,
+ });
+ break;
+ case GPUMapMode.WRITE:
+ buffer = device.createBuffer({
+ size,
+ usage: GPUBufferUsage.MAP_WRITE,
+ mappedAtCreation,
+ });
+ break;
+ default:
+ unreachable();
+ }
+ // If we want the buffer to be mappable and also mappedAtCreation, we call unmap on it now.
+ if (mappable && mappedAtCreation) {
+ buffer.unmap();
+ }
+ return buffer;
+}
+
+g.test('vram_oom')
+ .desc(`Tests that we can allocate buffers until we run out of VRAM.`)
+ .fn(async t => {
+ await exhaustVramUntilUnder64MB(t.device);
+ });
+
+g.test('map_after_vram_oom')
+ .desc(
+ `Allocates tons of buffers and textures with varying mapping states (unmappable,
+mappable, mapAtCreation, mapAtCreation-then-unmapped) until OOM; then attempts
+to mapAsync all the mappable objects. The last buffer should be an error buffer so
+mapAsync on it should reject and produce a validation error. `
+ )
+ .params(u =>
+ u
+ .combine('mapState', ['mapped', 'unmapped'] as GPUBufferMapState[])
+ .combine('mode', [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE])
+ .combine('mappedAtCreation', [true, false])
+ .combine('unmapBeforeResolve', [true, false])
+ )
+ .fn(async t => {
+ // Use a relatively large size to quickly hit OOM.
+ const kSize = 512 * 1024 * 1024;
+
+ const { mapState, mode, mappedAtCreation, unmapBeforeResolve } = t.params;
+ const mappable = mapState === 'unmapped';
+ const buffers: GPUBuffer[] = [];
+ // Closure to call map and verify results on all of the buffers.
+ const finish = async () => {
+ if (mappable) {
+ await Promise.all(buffers.map(value => value.mapAsync(mode)));
+ } else {
+ buffers.forEach(value => {
+ t.expectValidationError(() => {
+ void value.mapAsync(mode);
+ });
+ });
+ }
+ // Finally, destroy all the buffers to free the resources.
+ buffers.forEach(buffer => buffer.destroy());
+ };
+
+ let errorBuffer: GPUBuffer;
+ for (;;) {
+ if (mappedAtCreation) {
+ // When mappedAtCreation is true, OOM can happen on the client which throws a RangeError. In
+ // this case, we don't do any validations on the OOM buffer.
+ try {
+ t.device.pushErrorScope('out-of-memory');
+ const buffer = t.trackForCleanup(
+ createBufferWithMapState(t.device, kSize, mapState, mode, mappedAtCreation)
+ );
+ if (await t.device.popErrorScope()) {
+ errorBuffer = buffer;
+ break;
+ }
+ buffers.push(buffer);
+ } catch (ex) {
+ t.expect(ex instanceof RangeError);
+ await finish();
+ return;
+ }
+ } else {
+ t.device.pushErrorScope('out-of-memory');
+ const buffer = t.trackForCleanup(
+ createBufferWithMapState(t.device, kSize, mapState, mode, mappedAtCreation)
+ );
+ if (await t.device.popErrorScope()) {
+ errorBuffer = buffer;
+ break;
+ }
+ buffers.push(buffer);
+ }
+ }
+
+ // Do some validation on the OOM buffer.
+ let promise: Promise<void>;
+ t.expectValidationError(() => {
+ promise = errorBuffer.mapAsync(mode);
+ });
+ if (unmapBeforeResolve) {
+ // Should reject with abort error because buffer will be unmapped
+ // before validation check finishes.
+ t.shouldReject('AbortError', promise!);
+ } else {
+ // Should also reject in addition to the validation error.
+ t.shouldReject('OperationError', promise!);
+
+ // Wait for validation error before unmap to ensure validation check
+ // ends before unmap.
+ try {
+ await promise!;
+ throw new Error('The promise should be rejected.');
+ } catch {
+ // Should cause an exception because the promise should be rejected.
+ }
+ }
+
+ // Should throw an OperationError because the buffer is not mapped.
+ // Note: not a RangeError because the state of the buffer is checked first.
+ t.shouldThrow('OperationError', () => {
+ errorBuffer.getMappedRange();
+ });
+
+ // Should't be a validation error even if the buffer failed to be mapped.
+ errorBuffer.unmap();
+ errorBuffer.destroy();
+
+ // Finish the rest of the test w.r.t the mappable buffers.
+ await finish();
+ });
+
+g.test('validation_vs_oom')
+ .desc(
+ `Tests that calls affected by both OOM and validation errors expose the
+validation error with precedence.`
+ )
+ .unimplemented();
+
+g.test('recovery')
+ .desc(
+ `Tests that after going VRAM-OOM, destroying allocated resources eventually
+allows new resources to be allocated.`
+ )
+ .unimplemented();