tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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();