feat: detect circular signal dependencies (#129)

This commit is contained in:
Jacob
2023-01-13 07:32:10 +01:00
committed by GitHub
parent 24f75cf7cd
commit 6fcdb41df9
15 changed files with 88 additions and 36 deletions

View File

@@ -14,6 +14,7 @@ export interface LogPayload {
level?: LogLevel;
message: string;
stack?: string;
remarks?: string;
object?: any;
durationMs?: number;
[K: string]: any;

View File

@@ -1,4 +1,4 @@
import {FullSceneDescription, Scene, SceneDescription} from './scenes';
import {FullSceneDescription, Scene} from './scenes';
import {Meta, Metadata} from './Meta';
import {EventDispatcher, ValueDispatcher} from './events';
import {CanvasColorSpace, CanvasOutputMimeType, Vector2} from './types';

View File

@@ -136,6 +136,7 @@ export abstract class GeneratorScene<T>
public async render(context: CanvasRenderingContext2D): Promise<void> {
let promises = DependencyContext.consumePromises();
let iterations = 0;
startScene(this);
do {
iterations++;
await Promise.all(promises.map(handle => handle.promise));
@@ -147,6 +148,7 @@ export abstract class GeneratorScene<T>
promises = DependencyContext.consumePromises();
} while (promises.length > 0 && iterations < 10);
endScene(this);
if (iterations > 1) {
this.project.logger.debug(`render iterations: ${iterations}`);

View File

@@ -1,4 +1,4 @@
import {useLogger} from '../utils';
import {errorToLog, useLogger} from '../utils';
import {DependencyContext} from './DependencyContext';
export interface Computed<TValue> {
@@ -30,8 +30,7 @@ export class ComputedContext<TValue> extends DependencyContext<any> {
this.last = this.factory(...args);
} catch (e: any) {
useLogger().error({
message: e.message,
stack: e.stack,
...errorToLog(e),
inspect: this.owner?.key,
});
}

View File

@@ -1,4 +1,5 @@
import {FlagDispatcher, Subscribable} from '../events';
import {DetailedError} from '../utils';
export interface PromiseHandle<T> {
promise: Promise<T>;
@@ -8,6 +9,7 @@ export interface PromiseHandle<T> {
}
export class DependencyContext<TOwner = void> {
protected static collectionSet = new Set<DependencyContext<any>>();
protected static collectionStack: DependencyContext<any>[] = [];
protected static promises: PromiseHandle<any>[] = [];
@@ -61,14 +63,24 @@ export class DependencyContext<TOwner = void> {
}
protected startCollecting() {
if (DependencyContext.collectionSet.has(this)) {
throw new DetailedError(
'A circular dependency occurred between signals.',
`This can happen when signals reference each other in a loop.
Try using the attached stack trace to locate said loop.`,
);
}
this.stack = new Error().stack;
DependencyContext.collectionSet.add(this);
DependencyContext.collectionStack.push(this);
}
protected finishCollecting() {
this.stack = undefined;
DependencyContext.collectionSet.delete(this);
if (DependencyContext.collectionStack.pop() !== this) {
throw new Error('collectStart/collectEnd was called out of order');
throw new Error('collectStart/collectEnd was called out of order.');
}
}

View File

@@ -4,7 +4,7 @@ import {
TimingFunction,
tween,
} from '../tweening';
import {useLogger} from '../utils';
import {errorToLog, useLogger} from '../utils';
import {ThreadGenerator} from '../threading';
import {run} from '../flow';
import {DependencyContext} from './DependencyContext';
@@ -133,8 +133,7 @@ export class SignalContext<
this.last = this.parse(this.current());
} catch (e: any) {
useLogger().error({
message: e.message,
stack: e.stack,
...errorToLog(e),
inspect: (<any>this.owner)?.key,
});
}

View File

@@ -0,0 +1,5 @@
export class DetailedError extends Error {
public constructor(message: string, public readonly remarks?: string) {
super(message);
}
}

View File

@@ -0,0 +1,9 @@
import {LogPayload} from '../Logger';
export function errorToLog(error: any): LogPayload {
return {
message: error.message,
stack: error.stack,
remarks: error.remarks,
};
}

View File

@@ -3,7 +3,7 @@ export function getContext(
): CanvasRenderingContext2D {
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Could not create a 2D context');
throw new Error('Could not create a 2D context.');
}
return context;
}

View File

@@ -5,6 +5,8 @@
*/
export * from './capitalize';
export * from './DetailedError';
export * from './errorToLog';
export * from './getContext';
export * from './range';
export * from './useProject';

View File

@@ -8,7 +8,7 @@ const sceneStack: Scene[] = [];
export function useScene(): Scene {
const scene = sceneStack.at(-1);
if (!scene) {
throw new Error('The scene is not available in the current context');
throw new Error('The scene is not available in the current context.');
}
return scene;
}
@@ -19,6 +19,6 @@ export function startScene(scene: Scene) {
export function endScene(scene: Scene) {
if (sceneStack.pop() !== scene) {
throw new Error('startScene/endScene was called out of order');
throw new Error('startScene/endScene was called out of order.');
}
}

View File

@@ -1,4 +1,5 @@
import type {Thread} from '../threading';
import {DetailedError} from '../utils';
const threadStack: Thread[] = [];
@@ -8,7 +9,11 @@ const threadStack: Thread[] = [];
export function useThread(): Thread {
const thread = threadStack.at(-1);
if (!thread) {
throw new Error('The thread is not available in the current context');
throw new DetailedError(
'The thread is not available in the current context.',
`<code>useThread()</code> can only be called from within generator functions.
It's not available during rendering.`,
);
}
return thread;
}
@@ -19,6 +24,6 @@ export function startThread(thread: Thread) {
export function endThread(thread: Thread) {
if (threadStack.pop() !== thread) {
throw new Error('startThread/endThread was called out of order');
throw new Error('startThread/endThread was called out of order.');
}
}

View File

@@ -24,6 +24,7 @@ $colors: (
border-left: 4px solid transparent;
background-color: var(--background-color-dark);
border-radius: 4px;
line-height: 24px;
padding: 8px;
overflow: hidden;
display: none;
@@ -38,7 +39,6 @@ $colors: (
.header {
display: flex;
align-items: flex-start;
line-height: 24px;
gap: 8px;
margin: 0;
@@ -53,35 +53,43 @@ $colors: (
.duration {
background-color: var(--surface-color);
line-height: 24px;
padding: 0 8px;
border-radius: 4px;
white-space: nowrap;
}
.remarks pre,
.code {
background-color: var(--surface-color);
border-radius: 4px;
line-height: 24px;
padding: 8px;
margin: 8px 0 0;
overflow-x: auto;
}
.stack {
margin-top: 8px;
padding: 8px;
overflow-x: auto;
margin-top: 8px;
--scrollbar-background: var(--background-color-dark);
}
.remarks {
margin-top: 16px;
.section {
margin: 16px 8px 8px;
p {
margin: 8px;
pre {
background-color: var(--surface-color);
border-radius: 4px;
padding: 8px;
margin: 8px -8px;
overflow-x: auto;
}
code {
background-color: var(--surface-color);
padding: 2px 0;
border-radius: 4px;
}
a {
color: var(--theme);
text-decoration: none;
&:hover,
&:focus {
text-decoration: underline;
}
}
}

View File

@@ -60,15 +60,25 @@ export function Log({payload}: LogProps) {
</div>
{hasBody && open && (
<div>
{userEntry && <SourceCodeFrame entry={userEntry} />}
{entries && <StackTrace entries={entries} />}
{object && <pre className={styles.code}>{object}</pre>}
{payload.remarks && (
<div
className={styles.remarks}
className={clsx(styles.section, styles.remarks)}
dangerouslySetInnerHTML={{__html: payload.remarks}}
/>
)}
{object && (
<div className={styles.section}>
Related object:
<pre className={styles.code}>{object}</pre>
</div>
)}
{entries && (
<div className={styles.section}>
The problem occurred here:
{userEntry && <SourceCodeFrame entry={userEntry} />}
<StackTrace entries={entries} />
</div>
)}
</div>
)}
</div>

View File

@@ -194,7 +194,7 @@ export default ({
` config.name = '${name}';`,
` config.logger.warn({`,
` message: 'A project instance was exported instead of a project factory.',`,
` remarks: \`<p>Use the "makeProject()" function instead:</p><pre>import {makeProject} from '@motion-canvas/core';\nexport default makeProject({\n // Configuration and scenes go here.\n});</pre>\`,`,
` remarks: \`Use the <code>makeProject()</code> function instead:<pre>import {makeProject} from '@motion-canvas/core';\nexport default makeProject({\n // Configuration and scenes go here.\n});</pre>\`,`,
` stack: config.creationStack,`,
` });`,
` return config;`,