Skip to content

Commit 1ce8fd5

Browse files
committed
fix: make circular buffer not recreate an array on each overflow
1 parent 24af2da commit 1ce8fd5

File tree

5 files changed

+198
-44
lines changed

5 files changed

+198
-44
lines changed

ts/data/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ async function removeMessage(id: string): Promise<void> {
392392

393393
/**
394394
* Note: this method will not clean up external files, just delete from SQL.
395-
* File are cleaned up on app start if they are not linked to any messages
395+
* Files are cleaned up on app start if they are not linked to any messages
396396
*
397397
*/
398398
async function removeMessagesByIds(ids: Array<string>): Promise<void> {

ts/session/apis/open_group_api/sogsv3/sogsApiV3.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ const handleMessagesResponseV4 = async (
326326
/*
327327
* When a message is deleted from the server, we get the deleted event as a data: null on the message itself
328328
* and an update on its reactions.
329-
* But, because we just deleted that message, we can skip trying to udpate its reactions: it's not in the DB anymore.
329+
* But, because we just deleted that message, we can skip trying to update its reactions: it's not in the DB anymore.
330330
*/
331331
if (sogsRollingDeletions.hasMessageDeletedId(conversationId, messageWithReaction.id)) {
332332
continue;

ts/session/apis/open_group_api/sogsv3/sogsRollingDeletions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const addMessageDeletedId = (conversationId: string, messageDeletedId: number) =
1313
if (!ringBuffer) {
1414
return;
1515
}
16-
ringBuffer.add(messageDeletedId);
16+
ringBuffer.insert(messageDeletedId);
1717
};
1818

1919
const hasMessageDeletedId = (conversationId: string, messageDeletedId: number) => {

ts/session/utils/RingBuffer.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
*
55
*/
66
export class RingBuffer<T> {
7+
private newest = -1;
8+
private oldest = 0;
79
private buffer: Array<T> = [];
810
private readonly capacity: number;
911

@@ -16,25 +18,59 @@ export class RingBuffer<T> {
1618
}
1719

1820
public getLength(): number {
19-
return this.buffer.length;
21+
if (this.isEmpty()) {
22+
return 0;
23+
}
24+
25+
// When only one item was added, newest = 0 and oldest = 0.
26+
// When more than one item was added, but less than capacity, newest = nbItemsAdded & oldest = 0.
27+
// As soon as we overflow, oldest is incremented to oldest+1 and newest rolls back to 0,
28+
// so this test fails here and we have to extract the length based on the two parts instead.
29+
if (this.newest >= this.oldest) {
30+
return this.newest + 1;
31+
}
32+
const firstPart = this.capacity - this.oldest;
33+
const secondPart = this.newest + 1;
34+
return firstPart + secondPart;
2035
}
2136

22-
public add(item: T) {
23-
this.buffer.push(item);
24-
this.crop();
37+
public insert(item: T) {
38+
// see comments in `getLength()`
39+
this.newest = (this.newest + 1) % this.capacity;
40+
if (this.buffer.length >= this.capacity) {
41+
this.oldest = (this.oldest + 1) % this.capacity;
42+
}
43+
this.buffer[this.newest] = item;
2544
}
2645

2746
public has(item: T) {
28-
return this.buffer.includes(item);
47+
// no items at all
48+
if (this.isEmpty()) {
49+
return false;
50+
}
51+
return this.toArray().includes(item);
52+
}
53+
54+
public isEmpty() {
55+
return this.newest === -1;
2956
}
3057

3158
public clear() {
3259
this.buffer = [];
60+
this.newest = -1;
61+
this.oldest = 0;
3362
}
3463

35-
private crop() {
36-
while (this.buffer.length > this.capacity) {
37-
this.buffer.shift();
64+
public toArray(): Array<T> {
65+
if (this.isEmpty()) {
66+
return [];
67+
}
68+
69+
if (this.newest >= this.oldest) {
70+
return this.buffer.slice(0, this.newest + 1);
3871
}
72+
const firstPart = this.buffer.slice(this.oldest, this.capacity);
73+
const secondPart = this.buffer.slice(0, this.newest + 1);
74+
return [...firstPart, ...secondPart];
3975
}
4076
}

ts/test/session/unit/utils/RingBuffer_test.ts

Lines changed: 151 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,43 +10,80 @@ describe('RingBuffer Utils', () => {
1010
const ring = new RingBuffer<number>(5000);
1111
expect(ring.getCapacity()).to.equal(5000);
1212
expect(ring.getLength()).to.equal(0);
13-
expect(ring.has(0)).to.equal(false, '4 should not be there');
13+
expect(ring.has(0)).to.equal(false, '0 should not be there');
1414
});
1515

1616
describe('length & capacity are right', () => {
17+
it('length is right 0', () => {
18+
const ring = new RingBuffer<number>(4);
19+
expect(ring.getLength()).to.equal(0);
20+
});
21+
1722
it('length is right 1', () => {
1823
const ring = new RingBuffer<number>(4);
19-
ring.add(0);
24+
ring.insert(0);
2025
expect(ring.getLength()).to.equal(1);
2126
});
2227

2328
it('length is right 4', () => {
2429
const ring = new RingBuffer<number>(4);
25-
ring.add(0);
26-
ring.add(1);
27-
ring.add(2);
28-
ring.add(3);
30+
ring.insert(0);
31+
ring.insert(1);
32+
ring.insert(2);
33+
ring.insert(3);
2934
expect(ring.getLength()).to.equal(4);
3035
});
3136

3237
it('capacity does not get exceeded', () => {
3338
const ring = new RingBuffer<number>(4);
34-
ring.add(0);
35-
ring.add(1);
36-
ring.add(2);
37-
ring.add(3);
38-
ring.add(4);
39+
ring.insert(0);
40+
ring.insert(1);
41+
ring.insert(2);
42+
ring.insert(3);
43+
ring.insert(4);
3944
expect(ring.getLength()).to.equal(4);
4045
});
4146
});
4247

48+
describe('isEmpty is correct', () => {
49+
it('no items', () => {
50+
const ring = new RingBuffer<number>(4);
51+
expect(ring.isEmpty()).to.equal(true, 'no items isEmpty should be true');
52+
});
53+
54+
it('length is right 1', () => {
55+
const ring = new RingBuffer<number>(4);
56+
ring.insert(0);
57+
expect(ring.isEmpty()).to.equal(false, '1 item isEmpty should be false');
58+
});
59+
60+
it('length is right 4', () => {
61+
const ring = new RingBuffer<number>(4);
62+
ring.insert(0);
63+
ring.insert(1);
64+
ring.insert(2);
65+
ring.insert(3);
66+
expect(ring.isEmpty()).to.equal(false, '4 items isEmpty should be false');
67+
});
68+
69+
it('more than capacity', () => {
70+
const ring = new RingBuffer<number>(4);
71+
ring.insert(0);
72+
ring.insert(1);
73+
ring.insert(2);
74+
ring.insert(3);
75+
ring.insert(4);
76+
expect(ring.isEmpty()).to.equal(false, '5 item isEmpty should be false');
77+
});
78+
});
79+
4380
it('items are removed in order 1', () => {
4481
const ring = new RingBuffer<number>(4);
45-
ring.add(0);
46-
ring.add(1);
47-
ring.add(2);
48-
ring.add(3);
49-
ring.add(4);
82+
ring.insert(0);
83+
ring.insert(1);
84+
ring.insert(2);
85+
ring.insert(3);
86+
ring.insert(4);
5087
expect(ring.has(0)).to.equal(false, '0 should not be there anymore');
5188
expect(ring.has(1)).to.equal(true, '1 should still be there');
5289
expect(ring.has(2)).to.equal(true, '2 should still be there');
@@ -56,11 +93,11 @@ describe('RingBuffer Utils', () => {
5693

5794
it('two times the same items can exist', () => {
5895
const ring = new RingBuffer<number>(4);
59-
ring.add(0);
60-
ring.add(1);
61-
ring.add(2);
62-
ring.add(1);
63-
ring.add(4);
96+
ring.insert(0);
97+
ring.insert(1);
98+
ring.insert(2);
99+
ring.insert(1);
100+
ring.insert(4);
64101
expect(ring.has(0)).to.equal(false, '0 should not be there anymore');
65102
expect(ring.has(1)).to.equal(true, '1 should still be there');
66103
expect(ring.has(2)).to.equal(true, '2 should still be there');
@@ -70,14 +107,14 @@ describe('RingBuffer Utils', () => {
70107

71108
it('items are removed in order completely', () => {
72109
const ring = new RingBuffer<number>(4);
73-
ring.add(0);
74-
ring.add(1);
75-
ring.add(2);
76-
ring.add(3);
77-
ring.add(10);
78-
ring.add(20);
79-
ring.add(30);
80-
ring.add(40);
110+
ring.insert(0);
111+
ring.insert(1);
112+
ring.insert(2);
113+
ring.insert(3);
114+
ring.insert(10);
115+
ring.insert(20);
116+
ring.insert(30);
117+
ring.insert(40);
81118
expect(ring.has(0)).to.equal(false, '0 should not be there anymore');
82119
expect(ring.has(1)).to.equal(false, '1 should not be there');
83120
expect(ring.has(2)).to.equal(false, '2 should not be there');
@@ -92,15 +129,96 @@ describe('RingBuffer Utils', () => {
92129

93130
it('clear empties the list but keeps the capacity', () => {
94131
const ring = new RingBuffer<number>(4);
95-
ring.add(0);
96-
ring.add(1);
97-
ring.add(2);
98-
ring.add(1);
132+
ring.insert(0);
133+
ring.insert(1);
134+
ring.insert(2);
135+
ring.insert(1);
99136
expect(ring.getLength()).to.equal(4);
100137
expect(ring.getCapacity()).to.equal(4);
101138
ring.clear();
102139
expect(ring.getCapacity()).to.equal(4);
103140

104141
expect(ring.getLength()).to.equal(0);
105142
});
143+
144+
describe('toArray', () => {
145+
it('empty buffer', () => {
146+
const ring = new RingBuffer<number>(4);
147+
expect(ring.toArray()).to.deep.eq([]);
148+
});
149+
150+
it('with 1', () => {
151+
const ring = new RingBuffer<number>(4);
152+
ring.insert(0);
153+
154+
expect(ring.toArray()).to.deep.eq([0]);
155+
});
156+
157+
it('with 4', () => {
158+
const ring = new RingBuffer<number>(4);
159+
ring.insert(0);
160+
ring.insert(1);
161+
ring.insert(2);
162+
ring.insert(3);
163+
164+
expect(ring.toArray()).to.deep.eq([0, 1, 2, 3]);
165+
});
166+
167+
it('with 5', () => {
168+
const ring = new RingBuffer<number>(4);
169+
ring.insert(0);
170+
ring.insert(1);
171+
ring.insert(2);
172+
ring.insert(3);
173+
ring.insert(4);
174+
175+
expect(ring.toArray()).to.deep.eq([1, 2, 3, 4]);
176+
});
177+
178+
it('more than 2 full laps erasing data', () => {
179+
const ring = new RingBuffer<number>(4);
180+
ring.insert(0);
181+
ring.insert(1);
182+
ring.insert(2);
183+
ring.insert(3);
184+
ring.insert(4); // first lap first item
185+
ring.insert(5);
186+
ring.insert(6); // first item in toArray should be this one
187+
ring.insert(7);
188+
ring.insert(8); // second lap first item
189+
ring.insert(9);
190+
191+
expect(ring.toArray()).to.deep.eq([6, 7, 8, 9]);
192+
});
193+
});
194+
195+
describe('clear', () => {
196+
it('empty buffer', () => {
197+
const ring = new RingBuffer<number>(4);
198+
ring.clear();
199+
expect(ring.getCapacity()).to.deep.eq(4);
200+
expect(ring.getLength()).to.deep.eq(0);
201+
});
202+
203+
it('with 1', () => {
204+
const ring = new RingBuffer<number>(4);
205+
ring.insert(0);
206+
ring.clear();
207+
expect(ring.getCapacity()).to.deep.eq(4);
208+
expect(ring.getLength()).to.deep.eq(0);
209+
});
210+
211+
it('with 5', () => {
212+
const ring = new RingBuffer<number>(4);
213+
ring.insert(0);
214+
ring.insert(1);
215+
ring.insert(2);
216+
ring.insert(3);
217+
ring.insert(4);
218+
219+
ring.clear();
220+
expect(ring.getCapacity()).to.deep.eq(4);
221+
expect(ring.getLength()).to.deep.eq(0);
222+
});
223+
});
106224
});

0 commit comments

Comments
 (0)