Skip to content

Commit 968580e

Browse files
committed
refactor Timer
+ simpler constructor parameter options + support scheduler.postTask Signed-off-by: 🕷️ <[email protected]>
1 parent 601b2f2 commit 968580e

File tree

10 files changed

+199
-127
lines changed

10 files changed

+199
-127
lines changed

src/api-monitor-cs-main.ts

Lines changed: 30 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { EMsg, windowListen, windowPost } from './api/communication.ts';
22
import { TELEMETRY_FREQUENCY_1PS } from './api/const.ts';
3-
import { adjustTelemetryDelay, Timer } from './api/time.ts';
3+
import { adjustTelemetryDelay, ETimer, Timer } from './api/time.ts';
44
import {
55
applyConfig,
66
applySession,
@@ -14,36 +14,42 @@ import diff from './api/diff.ts';
1414

1515
let originalMetrics: TTelemetry | null;
1616
let currentMetrics: TTelemetry | null;
17-
const eachSecond = new Timer({ delay: 1e3, repetitive: true }, onEachSecond);
18-
const tick = new Timer(
19-
{ delay: TELEMETRY_FREQUENCY_1PS, repetitive: false },
20-
function apiMonitorTelemetryTick() {
21-
const now = Date.now();
22-
currentMetrics = structuredClone(collectMetrics());
17+
const eachSecond = new Timer(
18+
{ type: ETimer.TIMEOUT, delay: 1e3 },
19+
() => {
20+
onEachSecond();
21+
eachSecond.start();
22+
},
23+
);
24+
const tick = new Timer({
25+
type: ETimer.TIMEOUT,
26+
delay: TELEMETRY_FREQUENCY_1PS,
27+
}, function apiMonitorTelemetryTick() {
28+
const now = Date.now();
29+
currentMetrics = structuredClone(collectMetrics());
30+
31+
if (!originalMetrics) {
32+
originalMetrics = currentMetrics;
2333

24-
if (!originalMetrics) {
25-
originalMetrics = currentMetrics;
34+
windowPost({
35+
msg: EMsg.TELEMETRY,
36+
timeOfCollection: now,
37+
telemetry: originalMetrics,
38+
});
39+
} else {
40+
const delta = diff.diff(originalMetrics, currentMetrics);
2641

42+
if (delta) {
2743
windowPost({
28-
msg: EMsg.TELEMETRY,
44+
msg: EMsg.TELEMETRY_DELTA,
2945
timeOfCollection: now,
30-
telemetry: originalMetrics,
46+
telemetryDelta: delta,
3147
});
3248
} else {
33-
const delta = diff.diff(originalMetrics, currentMetrics);
34-
35-
if (delta) {
36-
windowPost({
37-
msg: EMsg.TELEMETRY_DELTA,
38-
timeOfCollection: now,
39-
telemetryDelta: delta,
40-
});
41-
} else {
42-
tick.start();
43-
}
49+
tick.start();
4450
}
45-
},
46-
);
51+
}
52+
});
4753

4854
windowListen((o) => {
4955
if (EMsg.TELEMETRY_ACKNOWLEDGED === o.msg) {

src/api/const.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { TWritableBooleanKeys } from './generics.ts';
22

3+
export function NOOP() {}
34
export const ERRORS_IGNORED = [
45
'Could not establish connection. Receiving end does not exist.',
56
'The message port closed before a response was received.',
@@ -24,6 +25,12 @@ export const requestAnimationFrame = /*@__PURE__*/ globalThis
2425
.requestAnimationFrame.bind(globalThis);
2526
export const cancelAnimationFrame = /*@__PURE__*/ globalThis
2627
.cancelAnimationFrame.bind(globalThis);
28+
export const nativeYield = /*@__PURE__*/ globalThis.scheduler.yield.bind(
29+
globalThis.scheduler,
30+
);
31+
export const nativePostTask = /*@__PURE__*/ globalThis.scheduler.postTask.bind(
32+
globalThis.scheduler,
33+
);
2734

2835
export const TAG_DELAY_NOT_FOUND = '';
2936
export const TAG_BAD_DELAY = (x: unknown) => `${x}`;

src/api/time.ts

Lines changed: 89 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {
22
cancelAnimationFrame,
33
clearTimeout,
4+
nativePostTask,
5+
NOOP,
46
requestAnimationFrame,
57
setTimeout,
68
TELEMETRY_FREQUENCY_30PS,
@@ -80,81 +82,106 @@ export class Stopper {
8082
}
8183
}
8284

83-
interface ITimerOptions {
84-
/** a delay of setTimeout or setInterval (default: 0); irrelevant if `animation` is true */
85-
delay?: number;
86-
/** act as setInterval by repeating setTimeout (default: false) */
87-
repetitive?: boolean;
88-
/** act as requestAnimationFrame called from another requestAnimationFrame (default: false);
89-
if true - `delay` is redundant */
90-
animation?: boolean;
85+
/** @link: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API#task_priorities */
86+
export type TTaskPriority =
87+
| 'user-blocking'
88+
| 'user-visible' // default
89+
| 'background';
90+
export enum ETimer {
91+
TIMEOUT,
92+
ANIMATION,
93+
TASK,
94+
}
95+
type TTimerMeasurable = {
9196
/** populate `callbackSelfTime` with measured execution time of `callback` (default: false) */
9297
measurable?: boolean;
93-
}
98+
};
99+
type TTimerTimeout = TTimerMeasurable & {
100+
type: ETimer.TIMEOUT;
101+
delay: number;
102+
};
103+
type TTimerAnimation = TTimerMeasurable & {
104+
type: ETimer.ANIMATION;
105+
};
106+
type TTimerTask = TTimerMeasurable & {
107+
type: ETimer.TASK;
108+
delay: number;
109+
priority?: TTaskPriority;
110+
};
111+
type TTimerOptions =
112+
| TTimerTimeout
113+
| TTimerAnimation
114+
| TTimerTask;
94115

95116
/**
96-
* A unification of ways to delay a callback to another time in javascript event-loop
97-
* - `repetitive: false` - will call `setTimeout` with constant `delay`.
98-
* - `repetitive: true` - will call `setTimeout` but act as `setInterval` with changeable `delay`.
99-
* - `animation: true` - will call `requestAnimationFrame` in recursive way (means to follow the browser's frame-rate).
100-
* - `measurable: true` - measure the callback's execution time.
117+
* A unification of ways to delay a callback execution
118+
* in javascript event-loop
101119
*/
102120
export class Timer {
103121
delay: number = 0;
104122
/** callback's self-time in milliseconds */
105123
callbackSelfTime: number = -1;
106124
#handler: number = 0;
125+
#abortController: AbortController | null = null;
107126
readonly #fn: (...args: unknown[]) => void;
108127
readonly #stopper?: Stopper;
109-
readonly #options: ITimerOptions;
110-
static readonly DEFAULT_OPTIONS: ITimerOptions = {
111-
delay: 0,
112-
repetitive: false,
113-
animation: false,
114-
measurable: false,
115-
};
128+
readonly #options: TTimerOptions;
116129

117-
constructor(o: ITimerOptions, fn: (...args: unknown[]) => void) {
118-
this.#options = Object.assign({}, Timer.DEFAULT_OPTIONS, o);
130+
constructor(o: TTimerOptions, fn: (...args: unknown[]) => void) {
131+
this.#options = Object.assign({}, o);
119132
this.#fn = fn;
120-
this.delay = this.#options.delay || 0;
133+
134+
if (
135+
this.#options.type === ETimer.TIMEOUT ||
136+
this.#options.type === ETimer.TASK
137+
) {
138+
this.delay = this.#options.delay;
139+
}
121140

122141
if (this.#options.measurable) {
123142
this.#stopper = new Stopper();
124143
}
125144
}
126145

127146
start(...args: unknown[]) {
128-
if (this.#handler) {
147+
if (this.isPending()) {
129148
this.stop();
130149
}
131150

132-
if (this.#options.animation) {
133-
this.#handler = requestAnimationFrame(() => {
151+
if (
152+
this.#options.type === ETimer.TIMEOUT
153+
) {
154+
this.#handler = setTimeout(() => {
155+
this.#handler = 0;
134156
this.trigger(...args);
157+
}, this.delay);
158+
} else if (
159+
this.#options.type === ETimer.ANIMATION
160+
) {
161+
this.#handler = requestAnimationFrame(() => {
135162
this.#handler = 0;
136-
137-
if (this.#options.repetitive) {
138-
this.start(...args);
139-
}
163+
this.trigger(...args);
140164
});
141-
} else {
142-
this.#handler = setTimeout(() => {
165+
} else if (this.#options.type === ETimer.TASK) {
166+
this.#abortController = new AbortController();
167+
nativePostTask(() => {
168+
this.#abortController = null;
143169
this.trigger(...args);
144-
this.#handler = 0;
145-
146-
if (this.#options.repetitive) {
147-
this.start(...args);
148-
}
149-
}, this.delay);
170+
}, {
171+
delay: this.delay,
172+
signal: this.#abortController.signal,
173+
priority: this.#options.priority,
174+
}).catch(NOOP);
150175
}
151176

152177
return this;
153178
}
154179

155180
trigger(...args: unknown[]) {
156181
this.#stopper?.start();
182+
157183
this.#fn(...args);
184+
158185
if (this.#stopper) {
159186
this.callbackSelfTime = this.#stopper.stop().value();
160187
}
@@ -163,21 +190,30 @@ export class Timer {
163190
}
164191

165192
stop() {
166-
if (this.#handler) {
167-
if (this.#options.animation) {
168-
cancelAnimationFrame(this.#handler);
169-
} else {
170-
clearTimeout(this.#handler);
171-
}
172-
193+
if (this.#options.type === ETimer.TIMEOUT) {
194+
this.#handler && clearTimeout(this.#handler);
195+
this.#handler = 0;
196+
} else if (this.#options.type === ETimer.ANIMATION) {
197+
this.#handler && cancelAnimationFrame(this.#handler);
173198
this.#handler = 0;
199+
} else if (this.#options.type === ETimer.TASK) {
200+
this.#abortController && this.#abortController.abort();
201+
this.#abortController = null;
174202
}
175203

176204
return this;
177205
}
178206

179-
/** Timer status: true | false => Pending | unstarted/finished/stopped */
180-
isPending() {
207+
/**
208+
* Timer status:
209+
* true := scheduled, pending execution
210+
* false := unstarted or finished
211+
*/
212+
isPending(): boolean {
213+
if (this.#options.type === ETimer.TASK) {
214+
return !!this.#abortController;
215+
}
216+
181217
return this.#handler !== 0;
182218
}
183219
}
@@ -190,20 +226,21 @@ export class Fps {
190226
/** registered number of calls */
191227
value = 0;
192228
#ticks = 0;
193-
#interval: Timer;
229+
#eachSecond: Timer;
194230

195231
constructor(callback?: (value: number) => void) {
196-
this.#interval = new Timer({ delay: 1e3, repetitive: true }, () => {
232+
this.#eachSecond = new Timer({ type: ETimer.TIMEOUT, delay: 1e3 }, () => {
197233
this.value = this.#ticks;
198234
this.#ticks = 0;
199235
callback?.(this.value);
236+
this.#eachSecond.start();
200237
});
201238
}
202239

203240
start() {
204241
this.#ticks = 0;
205242
this.value = 0;
206-
this.#interval.start();
243+
this.#eachSecond.start();
207244
return this;
208245
}
209246

@@ -213,7 +250,7 @@ export class Fps {
213250
}
214251

215252
stop() {
216-
this.#interval.stop();
253+
this.#eachSecond.stop();
217254
return this;
218255
}
219256
}

src/view/ConnectionAlert.svelte

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { EMsg, portPost, runtimeListen } from '../api/communication.ts';
3-
import { Timer } from '../api/time.ts';
3+
import { ETimer, Timer } from '../api/time.ts';
44
import Alert from './shared/Alert.svelte';
55
import {
66
INJECTION_ALERT_TIMEOUT,
@@ -12,16 +12,17 @@
1212
let tabReloadAlertEl: Alert | null = null;
1313
let devtoolsReloadAlertEl: Alert | null = null;
1414
const delayedAlert = new Timer(
15-
{ delay: INJECTION_ALERT_TIMEOUT },
15+
{ type: ETimer.TIMEOUT, delay: INJECTION_ALERT_TIMEOUT },
1616
() => void tabReloadAlertEl?.show(),
1717
);
1818
const extensionUpdateSensor = new Timer(
19-
{ delay: UPDATE_SENSOR_INTERVAL, repetitive: true },
19+
{ type: ETimer.TIMEOUT, delay: UPDATE_SENSOR_INTERVAL },
2020
() => {
2121
whenUpdateDetected(() => {
2222
devtoolsReloadAlertEl?.show();
2323
extensionUpdateSensor.stop();
2424
});
25+
extensionUpdateSensor.start();
2526
},
2627
);
2728

src/view/menu/SummaryBarItem.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import type { TPanel } from '../../api/storage/storage.local.ts';
33
import Variable from '../shared/Variable.svelte';
4-
import { Timer } from '../../api/time.ts';
4+
import { ETimer, Timer } from '../../api/time.ts';
55
import {
66
AFTER_SCROLL_ANIMATION_CLASSNAME,
77
SCROLLABLE_CLASSNAME,
@@ -22,7 +22,7 @@
2222
} = $props();
2323
let enabled: boolean = $derived.by(() => panel.visible && count > 0);
2424
const stopAnimate = new Timer(
25-
{ delay: 512 },
25+
{ type: ETimer.TIMEOUT, delay: 512 },
2626
(el: HTMLElement | unknown) => {
2727
if (el instanceof HTMLElement) {
2828
el.classList.remove(AFTER_SCROLL_ANIMATION_CLASSNAME);

src/view/panels/shared/CellSelfTimeStats.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { onMount, type Snippet } from 'svelte';
3-
import { Timer } from '../../../api/time.ts';
3+
import { ETimer, Timer } from '../../../api/time.ts';
44
import { Mean } from '../../../api/Mean.ts';
55
66
let {
@@ -16,7 +16,7 @@
1616
mean: time,
1717
max: time,
1818
});
19-
const eachSecond = new Timer({ delay: 1e3, repetitive: true }, () => {
19+
const eachSecond = new Timer({ type: ETimer.TIMEOUT, delay: 1e3 }, () => {
2020
if (!mean.samples) {
2121
return;
2222
}
@@ -26,6 +26,7 @@
2626
vs.max = mean.max;
2727
2828
mean.reset();
29+
eachSecond.start();
2930
});
3031
3132
$effect(() => void mean.add(time));

0 commit comments

Comments
 (0)