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 |