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