From d136e81c8f2f434442f2332e6141ac853fbd8482 Mon Sep 17 00:00:00 2001 From: rgthree Date: Sat, 13 Jan 2024 14:04:12 -0500 Subject: [PATCH] Increase efficiency of Groups Muter/Bypasser calculations by abandoning LiteGraph's built in mechanism. --- src_web/comfyui/fast_groups_muter.ts | 45 +++++++++++++++++++++++++++- src_web/typings/litegraph.d.ts | 9 ++++-- web/comfyui/fast_groups_muter.js | 27 ++++++++++++++++- 3 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src_web/comfyui/fast_groups_muter.ts b/src_web/comfyui/fast_groups_muter.ts index a73251d..2225bf5 100644 --- a/src_web/comfyui/fast_groups_muter.ts +++ b/src_web/comfyui/fast_groups_muter.ts @@ -12,6 +12,7 @@ import { SerializedLGraphNode, IWidget, LGraphGroup, + Vector4, } from "../typings/litegraph.js"; import { fitString } from "./utils_canvas.js"; @@ -45,6 +46,8 @@ class FastGroupsService { private runScheduleTimeout: number|null = null; private runScheduleAnimation: number|null = null; + private cachedNodeBoundings: {[key: number]: Vector4}|null = null; + constructor() { // Don't need to do anything, wait until a signal. } @@ -104,12 +107,52 @@ class FastGroupsService { this.runScheduledForMs = null; } + /** + * Returns the boundings for all nodes on the graph, then clears it after a short delay. This is + * to increase efficiency by caching the nodes' boundings when multiple groups are on the page. + */ + getBoundingsForAllNodes() { + if (!this.cachedNodeBoundings) { + this.cachedNodeBoundings = {}; + for (const node of app.graph._nodes) { + this.cachedNodeBoundings[node.id] = node.getBounding(); + } + setTimeout(() => { + this.cachedNodeBoundings = null; + }, 50); + } + return this.cachedNodeBoundings; + } + + /** + * This overrides `LGraphGroup.prototype.recomputeInsideNodes` to be much more efficient when + * calculating for many groups at once (only compute all nodes once in `getBoundingsForAllNodes`). + */ + recomputeInsideNodesForGroup(group: LGraphGroup) { + const cachedBoundings = this.getBoundingsForAllNodes(); + const nodes = group.graph._nodes; + group._nodes.length = 0; + + for (const node of nodes) { + const node_bounding = cachedBoundings[node.id]; + if (!node_bounding || !LiteGraph.overlapBounding(group._bounding, node_bounding)) { + continue; + } + group._nodes.push(node); + } + } + + /** + * Everything goes through getGroupsUnsorted, so we only get groups once. However, LiteGraph's + * `recomputeInsideNodes` is inefficient when calling multiple groups (it iterates over all nodes + * each time). So, we'll do our own dang thing, once. + */ private getGroupsUnsorted(now: number) { const graph = app.graph as TLGraph; if (!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold) { this.groupsUnsorted = [...graph._groups]; for (const group of this.groupsUnsorted) { - group.recomputeInsideNodes(); + this.recomputeInsideNodesForGroup(group); (group as any)._rgthreeHasAnyActiveNode = group._nodes.some( (n) => n.mode === LiteGraph.ALWAYS, ); diff --git a/src_web/typings/litegraph.d.ts b/src_web/typings/litegraph.d.ts index e84aa53..18de36b 100644 --- a/src_web/typings/litegraph.d.ts +++ b/src_web/typings/litegraph.d.ts @@ -297,6 +297,7 @@ export const LiteGraph: { //@rgthree isValidConnection(type: string|string[], type: string|string[]):boolean; + overlapBounding(a: Vector4, b: Vector4) : boolean; createNode(type: string): T; /** Register a node class so it can be listed when the user wants to create a new one */ @@ -435,7 +436,8 @@ export declare class LGraph { starttime: number; status: typeof LGraph.STATUS_RUNNING | typeof LGraph.STATUS_STOPPED; - private _nodes: LGraphNode[]; + // @rgthree, remove private; it's not really private b/c it's javascript. + _nodes: LGraphNode[]; // @rgthree, remove private; it's not really private b/c it's javascript. _groups: LGraphGroup[]; private _nodes_by_id: Record; @@ -1166,7 +1168,8 @@ export type SerializedLGraphGroup = { }; export declare class LGraphGroup { title: string; - private _bounding: Vector4; + // @rgthree - mark unprivate + _bounding: Vector4; color: string; font: string; // @rgthree @@ -1175,6 +1178,8 @@ export declare class LGraphGroup { _pos: Vector2; // @rgthree _size: Vector2; + // @rgthree + graph: LGraph; configure(o: SerializedLGraphGroup): void; diff --git a/web/comfyui/fast_groups_muter.js b/web/comfyui/fast_groups_muter.js index e42bf88..531bba4 100644 --- a/web/comfyui/fast_groups_muter.js +++ b/web/comfyui/fast_groups_muter.js @@ -21,6 +21,7 @@ class FastGroupsService { this.runScheduledForMs = null; this.runScheduleTimeout = null; this.runScheduleAnimation = null; + this.cachedNodeBoundings = null; } addFastGroupNode(node) { this.fastGroupNodes.push(node); @@ -67,12 +68,36 @@ class FastGroupsService { this.runScheduleAnimation = null; this.runScheduledForMs = null; } + getBoundingsForAllNodes() { + if (!this.cachedNodeBoundings) { + this.cachedNodeBoundings = {}; + for (const node of app.graph._nodes) { + this.cachedNodeBoundings[node.id] = node.getBounding(); + } + setTimeout(() => { + this.cachedNodeBoundings = null; + }, 50); + } + return this.cachedNodeBoundings; + } + recomputeInsideNodesForGroup(group) { + const cachedBoundings = this.getBoundingsForAllNodes(); + const nodes = group.graph._nodes; + group._nodes.length = 0; + for (const node of nodes) { + const node_bounding = cachedBoundings[node.id]; + if (!node_bounding || !LiteGraph.overlapBounding(group._bounding, node_bounding)) { + continue; + } + group._nodes.push(node); + } + } getGroupsUnsorted(now) { const graph = app.graph; if (!this.groupsUnsorted.length || now - this.msLastUnsorted > this.msThreshold) { this.groupsUnsorted = [...graph._groups]; for (const group of this.groupsUnsorted) { - group.recomputeInsideNodes(); + this.recomputeInsideNodesForGroup(group); group._rgthreeHasAnyActiveNode = group._nodes.some((n) => n.mode === LiteGraph.ALWAYS); } this.msLastUnsorted = now;