interleaved-cursors-common.js (7141B)
1 // Infrastructure shared by interleaved-cursors-{small,large}.html 2 'use strict'; 3 4 // Number of objects that each iterator goes over. 5 const itemCount = 10; 6 7 // Ratio of small objects to large objects. 8 const largeObjectRatio = 5; 9 10 // Size of large objects. This should exceed the size of a block in the storage 11 // method underlying the browser's IndexedDB implementation. For example, this 12 // needs to exceed the LevelDB block size on Chrome, and the SQLite block size 13 // on Firefox. 14 const largeObjectSize = 48 * 1024; 15 16 function objectKey(cursorIndex, itemIndex) { 17 return `${cursorIndex}-key-${itemIndex}`; 18 } 19 20 function objectValue(cursorIndex, itemIndex) { 21 if ((cursorIndex * itemCount + itemIndex) % largeObjectRatio === 0) { 22 // We use a typed array (as opposed to a string) because IndexedDB 23 // implementations may serialize strings using UTF-8 or UTF-16, yielding 24 // larger IndexedDB entries than we'd expect. It's very unlikely that an 25 // IndexedDB implementation would use anything other than the raw buffer to 26 // serialize a typed array. 27 const buffer = new Uint8Array(largeObjectSize); 28 29 // Some IndexedDB implementations, like LevelDB, compress their data blocks 30 // before storing them to disk. We use a simple 32-bit xorshift PRNG, which 31 // should be sufficient to foil any fast generic-purpose compression scheme. 32 33 // 32-bit xorshift - the seed can't be zero 34 let state = 1000 + (cursorIndex * itemCount + itemIndex); 35 36 for (let i = 0; i < largeObjectSize; ++i) { 37 state ^= state << 13; 38 state ^= state >> 17; 39 state ^= state << 5; 40 buffer[i] = state & 0xff; 41 } 42 43 return buffer; 44 } 45 return [cursorIndex, 'small', itemIndex]; 46 } 47 48 // Writes the objects to be read by one cursor. Returns a promise that resolves 49 // when the write completes. 50 // 51 // We want to avoid creating a large transaction, because that is outside the 52 // test's scope, and it's a bad practice. So we break up the writes across 53 // multiple transactions. For simplicity, each transaction writes all the 54 // objects that will be read by a cursor. 55 function writeCursorObjects(database, cursorIndex) { 56 return new Promise((resolve, reject) => { 57 const transaction = database.transaction('cache', 'readwrite'); 58 transaction.onabort = () => { reject(transaction.error); }; 59 60 const store = transaction.objectStore('cache'); 61 for (let i = 0; i < itemCount; ++i) { 62 store.put({ 63 key: objectKey(cursorIndex, i), value: objectValue(cursorIndex, i)}); 64 } 65 transaction.oncomplete = resolve; 66 }); 67 } 68 69 // Returns a promise that resolves when the store has been populated. 70 function populateTestStore(testCase, database, cursorCount) { 71 let promiseChain = Promise.resolve(); 72 73 for (let i = 0; i < cursorCount; ++i) 74 promiseChain = promiseChain.then(() => writeCursorObjects(database, i)); 75 76 return promiseChain; 77 } 78 79 // Reads cursors in an interleaved fashion, as shown below. 80 // 81 // Given N cursors, each of which points to the beginning of a K-item sequence, 82 // the following accesses will be made. 83 // 84 // OC(i) = open cursor i 85 // RD(i, j) = read result of cursor i, which should be at item j 86 // CC(i) = continue cursor i 87 // | = wait for onsuccess on the previous OC or CC 88 // 89 // OC(1) | RD(1, 1) OC(2) | RD(2, 1) OC(3) | ... | RD(n-1, 1) CC(n) | 90 // RD(n, 1) CC(1) | RD(1, 2) CC(2) | RD(2, 2) CC(3) | ... | RD(n-1, 2) CC(n) | 91 // RD(n, 2) CC(1) | RD(1, 3) CC(2) | RD(2, 3) CC(3) | ... | RD(n-1, 3) CC(n) | 92 // ... 93 // RD(n, k-1) CC(1) | RD(1, k) CC(2) | RD(2, k) CC(3) | ... | RD(n-1, k) CC(n) | 94 // RD(n, k) done 95 function interleaveCursors(testCase, store, cursorCount) { 96 return new Promise((resolve, reject) => { 97 // The cursors used for iteration are stored here so each cursor's onsuccess 98 // handler can call continue() on the next cursor. 99 const cursors = []; 100 101 // The results of IDBObjectStore.openCursor() calls are stored here so we 102 // we can change the requests' onsuccess handler after every 103 // IDBCursor.continue() call. 104 const requests = []; 105 106 const checkCursorState = (cursorIndex, itemIndex) => { 107 const cursor = cursors[cursorIndex]; 108 assert_equals(cursor.key, objectKey(cursorIndex, itemIndex)); 109 assert_equals(cursor.value.key, objectKey(cursorIndex, itemIndex)); 110 assert_equals( 111 cursor.value.value.join('-'), 112 objectValue(cursorIndex, itemIndex).join('-')); 113 }; 114 115 const openCursor = (cursorIndex, callback) => { 116 const request = store.openCursor( 117 IDBKeyRange.lowerBound(objectKey(cursorIndex, 0))); 118 requests[cursorIndex] = request; 119 120 request.onsuccess = testCase.step_func(() => { 121 const cursor = request.result; 122 cursors[cursorIndex] = cursor; 123 checkCursorState(cursorIndex, 0); 124 callback(); 125 }); 126 request.onerror = event => reject(request.error); 127 }; 128 129 const readItemFromCursor = (cursorIndex, itemIndex, callback) => { 130 const request = requests[cursorIndex]; 131 request.onsuccess = testCase.step_func(() => { 132 const cursor = request.result; 133 cursors[cursorIndex] = cursor; 134 checkCursorState(cursorIndex, itemIndex); 135 callback(); 136 }); 137 138 const cursor = cursors[cursorIndex]; 139 cursor.continue(); 140 }; 141 142 // We open all the cursors one at a time, then cycle through the cursors and 143 // call continue() on each of them. This access pattern causes maximal 144 // trashing to an LRU cursor cache. Eviction scheme aside, any cache will 145 // have to evict some cursors, and this access pattern verifies that the 146 // cache correctly restores the state of evicted cursors. 147 const steps = []; 148 for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) 149 steps.push(openCursor.bind(null, cursorIndex)); 150 for (let itemIndex = 1; itemIndex < itemCount; ++itemIndex) { 151 for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) 152 steps.push(readItemFromCursor.bind(null, cursorIndex, itemIndex)); 153 } 154 155 const runStep = (stepIndex) => { 156 if (stepIndex === steps.length) { 157 resolve(); 158 return; 159 } 160 steps[stepIndex](() => { runStep(stepIndex + 1); }); 161 }; 162 runStep(0); 163 }); 164 } 165 166 function cursorTest(cursorCount) { 167 promise_test(testCase => { 168 return createDatabase(testCase, (database, transaction) => { 169 const store = database.createObjectStore('cache', 170 { keyPath: 'key', autoIncrement: true }); 171 }).then(database => { 172 return populateTestStore(testCase, database, cursorCount).then( 173 () => database); 174 }).then(database => { 175 database.close(); 176 }).then(() => { 177 return openDatabase(testCase); 178 }).then(database => { 179 const transaction = database.transaction('cache', 'readonly'); 180 transaction.onabort = () => { reject(transaction.error); }; 181 182 const store = transaction.objectStore('cache'); 183 return interleaveCursors(testCase, store, cursorCount).then( 184 () => database); 185 }).then(database => { 186 database.close(); 187 }); 188 }, `${cursorCount} cursors`); 189 }