feat: binary-heap event queue with stable tie-breaking
This commit is contained in:
@@ -0,0 +1,54 @@
|
|||||||
|
// Binary min-heap keyed by (time, insertion sequence).
|
||||||
|
export class EventQueue {
|
||||||
|
constructor() {
|
||||||
|
this.heap = [];
|
||||||
|
this.seq = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get size() {
|
||||||
|
return this.heap.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
push(time, type, data) {
|
||||||
|
const e = { time, seq: this.seq++, type, data };
|
||||||
|
const h = this.heap;
|
||||||
|
h.push(e);
|
||||||
|
let i = h.length - 1;
|
||||||
|
while (i > 0) {
|
||||||
|
const p = (i - 1) >> 1;
|
||||||
|
if (!before(h[i], h[p])) break;
|
||||||
|
[h[i], h[p]] = [h[p], h[i]];
|
||||||
|
i = p;
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
peek() {
|
||||||
|
return this.heap[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
pop() {
|
||||||
|
const h = this.heap;
|
||||||
|
if (h.length === 0) return null;
|
||||||
|
const top = h[0];
|
||||||
|
const last = h.pop();
|
||||||
|
if (h.length > 0) {
|
||||||
|
h[0] = last;
|
||||||
|
let i = 0;
|
||||||
|
for (;;) {
|
||||||
|
const l = 2 * i + 1, r = 2 * i + 2;
|
||||||
|
let m = i;
|
||||||
|
if (l < h.length && before(h[l], h[m])) m = l;
|
||||||
|
if (r < h.length && before(h[r], h[m])) m = r;
|
||||||
|
if (m === i) break;
|
||||||
|
[h[i], h[m]] = [h[m], h[i]];
|
||||||
|
i = m;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function before(a, b) {
|
||||||
|
return a.time < b.time || (a.time === b.time && a.seq < b.seq);
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { EventQueue } from '../src/sim/events.js';
|
||||||
|
|
||||||
|
describe('EventQueue', () => {
|
||||||
|
it('pops events in time order regardless of insertion order', () => {
|
||||||
|
const q = new EventQueue();
|
||||||
|
q.push(5, 'c', {});
|
||||||
|
q.push(1, 'a', {});
|
||||||
|
q.push(3, 'b', {});
|
||||||
|
expect(q.pop().type).toBe('a');
|
||||||
|
expect(q.pop().type).toBe('b');
|
||||||
|
expect(q.pop().type).toBe('c');
|
||||||
|
expect(q.pop()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('breaks time ties by insertion order', () => {
|
||||||
|
const q = new EventQueue();
|
||||||
|
q.push(2, 'first', {});
|
||||||
|
q.push(2, 'second', {});
|
||||||
|
expect(q.pop().type).toBe('first');
|
||||||
|
expect(q.pop().type).toBe('second');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('peek does not remove; size tracks contents', () => {
|
||||||
|
const q = new EventQueue();
|
||||||
|
q.push(1, 'a', { n: 7 });
|
||||||
|
expect(q.size).toBe(1);
|
||||||
|
expect(q.peek().time).toBe(1);
|
||||||
|
expect(q.size).toBe(1);
|
||||||
|
expect(q.pop().data.n).toBe(7);
|
||||||
|
expect(q.size).toBe(0);
|
||||||
|
expect(q.peek()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('survives a large randomized load in order', () => {
|
||||||
|
const q = new EventQueue();
|
||||||
|
const times = Array.from({ length: 500 }, (_, i) => (i * 7919) % 1000);
|
||||||
|
for (const t of times) q.push(t, 'e', {});
|
||||||
|
let last = -1;
|
||||||
|
while (q.size > 0) {
|
||||||
|
const e = q.pop();
|
||||||
|
expect(e.time).toBeGreaterThanOrEqual(last);
|
||||||
|
last = e.time;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user