perf(ui): use rfdc for deep copying of objects

- Add and use more performant `deepClone` method for deep copying throughout the UI.

Benchmarks indicate the Really Fast Deep Clone library (`rfdc`) is the best all-around way to deep-clone large objects.

This is particularly relevant in canvas. When drawing or otherwise manipulating canvas objects, we need to do a lot of deep cloning of the canvas layer state objects.

Previously, we were using lodash's `cloneDeep`.

I did some fairly realistic benchmarks with a handful of deep-cloning algorithms/libraries (including the native `structuredClone`). I used a snapshot of the canvas state as the data to be copied:

On Chromium, `rfdc` is by far the fastest, over an order of magnitude faster than `cloneDeep`.

On FF, `fastest-json-copy` and `recursiveDeepCopy` are even faster, but are rather limited in data types. `rfdc`, while only half as fast as the former 2, is still nearly an order of magnitude faster than `cloneDeep`.

On Safari, `structuredClone` is the fastest, about 2x as fast as `cloneDeep`. `rfdc` is only 30% faster than `cloneDeep`.

`rfdc`'s peak memory usage is about 10% more than `cloneDeep` on Chrome. I couldn't get memory measurements from FF and Safari, but let's just assume the memory usage is similar relative to the other algos.

Overall, `rfdc` is the best choice for a single algo for all browsers. It's definitely the best for Chromium, by far the most popular desktop browser and thus our primary target.

A future enhancement might be to detect the browser and use that to determine which algorithm to use.
This commit is contained in:
psychedelicious
2024-04-02 22:59:07 +11:00
committed by Kent Keirsey
parent a6c91979af
commit 69ec14c7bb
14 changed files with 100 additions and 43 deletions

View File

@ -1,8 +1,9 @@
import { deepClone } from 'common/util/deepClone';
import { satisfies } from 'compare-versions';
import { NodeUpdateError } from 'features/nodes/types/error';
import type { InvocationNode, InvocationTemplate } from 'features/nodes/types/invocation';
import { zParsedSemver } from 'features/nodes/types/semver';
import { cloneDeep, defaultsDeep, keys, pick } from 'lodash-es';
import { defaultsDeep, keys, pick } from 'lodash-es';
import { buildInvocationNode } from './buildInvocationNode';
@ -50,7 +51,7 @@ export const updateNode = (node: InvocationNode, template: InvocationTemplate):
// The updateability of a node, via semver comparison, relies on the this kind of recursive merge
// being valid. We rely on the template's major version to be majorly incremented if this kind of
// merge would result in an invalid node.
const clone = cloneDeep(node);
const clone = deepClone(node);
clone.data.version = template.version;
defaultsDeep(clone, defaults); // mutates!

View File

@ -1,11 +1,12 @@
import { logger } from 'app/logging/logger';
import { deepClone } from 'common/util/deepClone';
import { parseify } from 'common/util/serialize';
import type { NodesState, WorkflowsState } from 'features/nodes/store/types';
import { isInvocationNode, isNotesNode } from 'features/nodes/types/invocation';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import { zWorkflowV3 } from 'features/nodes/types/workflow';
import i18n from 'i18n';
import { cloneDeep, pick } from 'lodash-es';
import { pick } from 'lodash-es';
import { fromZodError } from 'zod-validation-error';
export type BuildWorkflowArg = {
@ -30,7 +31,7 @@ const workflowKeys = [
type BuildWorkflowFunction = (arg: BuildWorkflowArg) => WorkflowV3;
export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflow }: BuildWorkflowArg): WorkflowV3 => {
const clonedWorkflow = pick(cloneDeep(workflow), workflowKeys);
const clonedWorkflow = pick(deepClone(workflow), workflowKeys);
const newWorkflow: WorkflowV3 = {
...clonedWorkflow,
@ -43,14 +44,14 @@ export const buildWorkflowFast: BuildWorkflowFunction = ({ nodes, edges, workflo
newWorkflow.nodes.push({
id: node.id,
type: node.type,
data: cloneDeep(node.data),
data: deepClone(node.data),
position: { ...node.position },
});
} else if (isNotesNode(node) && node.type) {
newWorkflow.nodes.push({
id: node.id,
type: node.type,
data: cloneDeep(node.data),
data: deepClone(node.data),
position: { ...node.position },
});
}

View File

@ -1,4 +1,5 @@
import { $store } from 'app/store/nanostores/store';
import { deepClone } from 'common/util/deepClone';
import { WorkflowMigrationError, WorkflowVersionError } from 'features/nodes/types/error';
import type { FieldType } from 'features/nodes/types/field';
import type { InvocationNodeData } from 'features/nodes/types/invocation';
@ -11,7 +12,7 @@ import { zWorkflowV2 } from 'features/nodes/types/v2/workflow';
import type { WorkflowV3 } from 'features/nodes/types/workflow';
import { zWorkflowV3 } from 'features/nodes/types/workflow';
import { t } from 'i18next';
import { cloneDeep, forEach } from 'lodash-es';
import { forEach } from 'lodash-es';
import { z } from 'zod';
/**
@ -89,7 +90,7 @@ export const parseAndMigrateWorkflow = (data: unknown): WorkflowV3 => {
throw new WorkflowVersionError(t('nodes.unableToGetWorkflowVersion'));
}
let workflow = cloneDeep(data) as WorkflowV1 | WorkflowV2 | WorkflowV3;
let workflow = deepClone(data) as WorkflowV1 | WorkflowV2 | WorkflowV3;
if (workflow.meta.version === '1.0.0') {
const v1 = zWorkflowV1.parse(workflow);