Skip to content

Commit c675c2a

Browse files
committed
perf(demo): improve main cpu loop performance
1 parent f86ab12 commit c675c2a

File tree

3 files changed

+103
-16
lines changed

3 files changed

+103
-16
lines changed

demo/src/execute.ts

+12-16
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
usart0Config
1212
} from 'avr8js';
1313
import { loadHex } from './intelhex';
14+
import { MicroTaskScheduler } from './task-scheduler';
1415

1516
// ATmega328p params
1617
const FLASH = 0x8000;
@@ -24,8 +25,8 @@ export class AVRRunner {
2425
readonly portD: AVRIOPort;
2526
readonly usart: AVRUSART;
2627
readonly speed = 16e6; // 16 MHZ
27-
28-
private stopped = false;
28+
readonly workUnitCycles = 500000;
29+
readonly taskScheduler = new MicroTaskScheduler();
2930

3031
constructor(hex: string) {
3132
loadHex(hex, new Uint8Array(this.program.buffer));
@@ -35,28 +36,23 @@ export class AVRRunner {
3536
this.portC = new AVRIOPort(this.cpu, portCConfig);
3637
this.portD = new AVRIOPort(this.cpu, portDConfig);
3738
this.usart = new AVRUSART(this.cpu, usart0Config, this.speed);
39+
this.taskScheduler.start();
3840
}
3941

40-
async execute(callback: (cpu: CPU) => void) {
41-
this.stopped = false;
42-
const workUnitCycles = 500000;
43-
let nextTick = this.cpu.cycles + workUnitCycles;
44-
for (;;) {
42+
// CPU main loop
43+
execute(callback: (cpu: CPU) => void) {
44+
const cyclesToRun = this.cpu.cycles + this.workUnitCycles;
45+
while (this.cpu.cycles < cyclesToRun) {
4546
avrInstruction(this.cpu);
4647
this.timer.tick();
4748
this.usart.tick();
48-
if (this.cpu.cycles >= nextTick) {
49-
callback(this.cpu);
50-
await new Promise((resolve) => setTimeout(resolve, 0));
51-
if (this.stopped) {
52-
break;
53-
}
54-
nextTick += workUnitCycles;
55-
}
5649
}
50+
51+
callback(this.cpu);
52+
this.taskScheduler.postTask(() => this.execute(callback));
5753
}
5854

5955
stop() {
60-
this.stopped = true;
56+
this.taskScheduler.stop();
6157
}
6258
}

demo/src/task-scheduler.spec.ts

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
/// <reference lib="dom" />
5+
6+
import { MicroTaskScheduler } from './task-scheduler';
7+
8+
describe('task-scheduler', () => {
9+
let taskScheduler: MicroTaskScheduler;
10+
let task: jest.Mock;
11+
12+
beforeEach(() => {
13+
taskScheduler = new MicroTaskScheduler();
14+
task = jest.fn();
15+
});
16+
17+
it('should execute task', async () => {
18+
taskScheduler.start();
19+
taskScheduler.postTask(task);
20+
await new Promise((resolve) => setTimeout(resolve, 0));
21+
expect(task).toHaveBeenCalledTimes(1);
22+
});
23+
24+
it('should execute task twice when posted twice', async () => {
25+
taskScheduler.start();
26+
taskScheduler.postTask(task);
27+
taskScheduler.postTask(task);
28+
await new Promise((resolve) => setTimeout(resolve, 0));
29+
expect(task).toHaveBeenCalledTimes(2);
30+
});
31+
32+
it('should not execute task when not started', async () => {
33+
taskScheduler.postTask(task);
34+
await new Promise((resolve) => setTimeout(resolve, 0));
35+
expect(task).not.toHaveBeenCalled();
36+
});
37+
38+
it('should not execute task when stopped', async () => {
39+
taskScheduler.start();
40+
taskScheduler.stop();
41+
taskScheduler.postTask(task);
42+
await new Promise((resolve) => setTimeout(resolve, 0));
43+
expect(task).not.toHaveBeenCalled();
44+
});
45+
46+
it('should not register listener twice', async () => {
47+
const addEventListenerSpy = jest.spyOn(window, 'addEventListener');
48+
taskScheduler.start();
49+
taskScheduler.start();
50+
expect(addEventListenerSpy).toHaveBeenCalledTimes(1);
51+
});
52+
});

demo/src/task-scheduler.ts

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
// Faster setTimeout(fn, 0) implementation using postMessage API
2+
// Based on https://dbaron.org/log/20100309-faster-timeouts
3+
export type IMicroTaskCallback = () => void;
4+
5+
export class MicroTaskScheduler {
6+
readonly messageName = 'zero-timeout-message';
7+
8+
private executionQueue: Array<IMicroTaskCallback> = [];
9+
private stopped = true;
10+
11+
start() {
12+
if (this.stopped) {
13+
this.stopped = false;
14+
window.addEventListener('message', this.handleMessage, true);
15+
}
16+
}
17+
18+
stop() {
19+
this.stopped = true;
20+
window.removeEventListener('message', this.handleMessage, true);
21+
}
22+
23+
postTask(fn: IMicroTaskCallback) {
24+
if (!this.stopped) {
25+
this.executionQueue.push(fn);
26+
window.postMessage(this.messageName, '*');
27+
}
28+
}
29+
30+
private handleMessage = (event: MessageEvent) => {
31+
if (event.data === this.messageName) {
32+
event.stopPropagation();
33+
const executeJob = this.executionQueue.shift();
34+
if (executeJob !== undefined) {
35+
executeJob();
36+
}
37+
}
38+
};
39+
}

0 commit comments

Comments
 (0)