OLD | NEW |
1 <!doctype html> | 1 <!doctype html> |
2 <meta charset="utf-8"> | 2 <meta charset="utf-8"> |
3 <meta name="timeout" content="long"> | 3 <meta name="timeout" content="long"> |
4 <title>IndexedDB: Interleaved iteration of multiple cursors</title> | 4 <title>IndexedDB: Interleaved iteration of multiple cursors over disjoint data r
anges</title> |
5 <link rel="author" href="pwnall@chromium.org" title="Victor Costan"> | 5 <link rel="author" href="pwnall@chromium.org" title="Victor Costan"> |
6 <script src="/resources/testharness.js"></script> | 6 <script src="/resources/testharness.js"></script> |
7 <script src="/resources/testharnessreport.js"></script> | 7 <script src="/resources/testharnessreport.js"></script> |
8 <script src="support-promises.js"></script> | 8 <script src="support-promises.js"></script> |
| 9 <script src="interleaved-cursors-support.js"></script> |
9 <script> | 10 <script> |
| 11 'use strict'; |
| 12 |
10 // Number of objects that each iterator goes over. | 13 // Number of objects that each iterator goes over. |
11 const itemCount = 10; | 14 const itemCount = 8; |
12 | 15 |
13 // Ratio of small objects to large objects. | 16 // Ratio of small objects to large objects. |
14 const largeObjectRatio = 5; | 17 const largeObjectRatio = 3; |
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 | 18 |
22 function objectKey(cursorIndex, itemIndex) { | 19 function objectKey(cursorIndex, itemIndex) { |
23 return `${cursorIndex}-key-${itemIndex}`; | 20 const cursorString = cursorIndex.toString().padStart(5, '0'); |
| 21 const itemString = itemIndex.toString().padStart(5, '0'); |
| 22 return `${cursorString}-key-${itemString}`; |
24 } | 23 } |
25 | 24 |
26 function objectValue(cursorIndex, itemIndex) { | 25 function objectValue(cursorIndex, itemIndex) { |
27 if ((cursorIndex * itemCount + itemIndex) % largeObjectRatio === 0) { | 26 if ((cursorIndex * itemCount + itemIndex) % largeObjectRatio === 0) |
28 // We use a typed array (as opposed to a string) because IndexedDB | 27 return largeObjectValue(cursorIndex, itemIndex); |
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]; | 28 return [cursorIndex, 'small', itemIndex]; |
52 } | 29 } |
53 | 30 |
54 // Writes the objects to be read by one cursor. Returns a promise that resolves | |
55 // when the write completes. | |
56 // | |
57 // We want to avoid creating a large transaction, because that is outside the | |
58 // test's scope, and it's a bad practice. So we break up the writes across | |
59 // multiple transactions. For simplicity, each transaction writes all the | |
60 // objects that will be read by a cursor. | |
61 function writeCursorObjects(database, cursorIndex) { | |
62 return new Promise((resolve, reject) => { | |
63 const transaction = database.transaction('cache', 'readwrite'); | |
64 transaction.onabort = () => { reject(transaction.error); }; | |
65 | |
66 const store = transaction.objectStore('cache'); | |
67 for (let i = 0; i < itemCount; ++i) { | |
68 store.put({ | |
69 key: objectKey(cursorIndex, i), value: objectValue(cursorIndex, i)}); | |
70 } | |
71 transaction.oncomplete = resolve; | |
72 }); | |
73 } | |
74 | |
75 // Returns a promise that resolves when the store has been populated. | |
76 function populateTestStore(testCase, database, cursorCount) { | |
77 let promiseChain = Promise.resolve(); | |
78 | |
79 for (let i = 0; i < cursorCount; ++i) | |
80 promiseChain = promiseChain.then(() => writeCursorObjects(database, i)); | |
81 | |
82 return promiseChain; | |
83 } | |
84 | |
85 // Reads cursors in an interleaved fashion, as shown below. | |
86 // | |
87 // Given N cursors, each of which points to the beginning of a K-item sequence, | |
88 // the following accesses will be made. | |
89 // | |
90 // OC(i) = open cursor i | |
91 // RD(i, j) = read result of cursor i, which should be at item j | |
92 // CC(i) = continue cursor i | |
93 // | = wait for onsuccess on the previous OC or CC | |
94 // | |
95 // OC(1) | RD(1, 1) OC(2) | RD(2, 1) OC(3) | ... | RD(n-1, 1) CC(n) | | |
96 // RD(n, 1) CC(1) | RD(1, 2) CC(2) | RD(2, 2) CC(3) | ... | RD(n-1, 2) CC(n) | | |
97 // RD(n, 2) CC(1) | RD(1, 3) CC(2) | RD(2, 3) CC(3) | ... | RD(n-1, 3) CC(n) | | |
98 // ... | |
99 // RD(n, k-1) CC(1) | RD(1, k) CC(2) | RD(2, k) CC(3) | ... | RD(n-1, k) CC(n) | | |
100 // RD(n, k) done | |
101 function interleaveCursors(testCase, store, cursorCount) { | |
102 return new Promise((resolve, reject) => { | |
103 // The cursors used for iteration are stored here so each cursor's onsuccess | |
104 // handler can call continue() on the next cursor. | |
105 const cursors = []; | |
106 | |
107 // The results of IDBObjectStore.openCursor() calls are stored here so we | |
108 // we can change the requests' onsuccess handler after every | |
109 // IDBCursor.continue() call. | |
110 const requests = []; | |
111 | |
112 const checkCursorState = (cursorIndex, itemIndex) => { | |
113 const cursor = cursors[cursorIndex]; | |
114 assert_equals(cursor.key, objectKey(cursorIndex, itemIndex)); | |
115 assert_equals(cursor.value.key, objectKey(cursorIndex, itemIndex)); | |
116 assert_equals( | |
117 cursor.value.value.join('-'), | |
118 objectValue(cursorIndex, itemIndex).join('-')); | |
119 }; | |
120 | |
121 const openCursor = (cursorIndex, callback) => { | |
122 const request = store.openCursor( | |
123 IDBKeyRange.lowerBound(objectKey(cursorIndex, 0))); | |
124 requests[cursorIndex] = request; | |
125 | |
126 request.onsuccess = testCase.step_func(() => { | |
127 const cursor = request.result; | |
128 cursors[cursorIndex] = cursor; | |
129 checkCursorState(cursorIndex, 0); | |
130 callback(); | |
131 }); | |
132 request.onerror = event => reject(request.error); | |
133 }; | |
134 | |
135 const readItemFromCursor = (cursorIndex, itemIndex, callback) => { | |
136 const request = requests[cursorIndex]; | |
137 request.onsuccess = testCase.step_func(() => { | |
138 const cursor = request.result; | |
139 cursors[cursorIndex] = cursor; | |
140 checkCursorState(cursorIndex, itemIndex); | |
141 callback(); | |
142 }); | |
143 | |
144 const cursor = cursors[cursorIndex]; | |
145 cursor.continue(); | |
146 }; | |
147 | |
148 // We open all the cursors one at a time, then cycle through the cursors and | |
149 // call continue() on each of them. This access pattern causes maximal | |
150 // trashing to an LRU cursor cache. Eviction scheme aside, any cache will | |
151 // have to evict some cursors, and this access pattern verifies that the | |
152 // cache correctly restores the state of evicted cursors. | |
153 const steps = []; | |
154 for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) | |
155 steps.push(openCursor.bind(null, cursorIndex)); | |
156 for (let itemIndex = 1; itemIndex < itemCount; ++itemIndex) { | |
157 for (let cursorIndex = 0; cursorIndex < cursorCount; ++cursorIndex) | |
158 steps.push(readItemFromCursor.bind(null, cursorIndex, itemIndex)); | |
159 } | |
160 | |
161 const runStep = (stepIndex) => { | |
162 if (stepIndex === steps.length) { | |
163 resolve(); | |
164 return; | |
165 } | |
166 steps[stepIndex](() => { runStep(stepIndex + 1); }); | |
167 }; | |
168 runStep(0); | |
169 }); | |
170 } | |
171 | |
172 for (let cursorCount of [1, 10, 100, 500]) { | 31 for (let cursorCount of [1, 10, 100, 500]) { |
173 promise_test(testCase => { | 32 promise_test(testCase => { |
174 return createDatabase(testCase, (database, transaction) => { | 33 return createDatabase(testCase, (database, transaction) => { |
175 const store = database.createObjectStore('cache', | 34 const store = database.createObjectStore('cache', { keyPath: 'key' }); |
176 { keyPath: 'key', autoIncrement: true }); | |
177 }).then(database => { | 35 }).then(database => { |
178 return populateTestStore(testCase, database, cursorCount).then( | 36 return populateTestStore(testCase, database, cursorCount).then( |
179 () => database); | 37 () => database); |
180 }).then(database => { | 38 }).then(database => { |
181 database.close(); | 39 database.close(); |
182 }).then(() => { | 40 }).then(() => { |
183 return openDatabase(testCase); | 41 return openDatabase(testCase); |
184 }).then(database => { | 42 }).then(database => { |
185 const transaction = database.transaction('cache', 'readonly'); | 43 const transaction = database.transaction('cache', 'readonly'); |
186 transaction.onabort = () => { reject(transaction.error); }; | 44 transaction.onabort = () => { reject(transaction.error); }; |
187 | 45 |
188 const store = transaction.objectStore('cache'); | 46 const store = transaction.objectStore('cache'); |
189 return interleaveCursors(testCase, store, cursorCount).then( | 47 return interleaveCursors(testCase, store, cursorCount, itemCount).then( |
190 () => database); | 48 () => database); |
191 }).then(database => { | 49 }).then(database => { |
192 database.close(); | 50 database.close(); |
193 }); | 51 }); |
194 }, `${cursorCount} cursors`); | 52 }, `${cursorCount} cursors`); |
195 } | 53 } |
| 54 |
196 </script> | 55 </script> |
OLD | NEW |