Add zero-sleep execution with debounced rendering (#12)

This commit is contained in:
Nilay Majorwar 2024-04-02 01:19:07 +05:30 committed by GitHub
parent 12a318dcbd
commit 5d4921432a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 54 additions and 8 deletions

View File

@ -7,6 +7,8 @@ import {
WorkerRuntimeError,
} from "./worker-errors";
const FORCE_SLEEP_INTERVAL_MS = 10;
type ExecuteAllArgs<RS> = {
/** Interval between two execution steps, in milliseconds */
interval: number;
@ -143,12 +145,27 @@ class ExecutionController<RS> {
this._isPaused = false;
return new Promise(async (resolve, reject) => {
let lastSleepTime = performance.now();
while (true) {
try {
const doBreak = this.runExecLoopIteration();
onResult(this._result!);
if (doBreak) break;
await this.sleep(interval);
/**
* The event loop causes sleep to add quite a bit of overhead. But sleep is
* required for allowing other message handlers to be run during execution.
* So for interval=0, as a middle ground, we sleep after every X ms.
*/
const shouldSleep =
interval > 0 ||
performance.now() - lastSleepTime > FORCE_SLEEP_INTERVAL_MS;
if (shouldSleep) {
await this.sleep(interval);
lastSleepTime = performance.now();
}
} catch (error) {
if (isRuntimeError(error)) {
this._isPaused = true;

View File

@ -38,7 +38,7 @@ const IntervalInput = (props: {
return (
<div style={styles.inputWrapper}>
<NumericInput
min={5}
min={0}
stepSize={5}
defaultValue={20}
minorStepSize={null}

View File

@ -1,6 +1,8 @@
import React from "react";
import { LanguageProvider } from "../languages/types";
const RENDER_INTERVAL_MS = 50;
export interface RendererRef<RS> {
/** Update runtime state to renderer */
updateState: (state: RS | null) => void;
@ -9,19 +11,46 @@ export interface RendererRef<RS> {
/**
* React component that acts as an imperatively controller wrapper
* around the actual language renderer. This is to pull renderer state updates
* outside of Mainframe for performance reasons.
* outside of Mainframe and debouncing rerenders for performance reasons.
*/
const RendererWrapperComponent = <RS extends {}>(
{ renderer }: { renderer: LanguageProvider<RS>["Renderer"] },
ref: React.Ref<RendererRef<RS>>
) => {
const [state, setState] = React.useState<RS | null>(null);
const [renderedState, setRenderedState] = React.useState<RS | null>(null);
React.useImperativeHandle(ref, () => ({
updateState: setState,
}));
/**
* Re-rendering on each state update becomes a bottleneck for running code at high speed.
* The main browser thread will take too much time handling each message and the message queue
* will pile up, leading to very visible delay in actions like pause and stop.
*
* To avoid this, we do some debouncing here. We only truly re-render at every X ms: on
* other updates we just store the latest state in a ref.
*/
return renderer({ state });
// Timer for re-rendering with the latest state
const debounceTimerRef = React.useRef<NodeJS.Timeout | null>(null);
// Ref to store the very latest state, that may be rendered upto X ms later
const latestState = React.useRef<RS | null>(null);
/**
* Update the current renderer state. Debounces re-render by default,
* pass `immediate` as true to immediately re-render with the given state.
*/
const updateState = (state: RS | null) => {
latestState.current = state;
if (debounceTimerRef.current) return; // Timeout will automatically render liveState
// Set a timer to re-render with the latest result after X ms
debounceTimerRef.current = setTimeout(() => {
setRenderedState(latestState.current);
debounceTimerRef.current = null;
}, RENDER_INTERVAL_MS);
};
React.useImperativeHandle(ref, () => ({ updateState }));
return renderer({ state: renderedState });
};
export const RendererWrapper = React.forwardRef(RendererWrapperComponent);