blob-composite-blob-reads.any.js (5756B)
1 // META: title=IDB-backed composite blobs maintain coherency 2 // META: script=resources/support-promises.js 3 // META: timeout=long 4 'use strict'; 5 6 // This test file is intended to help validate browser handling of complex blob 7 // scenarios where one or more levels of multipart blobs are used and varying 8 // IPC serialization strategies may be used depending on various complexity 9 // heuristics. 10 // 11 // A variety of approaches of reading the blob's contents are attempted for 12 // completeness: 13 // - `fetch-blob-url`: fetch of a URL created via URL.createObjectURL 14 // - Note that this is likely to involve multi-process behavior in a way that 15 // the next 2 currently will not unless their Blobs are round-tripped 16 // through a MessagePort. 17 // - `file-reader`: FileReader 18 // - `direct`: Blob.prototype.arrayBuffer() 19 20 function composite_blob_test({ blobCount, blobSize, name }) { 21 // NOTE: In order to reduce the runtime of this test and due to the similarity 22 // of the "file-reader" mechanism to the "direct", "file-reader" is commented 23 // out, but if you are investigating failures detected by this test, you may 24 // want to uncomment it. 25 for (const mode of ["fetch-blob-url", /*"file-reader",*/ "direct"]) { 26 promise_test(async testCase => { 27 const key = "the-blobs"; 28 let memBlobs = []; 29 for (let iBlob = 0; iBlob < blobCount; iBlob++) { 30 memBlobs.push(new Blob([make_arraybuffer_contents(iBlob, blobSize)])); 31 } 32 33 const db = await createDatabase(testCase, db => { 34 db.createObjectStore("blobs"); 35 }); 36 37 const write_tx = db.transaction("blobs", "readwrite"); 38 let store = write_tx.objectStore("blobs"); 39 store.put(memBlobs, key); 40 // Make the blobs eligible for GC which is most realistic and most likely 41 // to cause problems. 42 memBlobs = null; 43 44 await promiseForTransaction(testCase, write_tx); 45 46 const read_tx = db.transaction("blobs", "readonly"); 47 store = read_tx.objectStore("blobs"); 48 const read_req = store.get(key); 49 50 await promiseForTransaction(testCase, read_tx); 51 52 const diskBlobs = read_req.result; 53 const compositeBlob = new Blob(diskBlobs); 54 55 if (mode === "fetch-blob-url") { 56 const blobUrl = URL.createObjectURL(compositeBlob); 57 let urlResp = await fetch(blobUrl); 58 let urlFetchArrayBuffer = await urlResp.arrayBuffer(); 59 urlResp = null; 60 61 URL.revokeObjectURL(blobUrl); 62 validate_arraybuffer_contents("fetched URL", urlFetchArrayBuffer, blobCount, blobSize); 63 urlFetchArrayBuffer = null; 64 65 } else if (mode === "file-reader") { 66 let reader = new FileReader(); 67 let readerPromise = new Promise(resolve => { 68 reader.onload = () => { 69 resolve(reader.result); 70 } 71 }) 72 reader.readAsArrayBuffer(compositeBlob); 73 74 let readArrayBuffer = await readerPromise; 75 readerPromise = null; 76 reader = null; 77 78 validate_arraybuffer_contents("FileReader", readArrayBuffer, blobCount, blobSize); 79 readArrayBuffer = null; 80 } else if (mode === "direct") { 81 let directArrayBuffer = await compositeBlob.arrayBuffer(); 82 validate_arraybuffer_contents("arrayBuffer", directArrayBuffer, blobCount, blobSize); 83 } 84 }, `Composite Blob Handling: ${name}: ${mode}`); 85 } 86 } 87 88 // Create an ArrayBuffer whose even bytes are the index identifier and whose 89 // odd bytes are a sequence incremented by 3 (wrapping at 256) so that 90 // discontinuities at power-of-2 boundaries are more detectable. 91 function make_arraybuffer_contents(index, size) { 92 const arr = new Uint8Array(size); 93 for (let i = 0, counter = 0; i < size; i += 2, counter = (counter + 3) % 256) { 94 arr[i] = index; 95 arr[i + 1] = counter; 96 } 97 return arr.buffer; 98 } 99 100 function validate_arraybuffer_contents(source, buffer, blobCount, blobSize) { 101 // Accumulate a list of problems we perceive so we can report what seems to 102 // have happened all at once. 103 const problems = []; 104 105 const arr = new Uint8Array(buffer); 106 107 const expectedLength = blobCount * blobSize; 108 const actualCount = arr.length / blobSize; 109 if (arr.length !== expectedLength) { 110 problems.push(`ArrayBuffer only holds ${actualCount} blobs' worth instead of ${blobCount}.`); 111 problems.push(`Actual ArrayBuffer is ${arr.length} bytes but expected ${expectedLength}`); 112 } 113 114 const counterBlobStep = (blobSize / 2 * 3) % 256; 115 let expectedBlob = 0; 116 let blobSeenSoFar = 0; 117 let expectedCounter = 0; 118 let counterDrift = 0; 119 for (let i = 0; i < arr.length; i += 2) { 120 if (arr[i] !== expectedBlob || blobSeenSoFar >= blobSize) { 121 if (blobSeenSoFar !== blobSize) { 122 problems.push(`Truncated blob ${expectedBlob} after ${blobSeenSoFar} bytes.`); 123 } else { 124 expectedBlob++; 125 } 126 if (expectedBlob !== arr[i]) { 127 problems.push(`Expected blob ${expectedBlob} but found ${arr[i]}, compensating.`); 128 expectedBlob = arr[i]; 129 } 130 blobSeenSoFar = 0; 131 expectedCounter = (expectedBlob * counterBlobStep) % 256; 132 counterDrift = 0; 133 } 134 135 if (arr[i + 1] !== (expectedCounter + counterDrift) % 256) { 136 const newDrift = expectedCounter - arr[i + 1]; 137 problems.push(`In blob ${expectedBlob} at ${blobSeenSoFar + 1} bytes in, counter drift now ${newDrift} was ${counterDrift}`); 138 counterDrift = newDrift; 139 } 140 141 blobSeenSoFar += 2; 142 expectedCounter = (expectedCounter + 3) % 256; 143 } 144 145 if (problems.length) { 146 assert_true(false, `${source} blob payload problem: ${problems.join("\n")}`); 147 } else { 148 assert_true(true, `${source} blob payloads validated.`); 149 } 150 } 151 152 composite_blob_test({ 153 blobCount: 16, 154 blobSize: 256 * 1024, 155 name: "Many blobs", 156 });