Skip to content

Commit 727fa18

Browse files
committed
Use fixed-size circular buffer for queue
1 parent 8eddfb3 commit 727fa18

3 files changed

Lines changed: 137 additions & 30 deletions

File tree

src/draw.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,25 @@ type QueueEntry = {
9191
depth: number
9292
}
9393

94+
// We'll use a global queue to avoid reallocating it on every preview frame.
95+
const patternQueue = new Queue<QueueEntry>({
96+
initialItems: [],
97+
size: MAX_QUEUE_SIZE,
98+
})
99+
94100
function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
95101
console.log(
96102
`generateDrawQueue start. Initial screens: ${state.screens.length}, Patterns: ${state.patterns.length}`
97103
)
98104

99-
const patternQueue = new Queue<QueueEntry>(
100-
state.screens.map(screen => ({
105+
patternQueue.clear()
106+
107+
for (const screen of state.screens) {
108+
patternQueue.push({
101109
currentPattern: screen,
102110
depth: 0,
103-
}))
104-
)
111+
})
112+
}
105113

106114
let iterations = 0
107115
while (patternQueue.size > 0) {
@@ -127,14 +135,15 @@ function* generateDrawQueue(state: State): Generator<QueueEntry, void, void> {
127135
currentPattern: virtualScreen,
128136
depth: entry.depth + 1,
129137
})
138+
139+
// Give up if queue becomes too large.
140+
if (patternQueue.size >= MAX_QUEUE_SIZE) {
141+
console.warn('Maximum queue size reached. Rendering cancelled.')
142+
return
143+
}
130144
}
131145
}
132146

133-
// Give up if queue becomes too large.
134-
if (patternQueue.size > MAX_QUEUE_SIZE) {
135-
console.warn('Maximum queue size reached. Rendering cancelled.')
136-
return
137-
}
138147
}
139148

140149
console.log(

src/queue.test.ts

Lines changed: 86 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,34 @@ import { Queue } from './queue';
55

66
describe('Queue', () => {
77
it('should initialize an empty queue', () => {
8-
const queue = new Queue<number>();
8+
const queue = new Queue<number>({ size: 5 });
99
expect(queue.size).toBe(0);
1010
});
1111

1212
it('should initialize a queue with initial items', () => {
13-
const queue = new Queue<number>([1, 2, 3]);
13+
const queue = new Queue<number>({ initialItems: [1, 2, 3], size: 3 });
1414
expect(queue.size).toBe(3);
1515
});
1616

17-
it('should push items to the queue', () => {
18-
const queue = new Queue<string>();
17+
it('should throw an error if initial items exceed capacity', () => {
18+
expect(() => new Queue<number>({ initialItems: [1, 2, 3, 4], size: 3 })).toThrow('Initial items exceed queue capacity');
19+
});
20+
21+
it('should push items to the queue up to its capacity', () => {
22+
const queue = new Queue<string>({ size: 2 });
1923
queue.push('a');
2024
expect(queue.size).toBe(1);
2125
queue.push('b');
2226
expect(queue.size).toBe(2);
2327
});
2428

29+
it('should throw an error when pushing to a full queue', () => {
30+
const queue = new Queue<string>({ initialItems: ['a'], size: 1 });
31+
expect(() => queue.push('b')).toThrow('Queue is full');
32+
});
33+
2534
it('should shift items from the queue in FIFO order', () => {
26-
const queue = new Queue<number>([1, 2, 3]);
35+
const queue = new Queue<number>({ initialItems: [1, 2, 3], size: 3 });
2736
expect(queue.shift()).toBe(1);
2837
expect(queue.size).toBe(2);
2938
expect(queue.shift()).toBe(2);
@@ -33,12 +42,12 @@ describe('Queue', () => {
3342
});
3443

3544
it('should throw an error when shifting from an empty queue', () => {
36-
const queue = new Queue<number>();
45+
const queue = new Queue<number>({ size: 5 });
3746
expect(() => queue.shift()).toThrow('Queue is empty');
3847
});
3948

4049
it('should handle a mix of push and shift operations correctly', () => {
41-
const queue = new Queue<string>();
50+
const queue = new Queue<string>({ size: 2 });
4251
queue.push('one');
4352
queue.push('two');
4453
expect(queue.shift()).toBe('one');
@@ -50,10 +59,23 @@ describe('Queue', () => {
5059
});
5160

5261
it('should report correct size after multiple pushes and shifts', () => {
53-
const queue = new Queue<number>([1, 2]);
62+
const queue = new Queue<number>({ initialItems: [1, 2], size: 5 });
5463
queue.push(3);
5564
expect(queue.size).toBe(3);
56-
queue.shift(); // 1
65+
queue.shift();
66+
expect(queue.size).toBe(2);
67+
queue.push(4);
68+
expect(queue.size).toBe(3);
69+
expect(queue.shift()).toBe(2);
70+
expect(queue.shift()).toBe(3);
71+
expect(queue.shift()).toBe(4);
72+
expect(queue.size).toBe(0);
73+
});
74+
75+
it('should allow pushing after shifting from a full queue', () => {
76+
const queue = new Queue<number>({ initialItems: [1, 2, 3], size: 3 });
77+
expect(queue.size).toBe(3);
78+
expect(queue.shift()).toBe(1);
5779
expect(queue.size).toBe(2);
5880
queue.push(4);
5981
expect(queue.size).toBe(3);
@@ -62,4 +84,59 @@ describe('Queue', () => {
6284
expect(queue.shift()).toBe(4);
6385
expect(queue.size).toBe(0);
6486
});
87+
88+
it('should throw if initializing with more items than queue size', () => {
89+
expect(() => new Queue<number>({ initialItems: [1, 2], size: 1 })).toThrow('Initial items exceed queue capacity');
90+
});
91+
92+
it('should throw if initializing with non-positive size', () => {
93+
expect(() => new Queue<number>({ size: 0 })).toThrow('Queue size must be positive');
94+
});
95+
96+
it('should handle operations on a queue of size 1', () => {
97+
const queue = new Queue<number>({ size: 1 });
98+
expect(queue.size).toBe(0);
99+
100+
queue.push(10);
101+
expect(queue.size).toBe(1);
102+
expect(() => queue.push(20)).toThrow('Queue is full');
103+
104+
expect(queue.shift()).toBe(10);
105+
expect(queue.size).toBe(0);
106+
expect(() => queue.shift()).toThrow('Queue is empty');
107+
108+
queue.push(30);
109+
expect(queue.size).toBe(1);
110+
expect(queue.shift()).toBe(30);
111+
});
112+
113+
it('should correctly handle fill-empty-fill-empty cycles (circular buffer integrity)', () => {
114+
const queue = new Queue<number>({ size: 3 });
115+
116+
// Fill 1
117+
queue.push(1);
118+
queue.push(2);
119+
queue.push(3);
120+
expect(queue.size).toBe(3);
121+
expect(() => queue.push(4)).toThrow('Queue is full');
122+
123+
// Empty 1
124+
expect(queue.shift()).toBe(1);
125+
expect(queue.shift()).toBe(2);
126+
expect(queue.shift()).toBe(3);
127+
expect(queue.size).toBe(0);
128+
expect(() => queue.shift()).toThrow('Queue is empty');
129+
130+
// Fill 2 (after wrapping)
131+
queue.push(10);
132+
queue.push(20);
133+
queue.push(30);
134+
expect(queue.size).toBe(3);
135+
136+
// Empty 2
137+
expect(queue.shift()).toBe(10);
138+
expect(queue.shift()).toBe(20);
139+
expect(queue.shift()).toBe(30);
140+
expect(queue.size).toBe(0);
141+
});
65142
});

src/queue.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,42 @@
1+
/**
2+
* A fixed-size queue, backed by a circular buffer.
3+
*/
14
export class Queue<T> {
2-
#queue: T[] = []
5+
#buffer: (T | undefined)[] = []
36
#head = 0
7+
#tail = 0
48
#size = 0
59

6-
constructor(items: T[] = []) {
7-
this.#queue = items
8-
this.#size = items.length
10+
constructor({initialItems = [], size}: { initialItems?: T[], size: number}) {
11+
if (size <= 0) {
12+
throw new Error('Queue size must be positive')
13+
}
14+
15+
if (initialItems.length > size) {
16+
throw new Error('Initial items exceed queue capacity')
17+
}
18+
19+
this.#buffer = new Array(size).fill(undefined)
20+
21+
for (let i = 0; i< initialItems.length; i++) {
22+
this.#buffer[i] = initialItems[i]
23+
}
24+
25+
this.#size = initialItems.length
26+
this.#tail = initialItems.length
927
}
1028

1129
get size() {
1230
return this.#size
1331
}
1432

1533
push(value: T) {
16-
this.#queue.push(value)
34+
if (this.#size === this.#buffer.length) {
35+
throw new Error('Queue is full')
36+
}
37+
38+
this.#buffer[this.#tail] = value
39+
this.#tail = (this.#tail + 1) % this.#buffer.length
1740
this.#size++
1841
}
1942

@@ -22,19 +45,17 @@ export class Queue<T> {
2245
throw new Error('Queue is empty')
2346
}
2447

25-
const value = this.#queue[this.#head]!
48+
const value = this.#buffer[this.#head]!
2649

27-
this.#head++
50+
this.#head = (this.#head + 1) % this.#buffer.length
2851
this.#size--
2952

3053
return value
3154
}
3255

33-
/**
34-
* Compact the queue to make sure it doesn't grow indefinitely.
35-
*/
36-
compact() {
37-
this.#queue = this.#queue.slice(this.#head)
56+
clear() {
3857
this.#head = 0
58+
this.#tail = 0
59+
this.#size = 0
3960
}
4061
}

0 commit comments

Comments
 (0)