oom.spec.ts (5523B)
1 export const description = ` 2 Stress tests covering robustness when available VRAM is exhausted. 3 `; 4 5 import { makeTestGroup } from '../../common/framework/test_group.js'; 6 import { unreachable } from '../../common/util/util.js'; 7 import { GPUConst } from '../../webgpu/constants.js'; 8 import { GPUTest } from '../../webgpu/gpu_test.js'; 9 import { exhaustVramUntilUnder64MB } from '../../webgpu/util/memory.js'; 10 11 export const g = makeTestGroup(GPUTest); 12 13 function createBufferWithMapState( 14 t: GPUTest, 15 size: number, 16 mapState: GPUBufferMapState, 17 mode: GPUMapModeFlags, 18 mappedAtCreation: boolean 19 ) { 20 const mappable = mapState === 'unmapped'; 21 if (!mappable && !mappedAtCreation) { 22 return t.createBufferTracked({ 23 size, 24 usage: GPUBufferUsage.UNIFORM, 25 mappedAtCreation, 26 }); 27 } 28 let buffer: GPUBuffer; 29 switch (mode) { 30 case GPUMapMode.READ: 31 buffer = t.createBufferTracked({ 32 size, 33 usage: GPUBufferUsage.MAP_READ, 34 mappedAtCreation, 35 }); 36 break; 37 case GPUMapMode.WRITE: 38 buffer = t.createBufferTracked({ 39 size, 40 usage: GPUBufferUsage.MAP_WRITE, 41 mappedAtCreation, 42 }); 43 break; 44 default: 45 unreachable(); 46 } 47 // If we want the buffer to be mappable and also mappedAtCreation, we call unmap on it now. 48 if (mappable && mappedAtCreation) { 49 buffer.unmap(); 50 } 51 return buffer; 52 } 53 54 g.test('vram_oom') 55 .desc(`Tests that we can allocate buffers until we run out of VRAM.`) 56 .fn(async t => { 57 await exhaustVramUntilUnder64MB(t); 58 }); 59 60 g.test('map_after_vram_oom') 61 .desc( 62 `Allocates tons of buffers and textures with varying mapping states (unmappable, 63 mappable, mapAtCreation, mapAtCreation-then-unmapped) until OOM; then attempts 64 to mapAsync all the mappable objects. The last buffer should be an error buffer so 65 mapAsync on it should reject and produce a validation error. ` 66 ) 67 .params(u => 68 u 69 .combine('mapState', ['mapped', 'unmapped'] as GPUBufferMapState[]) 70 .combine('mode', [GPUConst.MapMode.READ, GPUConst.MapMode.WRITE]) 71 .combine('mappedAtCreation', [true, false]) 72 .combine('unmapBeforeResolve', [true, false]) 73 ) 74 .fn(async t => { 75 // Use a relatively large size to quickly hit OOM. 76 const kSize = 512 * 1024 * 1024; 77 78 const { mapState, mode, mappedAtCreation, unmapBeforeResolve } = t.params; 79 const mappable = mapState === 'unmapped'; 80 const buffers: GPUBuffer[] = []; 81 // Closure to call map and verify results on all of the buffers. 82 const finish = async () => { 83 if (mappable) { 84 await Promise.all(buffers.map(value => value.mapAsync(mode))); 85 } else { 86 buffers.forEach(value => { 87 t.expectValidationError(() => { 88 void value.mapAsync(mode); 89 }); 90 }); 91 } 92 // Finally, destroy all the buffers to free the resources. 93 buffers.forEach(buffer => buffer.destroy()); 94 }; 95 96 let errorBuffer: GPUBuffer; 97 for (;;) { 98 if (mappedAtCreation) { 99 // When mappedAtCreation is true, OOM can happen on the client which throws a RangeError. In 100 // this case, we don't do any validations on the OOM buffer. 101 try { 102 t.device.pushErrorScope('out-of-memory'); 103 const buffer = createBufferWithMapState(t, kSize, mapState, mode, mappedAtCreation); 104 if (await t.device.popErrorScope()) { 105 errorBuffer = buffer; 106 break; 107 } 108 buffers.push(buffer); 109 } catch (ex) { 110 t.expect(ex instanceof RangeError); 111 await finish(); 112 return; 113 } 114 } else { 115 t.device.pushErrorScope('out-of-memory'); 116 const buffer = createBufferWithMapState(t, kSize, mapState, mode, mappedAtCreation); 117 if (await t.device.popErrorScope()) { 118 errorBuffer = buffer; 119 break; 120 } 121 buffers.push(buffer); 122 } 123 } 124 125 // Do some validation on the OOM buffer. 126 let promise: Promise<void>; 127 t.expectValidationError(() => { 128 promise = errorBuffer.mapAsync(mode); 129 }); 130 if (unmapBeforeResolve) { 131 // Should reject with abort error because buffer will be unmapped 132 // before validation check finishes. 133 t.shouldReject('AbortError', promise!); 134 } else { 135 // Should also reject in addition to the validation error. 136 t.shouldReject('OperationError', promise!); 137 138 // Wait for validation error before unmap to ensure validation check 139 // ends before unmap. 140 try { 141 await promise!; 142 throw new Error('The promise should be rejected.'); 143 } catch { 144 // Should cause an exception because the promise should be rejected. 145 } 146 } 147 148 // Should throw an OperationError because the buffer is not mapped. 149 // Note: not a RangeError because the state of the buffer is checked first. 150 t.shouldThrow('OperationError', () => { 151 errorBuffer.getMappedRange(); 152 }); 153 154 // Should't be a validation error even if the buffer failed to be mapped. 155 errorBuffer.unmap(); 156 errorBuffer.destroy(); 157 158 // Finish the rest of the test w.r.t the mappable buffers. 159 await finish(); 160 }); 161 162 g.test('validation_vs_oom') 163 .desc( 164 `Tests that calls affected by both OOM and validation errors expose the 165 validation error with precedence.` 166 ) 167 .unimplemented(); 168 169 g.test('recovery') 170 .desc( 171 `Tests that after going VRAM-OOM, destroying allocated resources eventually 172 allows new resources to be allocated.` 173 ) 174 .unimplemented();