Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Flow graph node caching #14453

Merged
merged 7 commits into from Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,17 +1,16 @@
import type { FlowGraphDataConnection } from "../../flowGraphDataConnection";
import { FlowGraphBlock } from "../../flowGraphBlock";
import type { RichType } from "../../flowGraphRichTypes";
import type { FlowGraphContext } from "../../flowGraphContext";
import type { IFlowGraphBlockConfiguration } from "../../flowGraphBlock";
import { FlowGraphCachedOperationBlock } from "./flowGraphCachedOperationBlock";
/**
* @experimental
* The base block for all binary operation blocks. Receives an input of type
* LeftT, one of type RightT, and outputs a value of type ResultT.
*/
export class FlowGraphBinaryOperationBlock<LeftT, RightT, ResultT> extends FlowGraphBlock {
export class FlowGraphBinaryOperationBlock<LeftT, RightT, ResultT> extends FlowGraphCachedOperationBlock<ResultT> {
leftInput: FlowGraphDataConnection<LeftT>;
rightInput: FlowGraphDataConnection<RightT>;
output: FlowGraphDataConnection<ResultT>;

constructor(
leftRichType: RichType<LeftT>,
Expand All @@ -21,14 +20,13 @@ export class FlowGraphBinaryOperationBlock<LeftT, RightT, ResultT> extends FlowG
private _className: string,
config?: IFlowGraphBlockConfiguration
) {
super(config);
super(resultRichType, config);
this.leftInput = this._registerDataInput("leftInput", leftRichType);
this.rightInput = this._registerDataInput("rightInput", rightRichType);
this.output = this._registerDataOutput("Output", resultRichType);
}

public _updateOutputs(_context: FlowGraphContext): void {
this.output.setValue(this._operation(this.leftInput.getValue(_context), this.rightInput.getValue(_context)), _context);
public override _doOperation(context: FlowGraphContext): ResultT {
return this._operation(this.leftInput.getValue(context), this.rightInput.getValue(context));
}

public getClassName(): string {
Expand Down
@@ -0,0 +1,44 @@
import type { IFlowGraphBlockConfiguration } from "../../flowGraphBlock";
import { FlowGraphBlock } from "../../flowGraphBlock";
import type { FlowGraphContext } from "../../flowGraphContext";
import type { FlowGraphDataConnection } from "../../flowGraphDataConnection";
import type { RichType } from "../../flowGraphRichTypes";

const CACHE_NAME = "cachedOperationValue";
const CACHE_EXEC_ID_NAME = "cachedExecutionId";

/**
* @experimental
*/
export abstract class FlowGraphCachedOperationBlock<OutputT> extends FlowGraphBlock {
/**
* The output of the operation
*/
public readonly output: FlowGraphDataConnection<OutputT>;

constructor(outputRichType: RichType<OutputT>, config?: IFlowGraphBlockConfiguration) {
super(config);

this.output = this._registerDataOutput("output", outputRichType);
}

/**
* @internal
* Operation to realize
* @param context the graph context
*/
public abstract _doOperation(context: FlowGraphContext): OutputT;
carolhmj marked this conversation as resolved.
Show resolved Hide resolved

public _updateOutputs(context: FlowGraphContext) {
const cachedExecutionId = context._getExecutionVariable(this, CACHE_EXEC_ID_NAME);
const cachedValue = context._getExecutionVariable(this, CACHE_NAME);
if (cachedValue !== undefined && cachedExecutionId === context.executionId) {
this.output.setValue(cachedValue, context);
} else {
const calculatedValue = this._doOperation(context);
context._setExecutionVariable(this, CACHE_NAME, calculatedValue);
context._setExecutionVariable(this, CACHE_EXEC_ID_NAME, context.executionId);
this.output.setValue(calculatedValue, context);
}
}
}
@@ -1,22 +1,18 @@
import type { FlowGraphContext } from "../../flowGraphContext";
import { FlowGraphBlock } from "../../flowGraphBlock";
import type { FlowGraphDataConnection } from "../../flowGraphDataConnection";
import type { RichType } from "../../flowGraphRichTypes";
import type { IFlowGraphBlockConfiguration } from "../../flowGraphBlock";
import { FlowGraphCachedOperationBlock } from "./flowGraphCachedOperationBlock";
/**
* @experimental
* Block that outputs a value of type ResultT, resulting of an operation with no inputs.
*/
export class FlowGraphConstantOperationBlock<ResultT> extends FlowGraphBlock {
public output: FlowGraphDataConnection<ResultT>;

export class FlowGraphConstantOperationBlock<ResultT> extends FlowGraphCachedOperationBlock<ResultT> {
constructor(richType: RichType<ResultT>, private _operation: () => ResultT, private _className: string, config?: IFlowGraphBlockConfiguration) {
super(config);
this.output = this._registerDataOutput("output", richType);
super(richType, config);
}

public _updateOutputs(context: FlowGraphContext): void {
this.output.setValue(this._operation(), context);
public override _doOperation(_context: FlowGraphContext): ResultT {
return this._operation();
}

public getClassName(): string {
Expand Down
@@ -1,16 +1,15 @@
import type { FlowGraphDataConnection } from "../../flowGraphDataConnection";
import type { IFlowGraphBlockConfiguration } from "../../flowGraphBlock";
import { FlowGraphBlock } from "../../flowGraphBlock";
import type { RichType } from "../../flowGraphRichTypes";
import type { FlowGraphContext } from "../../flowGraphContext";
import { FlowGraphCachedOperationBlock } from "./flowGraphCachedOperationBlock";

/**
* @experimental
* The base block for all unary operation blocks. Receives an input of type InputT, and outputs a value of type ResultT.
*/
export class FlowGraphUnaryOperationBlock<InputT, ResultT> extends FlowGraphBlock {
export class FlowGraphUnaryOperationBlock<InputT, ResultT> extends FlowGraphCachedOperationBlock<ResultT> {
input: FlowGraphDataConnection<InputT>;
output: FlowGraphDataConnection<ResultT>;

constructor(
inputRichType: RichType<InputT>,
Expand All @@ -19,13 +18,11 @@ export class FlowGraphUnaryOperationBlock<InputT, ResultT> extends FlowGraphBloc
private _className: string,
config?: IFlowGraphBlockConfiguration
) {
super(config);
super(resultRichType, config);
this.input = this._registerDataInput("input", inputRichType);
this.output = this._registerDataOutput("resultOutput", resultRichType);
}

public _updateOutputs(_context: FlowGraphContext): void {
this.output.setValue(this._operation(this.input.getValue(_context)), _context);
public override _doOperation(context: FlowGraphContext): ResultT {
return this._operation(this.input.getValue(context));
}

public getClassName(): string {
Expand Down
16 changes: 16 additions & 0 deletions packages/dev/core/src/FlowGraph/flowGraphContext.ts
Expand Up @@ -90,6 +90,11 @@ export class FlowGraphContext {
* These are blocks that have currently pending tasks/listeners that need to be cleaned up.
*/
private _pendingBlocks: FlowGraphAsyncExecutionBlock[] = [];
/**
* A monotonically increasing ID for each execution.
* Incremented for every block executed.
*/
private _executionId = 0;

constructor(params: IFlowGraphContextConfiguration) {
this._configuration = params;
Expand Down Expand Up @@ -243,6 +248,17 @@ export class FlowGraphContext {
this._pendingBlocks.length = 0;
}

/**
* @internal
*/
public _increaseExecutionId() {
this._executionId++;
}

public get executionId() {
return this._executionId;
}

/**
* Serializes a context
* @param serializationObject the object to write the values in
Expand Down
Expand Up @@ -24,6 +24,7 @@ export class FlowGraphSignalConnection extends FlowGraphConnection<FlowGraphExec
public _activateSignal(context: FlowGraphContext): void {
if (this.connectionType === FlowGraphConnectionType.Input) {
this._ownerBlock._execute(context, this);
context._increaseExecutionId();
} else {
this._connectedPoint[0]?._activateSignal(context);
}
Expand Down
51 changes: 50 additions & 1 deletion packages/dev/core/test/unit/FlowGraph/flowGraphDataNodes.test.ts
@@ -1,7 +1,14 @@
import type { Engine } from "core/Engines";
import { NullEngine } from "core/Engines";
import type { FlowGraph, FlowGraphContext } from "core/FlowGraph";
import { FlowGraphCoordinator, FlowGraphGetVariableBlock, FlowGraphSceneReadyEventBlock, FlowGraphLogBlock } from "core/FlowGraph";
import {
FlowGraphCoordinator,
FlowGraphGetVariableBlock,
FlowGraphSceneReadyEventBlock,
FlowGraphLogBlock,
FlowGraphAddNumberBlock,
FlowGraphRandomNumberBlock,
} from "core/FlowGraph";
import { Scene } from "core/scene";

describe("Flow Graph Data Nodes", () => {
Expand Down Expand Up @@ -51,4 +58,46 @@ describe("Flow Graph Data Nodes", () => {
expect(console.log).toHaveBeenCalledWith(42);
expect(console.log).toHaveBeenCalledWith(43);
});

it("Values are cached for the same execution id", () => {
const sceneReady = new FlowGraphSceneReadyEventBlock({ name: "SceneReady" });
flowGraph.addEventBlock(sceneReady);

const add = new FlowGraphAddNumberBlock();

const rnd = new FlowGraphRandomNumberBlock();
rnd.leftInput.setValue(0, flowGraphContext);
rnd.rightInput.setValue(1, flowGraphContext);

// add a number to itself, which should only trigger the random number block once and cache the result
add.leftInput.connectTo(rnd.output);
add.rightInput.connectTo(rnd.output);

// log ther result
const log = new FlowGraphLogBlock();
log.message.connectTo(add.output);
sceneReady.onDone.connectTo(log.onStart);

flowGraph.start();

let mockRandomIndex = 1;
const mockedRandom = (): number => {
return mockRandomIndex++;
};

// clear the random mock before calling
const random = jest.spyOn(global.Math, "random").mockImplementation(mockedRandom);

scene.onReadyObservable.notifyObservers(scene);

expect(random).toHaveBeenCalledTimes(1);
expect(console.log).toHaveBeenCalledWith(2); // 1 + 1

random.mockRestore();
});

afterEach(() => {
scene.dispose();
engine.dispose();
});
});
Expand Up @@ -155,7 +155,6 @@ describe("Flow Graph Execution Nodes", () => {
const sceneReady = new FlowGraphSceneReadyEventBlock();
flowGraph.addEventBlock(sceneReady);

debugger;
const switchBlock = new FlowGraphSwitchBlock({ cases: [1, 2, 3] });
sceneReady.onDone.connectTo(switchBlock.onStart);
switchBlock.selection.setValue(2, flowGraphContext);
Expand Down