diff options
Diffstat (limited to 'testing/web-platform/tests/IndexedDB/resources/interleaved-cursors-common.js')
-rw-r--r-- | testing/web-platform/tests/IndexedDB/resources/interleaved-cursors-common.js | 188 |
1 files changed, 188 insertions, 0 deletions
diff --git a/testing/web-platform/tests/IndexedDB/resources/interleaved-cursors-common.js b/testing/web-platform/tests/IndexedDB/resources/interleaved-cursors-common.js new file mode 100644 index 0000000000..09ed078c1f --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/resources/interleaved-cursors-common.js @@ -0,0 +1,188 @@ +// Infrastructure shared by interleaved-cursors-{small,large}.html + +// Number of objects that each iterator goes over. +const itemCount = 10; + +// Ratio of small objects to large objects. +const largeObjectRatio = 5; + +// Size of large objects. This should exceed the size of a block in the storage +// method underlying the browser's IndexedDB implementation. For example, this +// needs to exceed the LevelDB block size on Chrome, and the SQLite block size +// on Firefox. +const largeObjectSize = 48 * 1024; + +function objectKey(cursorIndex, itemIndex) { + return `${cursorIndex}-key-${itemIndex}`; +} + +function objectValue(cursorIndex, itemIndex) { + if ((cursorIndex * itemCount + itemIndex) % largeObjectRatio === 0) { + // We use a typed array (as opposed to a string) because IndexedDB + // implementations may serialize strings using UTF-8 or UTF-16, yielding + // larger IndexedDB entries than we'd expect. It's very unlikely that an + // IndexedDB implementation would use anything other than the raw buffer to + // serialize a typed array. + const buffer = new Uint8Array(largeObjectSize); + + // Some IndexedDB implementations, like LevelDB, compress their data blocks + // before storing them to disk. We use a simple 32-bit xorshift PRNG, which + // should be sufficient to foil any fast generic-purpose compression scheme. + + // 32-bit xorshift - the seed can't be zero + let state = 1000 + (cursorIndex * itemCount + itemIndex); + + for (let i = 0; i < largeObjectSize; ++i) { + state ^= state << 13; + state ^= state >> 17; + state ^= state << 5; + buffer[i] = state & 0xff; + } + + return buffer; + } + return [cursorIndex, 'small', itemIndex]; +} + +// Writes the objects to be read by one cursor. Returns a promise that resolves +// when the write completes. +// +// We want to avoid creating a large transaction, because that is outside the +// test's scope, and it's a bad practice. So we break up the writes across +// multiple transactions. For simplicity, each transaction writes all the +// objects that will be read by a cursor. +function writeCursorObjects(database, cursorIndex) { + return new Promise((resolve, reject) => { + const transaction = database.transaction('cache', 'readwrite', {durability: 'relaxed'}); + transaction.onabort = () => { reject(transaction.error); }; + + const store = transaction.objectStore('cache'); + for (let i = 0; i < itemCount; ++i) { + store.put({ + key: objectKey(cursorIndex, i), value: objectValue(cursorIndex, i)}); + } + transaction.oncomplete = resolve; + }); +} + +// Returns a promise that resolves when the store has been populated. +function populateTestStore(testCase, database, cursorCount) { + let promiseChain = Promise.resolve(); + + for (let i = 0; i < cursorCount; ++i) + promiseChain = promiseChain.then(() => writeCursorObjects(database, i)); + + return promiseChain; +} + +// Reads cursors in an interleaved fashion, as shown below. +// +// Given N cursors, each of which points to the beginning of a K-item sequence, +// the following accesses will be made. +// +// OC(i) = open cursor i +// RD(i, j) = read result of cursor i, which should be at item j +// CC(i) = continue cursor i +// | = wait for onsuccess on the previous OC or CC +// +// OC(1) | RD(1, 1) OC(2) | RD(2, 1) OC(3) | ... | RD(n-1, 1) CC(n) | +// RD(n, 1) CC(1) | RD(1, 2) CC(2) | RD(2, 2) CC(3) | ... | RD(n-1, 2) CC(n) | +// RD(n, 2) CC(1) | RD(1, 3) CC(2) | RD(2, 3) CC(3) | ... | RD(n-1, 3) CC(n) | +// ... +// RD(n, k-1) CC(1) | RD(1, k) CC(2) | RD(2, k) CC(3) | ... | RD(n-1, k) CC(n) | +// RD(n, k) done +function interleaveCursors(testCase, store, cursorCount) { + return new Promise((resolve, reject) => { + // The cursors used for iteration are stored here so each cursor's onsuccess + // handler can call continue() on the next cursor. + const cursors = []; + + // The results of IDBObjectStore.openCursor() calls are stored here so we + // we can change the requests' onsuccess handler after every + // IDBCursor.continue() call. + const requests = []; + + const checkCursorState = (cursorIndex, itemIndex) => { + const cursor = cursors[cursorIndex]; + assert_equals(cursor.key, objectKey(cursorIndex, itemIndex)); + assert_equals(cursor.value.key, objectKey(cursorIndex, itemIndex)); + assert_equals( + cursor.value.value.join('-'), + objectValue(cursorIndex, itemIndex).join('-')); + }; + + const openCursor = (cursorIndex, callback) => { + const request = store.openCursor( + IDBKeyRange.lowerBound(objectKey(cursorIndex, 0))); + requests[cursorIndex] = request; + + request.onsuccess = testCase.step_func(() => { + const cursor = request.result; + cursors[cursorIndex] = cursor; + checkCursorState(cursorIndex, 0); + callback(); + }); + request.onerror = event => reject(request.error); + }; + + const readItemFromCursor = (cursorIndex, itemIndex, callback) => { + const request = requests[cursorIndex]; + request.onsuccess = testCase.step_func(() => { + const cursor = request.result; + cursors[cursorIndex] = cursor; + checkCursorState(cursorIndex, itemIndex); + callback(); + }); + + const cursor = cursors[cursorIndex]; + cursor.continue(); + }; + + // We open all the cursors one at a time, then cycle through the cursors and + // call continue() on each of them. This access pattern causes maximal + // trashing to an LRU cursor cache. Eviction scheme aside, any cache will + // have to evict some cursors, and this access pattern verifies that the + // cache correctly restores the state of evicted cursors. + const steps = []; + for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) + steps.push(openCursor.bind(null, cursorIndex)); + for (let itemIndex = 1; itemIndex < itemCount; ++itemIndex) { + for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) + steps.push(readItemFromCursor.bind(null, cursorIndex, itemIndex)); + } + + const runStep = (stepIndex) => { + if (stepIndex === steps.length) { + resolve(); + return; + } + steps[stepIndex](() => { runStep(stepIndex + 1); }); + }; + runStep(0); + }); +} + +function cursorTest(cursorCount) { + promise_test(testCase => { + return createDatabase(testCase, (database, transaction) => { + const store = database.createObjectStore('cache', + { keyPath: 'key', autoIncrement: true }); + }).then(database => { + return populateTestStore(testCase, database, cursorCount).then( + () => database); + }).then(database => { + database.close(); + }).then(() => { + return openDatabase(testCase); + }).then(database => { + const transaction = database.transaction('cache', 'readonly', {durability: 'relaxed'}); + transaction.onabort = () => { reject(transaction.error); }; + + const store = transaction.objectStore('cache'); + return interleaveCursors(testCase, store, cursorCount).then( + () => database); + }).then(database => { + database.close(); + }); + }, `${cursorCount} cursors`); +} |