Add zero-sleep execution with debounced rendering (#12)
This commit is contained in:
parent
12a318dcbd
commit
5d4921432a
@ -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;
|
||||
|
@ -38,7 +38,7 @@ const IntervalInput = (props: {
|
||||
return (
|
||||
<div style={styles.inputWrapper}>
|
||||
<NumericInput
|
||||
min={5}
|
||||
min={0}
|
||||
stepSize={5}
|
||||
defaultValue={20}
|
||||
minorStepSize={null}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user