// 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", });