Skip to content

Commit ce382d4

Browse files
committed
refactor: make SortedMap more efficient
1 parent 7d20508 commit ce382d4

File tree

2 files changed

+155
-53
lines changed

2 files changed

+155
-53
lines changed

src/utils/sorted-map.js

Lines changed: 58 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,50 +6,76 @@
66
class SortedMap extends Map {
77
/**
88
* @param {Array<k, v>} [entries]
9-
* @param {function(a, b)} [cmp]
9+
* @param {function(a, b)} [cmp] compares [k1, v1] to [k2, v2]
1010
*/
1111
constructor (entries, cmp) {
12-
super(entries)
13-
this._cmp = cmp
12+
super()
13+
this._cmp = cmp || this._defaultSort
1414
this._keys = []
15-
}
16-
17-
// Performance is O(N log(N)).
18-
// Could be O(log(N)) if we use a sorted array. Fix this if performance isn't
19-
// good enough.
20-
set (k, data) {
21-
super.set(k, data)
22-
const i = this._keys.indexOf(k)
23-
if (i < 0) {
24-
this._keys.push(k)
25-
this._keys.sort(this._kvCmp.bind(this))
15+
for (const [k, v] of entries || []) {
16+
this.set(k, v)
2617
}
2718
}
2819

29-
clear () {
30-
super.clear()
31-
this._keys = []
32-
}
33-
3420
/**
35-
* Call update to manually trigger a sort.
21+
* Call update to update the position of the key when it should change.
3622
* For example if the compare function sorts by the priority field, and the
3723
* priority changes, call update.
3824
*
3925
* @param {Object} [k] the key corresponding to the entry whose position
4026
* should be updated.
4127
*/
4228
update (k) {
43-
this._keys.sort(this._kvCmp.bind(this))
29+
if (this.has(k)) {
30+
this.set(k, this.get(k))
31+
}
4432
}
4533

46-
// Same performance comments as set()
47-
delete (k) {
48-
const i = this._keys.indexOf(k)
49-
if (i >= 0) {
34+
set (k, v) {
35+
// If the key is already in the map, remove it from the ordering and
36+
// re-insert it below
37+
if (this.has(k)) {
38+
const i = this._find(k)
5039
this._keys.splice(i, 1)
51-
super.delete(k)
5240
}
41+
42+
// Update / insert the k/v into the map
43+
super.set(k, v)
44+
45+
// Find the correct position of the newly inserted k/v in the order
46+
const i = this._find(k)
47+
this._keys.splice(i, 0, k)
48+
}
49+
50+
clear () {
51+
super.clear()
52+
this._keys = []
53+
}
54+
55+
delete (k) {
56+
if (!this.has(k)) {
57+
return
58+
}
59+
const i = this._find(k)
60+
this._keys.splice(i, 1)
61+
super.delete(k)
62+
}
63+
64+
_find (k) {
65+
let lower = 0
66+
let upper = this._keys.length
67+
while (lower < upper) {
68+
const pivot = (lower + upper) >>> 1 // lower + (upper - lower) / 2
69+
const cmp = this._kCmp(this._keys[pivot], k)
70+
if (cmp < 0) { // pivot < k
71+
lower = pivot + 1
72+
} else if (cmp > 0) { // pivot > k
73+
upper = pivot
74+
} else { // pivot == k
75+
return pivot
76+
}
77+
}
78+
return lower
5379
}
5480

5581
* keys () {
@@ -84,14 +110,13 @@ class SortedMap extends Map {
84110
}
85111
}
86112

87-
_kvCmp (a, b) {
88-
// By default sort by built-in compare
89-
if (!this._cmp) {
90-
if (a < b) return -1
91-
if (b < a) return 1
92-
return 0
93-
}
113+
_defaultSort (a, b) {
114+
if (a[0] < b[0]) return -1
115+
if (b[0] < a[0]) return 1
116+
return 0
117+
}
94118

119+
_kCmp (a, b) {
95120
return this._cmp(
96121
[a, this.get(a)],
97122
[b, this.get(b)]

test/utils.spec.js

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,9 @@ describe('utils spec', function () {
200200

201201
expect(sm.get('two')).to.be.undefined()
202202
expect(sm.size).to.eql(2)
203+
204+
sm.delete('two')
205+
expect(sm.size).to.eql(2)
203206
})
204207

205208
it('clear', () => {
@@ -235,26 +238,100 @@ describe('utils spec', function () {
235238
expect(collected).to.eql([...sm])
236239
})
237240

238-
it('custom order', () => {
239-
const sm = new SortedMap([], (a, b) => b[1].priority - a[1].priority)
240-
241-
const data1 = { k: 'v1', priority: 1 }
242-
const data2 = { k: 'v2', priority: 3 }
243-
const data3 = { k: 'v3', priority: 2 }
244-
sm.set('one', data1)
245-
sm.set('two', data2)
246-
sm.set('three', data3)
247-
248-
expect([...sm.keys()]).to.eql(['two', 'three', 'one'])
249-
expect([...sm.values()].map(v => v.k)).to.eql(['v2', 'v3', 'v1'])
250-
251-
// After changing data that affects the sort order, need to call update
252-
// to actually trigger the sort
253-
data3.priority = 5
254-
sm.update('three')
255-
256-
expect([...sm.keys()]).to.eql(['three', 'two', 'one'])
257-
expect([...sm.values()].map(v => v.k)).to.eql(['v3', 'v2', 'v1'])
241+
describe('custom order', () => {
242+
const prioritySort = (a, b) => b[1].priority - a[1].priority
243+
244+
it('forward', () => {
245+
const sm = new SortedMap([
246+
['low', { priority: 1 }],
247+
['high', { priority: 2 }]
248+
], prioritySort)
249+
expect([...sm.keys()]).to.eql(['high', 'low'])
250+
})
251+
252+
it('backward', () => {
253+
const sm = new SortedMap([
254+
['high', { priority: 2 }],
255+
['low', { priority: 1 }]
256+
], prioritySort)
257+
expect([...sm.keys()]).to.eql(['high', 'low'])
258+
})
259+
260+
it('insert start', () => {
261+
const sm = new SortedMap([
262+
['mid', { priority: 2 }],
263+
['low', { priority: 1 }],
264+
['high', { priority: 3 }]
265+
], prioritySort)
266+
expect([...sm.keys()]).to.eql(['high', 'mid', 'low'])
267+
})
268+
269+
it('insert end', () => {
270+
const sm = new SortedMap([
271+
['low', { priority: 1 }],
272+
['mid', { priority: 2 }],
273+
['high', { priority: 3 }]
274+
], prioritySort)
275+
expect([...sm.keys()]).to.eql(['high', 'mid', 'low'])
276+
})
277+
278+
it('insert middle', () => {
279+
const sm = new SortedMap([
280+
['low', { priority: 1 }],
281+
['high', { priority: 3 }],
282+
['mid', { priority: 2 }]
283+
], prioritySort)
284+
expect([...sm.keys()]).to.eql(['high', 'mid', 'low'])
285+
})
286+
287+
it('insert same priority start', () => {
288+
const sm = new SortedMap([
289+
['low', { priority: 1 }],
290+
['high-a', { priority: 3 }],
291+
['high-b', { priority: 3 }]
292+
], prioritySort)
293+
expect([...sm.keys()].map(s => s.substring(0, 4))).to.eql(['high', 'high', 'low'])
294+
})
295+
296+
it('insert same priority end', () => {
297+
const sm = new SortedMap([
298+
['hi', { priority: 3 }],
299+
['low-a', { priority: 1 }],
300+
['low-b', { priority: 1 }]
301+
], prioritySort)
302+
expect([...sm.keys()].map(s => s.substring(0, 3))).to.eql(['hi', 'low', 'low'])
303+
})
304+
305+
it('insert same key', () => {
306+
const sm = new SortedMap([
307+
['low', { priority: 1 }],
308+
['high', { priority: 3 }],
309+
['high', { priority: 4 }]
310+
], prioritySort)
311+
expect([...sm.keys()]).to.eql(['high', 'low'])
312+
})
313+
314+
it('update', () => {
315+
const sm = new SortedMap([], prioritySort)
316+
317+
const data1 = { k: 'v1', priority: 1 }
318+
const data2 = { k: 'v2', priority: 3 }
319+
const data3 = { k: 'v3', priority: 2 }
320+
sm.set('one', data1)
321+
sm.set('two', data2)
322+
sm.set('three', data3)
323+
324+
expect([...sm.keys()]).to.eql(['two', 'three', 'one'])
325+
expect([...sm.values()].map(v => v.k)).to.eql(['v2', 'v3', 'v1'])
326+
327+
// After changing data that affects the sort order, need to call update
328+
// to actually trigger the sort
329+
data3.priority = 5
330+
sm.update('three')
331+
332+
expect([...sm.keys()]).to.eql(['three', 'two', 'one'])
333+
expect([...sm.values()].map(v => v.k)).to.eql(['v3', 'v2', 'v1'])
334+
})
258335
})
259336
})
260337
})

0 commit comments

Comments
 (0)