Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 <!doctype html> | |
| 2 <meta charset="utf-8"> | |
| 3 <meta name="timeout" content="long"> | |
| 4 <title>IndexedDB: Interleaved iteration of multiple cursors</title> | |
| 5 <link rel="author" href="pwnall@chromium.org" title="Victor Costan"> | |
| 6 <script src="/resources/testharness.js"></script> | |
| 7 <script src="/resources/testharnessreport.js"></script> | |
| 8 <script src="support-promises.js"></script> | |
| 9 <script> | |
| 10 // Number of objects that each iterator goes over. | |
| 11 const itemCount = 10; | |
| 12 | |
| 13 // Ratio of small objects to large objects. | |
| 14 const largeObjectRatio = 5; | |
| 15 | |
| 16 // Size of large objects. This should exceed the size of a block in the storage | |
| 17 // method underlying the browser's IndexedDB implementation. For example, this | |
| 18 // needs to exceed the LevelDB block size on Chrome, and the SQLite block size | |
| 19 // on Firefox. | |
| 20 const largeObjectSize = 48 * 1024; | |
| 21 | |
| 22 function objectKey(cursorIndex, itemIndex) { | |
| 23 return `${cursorIndex}-key-${itemIndex}`; | |
| 24 } | |
| 25 | |
| 26 function objectValue(cursorIndex, itemIndex) { | |
| 27 if ((cursorIndex * itemCount + itemIndex) % largeObjectRatio === 0) { | |
| 28 // We use a typed array (as opposed to a string) because IndexedDB | |
| 29 // implementations may serialize strings using UTF-8 or UTF-16, yielding | |
| 30 // larger IndexedDB entries than we'd expect. It's very unlikely that an | |
| 31 // IndexedDB implementation would use anything other than the raw buffer to | |
| 32 // serialize a typed array. | |
| 33 const buffer = new Uint8Array(largeObjectSize); | |
| 34 | |
| 35 // Some IndexedDB implementations, like LevelDB, compress their data blocks | |
| 36 // before storing them to disk. We use a simple 32-bit xorshift PRNG, which | |
| 37 // should be sufficient to foil any fast generic-purpose compression scheme. | |
| 38 | |
| 39 // 32-bit xorshift - the seed can't be zero | |
| 40 let state = 1000 + (cursorIndex * itemCount + itemIndex); | |
| 41 | |
| 42 for (let i = 0; i < largeObjectSize; ++i) { | |
| 43 state ^= state << 13; | |
| 44 state ^= state >> 17; | |
| 45 state ^= state << 5; | |
| 46 buffer[i] = state & 0xff; | |
| 47 } | |
| 48 | |
| 49 return buffer; | |
| 50 } | |
| 51 return [cursorIndex, 'small', itemIndex]; | |
| 52 } | |
| 53 | |
| 54 | |
| 55 // Writes the objects to be read by one cursor. Returns a promise that resolves | |
| 56 // when the write completes. | |
| 57 // | |
| 58 // We want to avoid creating a large transaction, because that is outside the | |
| 59 // test's scope, and it's a bad practice. So we break up the writes across | |
| 60 // multiple transactions. For simplicity, each transaction writes all the | |
| 61 // objects that will be read by a cursor. | |
| 62 function writeCursorObjects(database, cursorIndex) { | |
| 63 return new Promise((resolve, reject) => { | |
| 64 const transaction = database.transaction('cache', 'readwrite'); | |
| 65 const store = transaction.objectStore('cache'); | |
| 66 for (let i = 0; i < itemCount; ++i) { | |
| 67 store.put({ | |
| 68 key: objectKey(cursorIndex, i), value: objectValue(cursorIndex, i)}); | |
| 69 } | |
| 70 transaction.oncomplete = resolve; | |
| 71 transaction.onerror = () => { reject(transaction.error); }; | |
|
jsbell
2017/03/23 21:20:01
transaction.error won't be set to anything here si
pwnall
2017/03/23 22:08:27
Done.
| |
| 72 transaction.onabort = () => { reject(transaction.error); }; | |
| 73 }); | |
| 74 } | |
| 75 | |
| 76 // Returns a promise that resolves when the store has been populated. | |
| 77 function populateTestStore(testCase, database, cursorCount) { | |
| 78 let promiseChain = Promise.resolve(); | |
| 79 | |
| 80 for (let i = 0; i < cursorCount; ++i) | |
| 81 promiseChain = promiseChain.then(() => writeCursorObjects(database, i)); | |
| 82 | |
| 83 return promiseChain; | |
| 84 } | |
| 85 | |
| 86 function interleaveCursors(testCase, store, cursorCount) { | |
| 87 return new Promise((resolve, reject) => { | |
| 88 const cursors = []; | |
| 89 const requests = []; | |
|
dmurph
2017/03/23 21:10:52
Can you add a comment like the following:
// Reque
pwnall
2017/03/23 22:08:27
Done.
| |
| 90 | |
| 91 const checkCursorState = (cursorIndex, itemIndex) => { | |
| 92 const cursor = cursors[cursorIndex]; | |
| 93 assert_equals(cursor.key, objectKey(cursorIndex, itemIndex)); | |
| 94 assert_equals(cursor.value.key, objectKey(cursorIndex, itemIndex)); | |
| 95 assert_equals( | |
| 96 cursor.value.value.join('-'), | |
| 97 objectValue(cursorIndex, itemIndex).join('-')); | |
| 98 }; | |
| 99 | |
| 100 const openCursor = (cursorIndex, callback) => { | |
| 101 const request = store.openCursor( | |
| 102 IDBKeyRange.lowerBound(objectKey(cursorIndex, 0))); | |
| 103 requests[cursorIndex] = request; | |
| 104 | |
| 105 request.onsuccess = testCase.step_func(() => { | |
| 106 const cursor = request.result; | |
| 107 cursors[cursorIndex] = cursor; | |
| 108 checkCursorState(cursorIndex, 0); | |
| 109 callback(); | |
| 110 }); | |
| 111 request.onerror = event => reject(request.error); | |
| 112 }; | |
| 113 | |
| 114 const readItemFromCursor = (cursorIndex, itemIndex, callback) => { | |
| 115 const request = requests[cursorIndex]; | |
| 116 request.onsuccess = testCase.step_func(() => { | |
| 117 const cursor = request.result; | |
| 118 cursors[cursorIndex] = cursor; | |
| 119 checkCursorState(cursorIndex, itemIndex); | |
| 120 callback(); | |
| 121 }); | |
| 122 | |
| 123 const cursor = cursors[cursorIndex]; | |
| 124 cursor.continue(); | |
| 125 }; | |
| 126 | |
| 127 // We open all the cursors one at a time, then cycle through the cursors and | |
| 128 // call continue() on each of them. This access pattern causes maximal | |
| 129 // trashing to an LRU cursor cache. Eviction scheme aside, any cache will | |
| 130 // have to evict some cursors, and this access pattern verifies that the | |
| 131 // cache correctly restores the state of evicted cursors. | |
| 132 const steps = []; | |
| 133 for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) | |
| 134 steps.push(openCursor.bind(null, cursorIndex)); | |
| 135 for (let itemIndex = 1; itemIndex < itemCount; ++itemIndex) { | |
| 136 for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) | |
| 137 steps.push(readItemFromCursor.bind(null, cursorIndex, itemIndex)); | |
| 138 } | |
| 139 | |
| 140 const runStep = (stepIndex) => { | |
| 141 if (stepIndex === steps.length) { | |
| 142 resolve(); | |
| 143 return; | |
| 144 } | |
| 145 steps[stepIndex](() => { runStep(stepIndex + 1); }); | |
| 146 }; | |
| 147 runStep(0); | |
| 148 }); | |
| 149 } | |
| 150 | |
| 151 for (let cursorCount of [1, 10, 100, 500]) { | |
| 152 promise_test(testCase => { | |
| 153 return createDatabase(testCase, (database, transaction) => { | |
| 154 const store = database.createObjectStore('cache', | |
| 155 { keyPath: 'key', autoIncrement: true }); | |
| 156 }).then(database => { | |
| 157 return populateTestStore(testCase, database, cursorCount).then( | |
| 158 () => database); | |
| 159 }).then(database => { | |
| 160 database.close(); | |
| 161 }).then(() => { | |
| 162 return openDatabase(testCase); | |
| 163 }).then(database => { | |
| 164 const transaction = database.transaction('cache', 'readonly'); | |
| 165 const store = transaction.objectStore('cache'); | |
| 166 return interleaveCursors(testCase, store, cursorCount).then( | |
|
jsbell
2017/03/23 21:20:01
This return means the transaction.onXXX below won'
pwnall
2017/03/23 22:08:27
Done.
Durr! Thanks for catching that!
| |
| 167 () => database); | |
| 168 | |
| 169 transaction.onerror = () => { reject(transaction.error); }; | |
|
jsbell
2017/03/23 21:20:01
Same as above - don't bother with transaction.oner
pwnall
2017/03/23 22:08:27
Done.
| |
| 170 transaction.onabort = () => { reject(transaction.error); }; | |
| 171 }).then(database => { | |
| 172 database.close(); | |
| 173 indexedDB.deleteDatabase(database.name); | |
| 174 }); | |
| 175 }, `${cursorCount} cursors`); | |
| 176 } | |
| 177 </script> | |
| OLD | NEW |