| // META: title=IDB-backed composite blobs maintain coherency |
| // META: script=resources/support-promises.js |
| // META: timeout=long |
| |
| // This test file is intended to help validate browser handling of complex blob |
| // scenarios where one or more levels of multipart blobs are used and varying |
| // IPC serialization strategies may be used depending on various complexity |
| // heuristics. |
| // |
| // A variety of approaches of reading the blob's contents are attempted for |
| // completeness: |
| // - `fetch-blob-url`: fetch of a URL created via URL.createObjectURL |
| // - Note that this is likely to involve multi-process behavior in a way that |
| // the next 2 currently will not unless their Blobs are round-tripped |
| // through a MessagePort. |
| // - `file-reader`: FileReader |
| // - `direct`: Blob.prototype.arrayBuffer() |
| |
| function composite_blob_test({ blobCount, blobSize, name }) { |
| // NOTE: In order to reduce the runtime of this test and due to the similarity |
| // of the "file-reader" mechanism to the "direct", "file-reader" is commented |
| // out, but if you are investigating failures detected by this test, you may |
| // want to uncomment it. |
| for (const mode of ["fetch-blob-url", /*"file-reader",*/ "direct"]) { |
| promise_test(async testCase => { |
| const key = "the-blobs"; |
| let memBlobs = []; |
| for (let iBlob = 0; iBlob < blobCount; iBlob++) { |
| memBlobs.push(new Blob([make_arraybuffer_contents(iBlob, blobSize)])); |
| } |
| |
| const db = await createDatabase(testCase, db => { |
| db.createObjectStore("blobs"); |
| }); |
| |
| const write_tx = db.transaction("blobs", "readwrite", {durability: "relaxed"}); |
| let store = write_tx.objectStore("blobs"); |
| store.put(memBlobs, key); |
| // Make the blobs eligible for GC which is most realistic and most likely |
| // to cause problems. |
| memBlobs = null; |
| |
| await promiseForTransaction(testCase, write_tx); |
| |
| const read_tx = db.transaction("blobs", "readonly", {durability: "relaxed"}); |
| store = read_tx.objectStore("blobs"); |
| const read_req = store.get(key); |
| |
| await promiseForTransaction(testCase, read_tx); |
| |
| const diskBlobs = read_req.result; |
| const compositeBlob = new Blob(diskBlobs); |
| |
| if (mode === "fetch-blob-url") { |
| const blobUrl = URL.createObjectURL(compositeBlob); |
| let urlResp = await fetch(blobUrl); |
| let urlFetchArrayBuffer = await urlResp.arrayBuffer(); |
| urlResp = null; |
| |
| URL.revokeObjectURL(blobUrl); |
| validate_arraybuffer_contents("fetched URL", urlFetchArrayBuffer, blobCount, blobSize); |
| urlFetchArrayBuffer = null; |
| |
| } else if (mode === "file-reader") { |
| let reader = new FileReader(); |
| let readerPromise = new Promise(resolve => { |
| reader.onload = () => { |
| resolve(reader.result); |
| } |
| }) |
| reader.readAsArrayBuffer(compositeBlob); |
| |
| let readArrayBuffer = await readerPromise; |
| readerPromise = null; |
| reader = null; |
| |
| validate_arraybuffer_contents("FileReader", readArrayBuffer, blobCount, blobSize); |
| readArrayBuffer = null; |
| } else if (mode === "direct") { |
| let directArrayBuffer = await compositeBlob.arrayBuffer(); |
| validate_arraybuffer_contents("arrayBuffer", directArrayBuffer, blobCount, blobSize); |
| } |
| }, `Composite Blob Handling: ${name}: ${mode}`); |
| } |
| } |
| |
| // Create an ArrayBuffer whose even bytes are the index identifier and whose |
| // odd bytes are a sequence incremented by 3 (wrapping at 256) so that |
| // discontinuities at power-of-2 boundaries are more detectable. |
| function make_arraybuffer_contents(index, size) { |
| const arr = new Uint8Array(size); |
| for (let i = 0, counter = 0; i < size; i += 2, counter = (counter + 3) % 256) { |
| arr[i] = index; |
| arr[i + 1] = counter; |
| } |
| return arr.buffer; |
| } |
| |
| function validate_arraybuffer_contents(source, buffer, blobCount, blobSize) { |
| // Accumulate a list of problems we perceive so we can report what seems to |
| // have happened all at once. |
| const problems = []; |
| |
| const arr = new Uint8Array(buffer); |
| |
| const expectedLength = blobCount * blobSize; |
| const actualCount = arr.length / blobSize; |
| if (arr.length !== expectedLength) { |
| problems.push(`ArrayBuffer only holds ${actualCount} blobs' worth instead of ${blobCount}.`); |
| problems.push(`Actual ArrayBuffer is ${arr.length} bytes but expected ${expectedLength}`); |
| } |
| |
| const counterBlobStep = (blobSize / 2 * 3) % 256; |
| let expectedBlob = 0; |
| let blobSeenSoFar = 0; |
| let expectedCounter = 0; |
| let counterDrift = 0; |
| for (let i = 0; i < arr.length; i += 2) { |
| if (arr[i] !== expectedBlob || blobSeenSoFar >= blobSize) { |
| if (blobSeenSoFar !== blobSize) { |
| problems.push(`Truncated blob ${expectedBlob} after ${blobSeenSoFar} bytes.`); |
| } else { |
| expectedBlob++; |
| } |
| if (expectedBlob !== arr[i]) { |
| problems.push(`Expected blob ${expectedBlob} but found ${arr[i]}, compensating.`); |
| expectedBlob = arr[i]; |
| } |
| blobSeenSoFar = 0; |
| expectedCounter = (expectedBlob * counterBlobStep) % 256; |
| counterDrift = 0; |
| } |
| |
| if (arr[i + 1] !== (expectedCounter + counterDrift) % 256) { |
| const newDrift = expectedCounter - arr[i + 1]; |
| problems.push(`In blob ${expectedBlob} at ${blobSeenSoFar + 1} bytes in, counter drift now ${newDrift} was ${counterDrift}`); |
| counterDrift = newDrift; |
| } |
| |
| blobSeenSoFar += 2; |
| expectedCounter = (expectedCounter + 3) % 256; |
| } |
| |
| if (problems.length) { |
| assert_true(false, `${source} blob payload problem: ${problems.join("\n")}`); |
| } else { |
| assert_true(true, `${source} blob payloads validated.`); |
| } |
| } |
| |
| composite_blob_test({ |
| blobCount: 16, |
| blobSize: 256 * 1024, |
| name: "Many blobs", |
| }); |