perf(editor): Fix log view related slowdown of manual execution with large data (#18256)

This commit is contained in:
Suguru Inoue
2025-08-12 15:07:51 +02:00
committed by GitHub
parent 98dc71e6a7
commit 56c278cda0
3 changed files with 5 additions and 76 deletions

View File

@@ -4,12 +4,7 @@ import { Workflow, type IRunExecutionData } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useThrottleFn } from '@vueuse/core';
import {
createLogTree,
deepToRaw,
findSubExecutionLocator,
mergeStartData,
} from '@/features/logs/logs.utils';
import { createLogTree, findSubExecutionLocator, mergeStartData } from '@/features/logs/logs.utils';
import { parse } from 'flatted';
import { useToast } from '@/composables/useToast';
import type { LatestNodeInfo, LogEntry } from '../logs.types';
@@ -113,12 +108,10 @@ export function useLogsExecutionData() {
execData.value =
workflowsStore.workflowExecutionData === null
? undefined
: deepToRaw(
mergeStartData(
workflowsStore.workflowExecutionStartedData?.[1] ?? {},
workflowsStore.workflowExecutionData,
),
); // Create deep copy to disable reactivity
: mergeStartData(
workflowsStore.workflowExecutionStartedData?.[1] ?? {},
workflowsStore.workflowExecutionData,
);
if (executionId !== previousExecutionId) {
// Reset sub workflow data when top-level execution changes

View File

@@ -7,7 +7,6 @@ import {
} from '@/__tests__/mocks';
import {
createLogTree,
deepToRaw,
findSelectedLogEntry,
findSubExecutionLocator,
getDefaultCollapsedEntries,
@@ -27,7 +26,6 @@ import {
} from './__test__/data';
import type { LogEntrySelection } from './logs.types';
import type { IExecutionResponse } from '@/Interface';
import { isReactive, reactive } from 'vue';
import { createTestLogEntry } from './__test__/mocks';
import { AGENT_NODE_TYPE, CHAT_TRIGGER_NODE_TYPE } from '@/constants';
@@ -1279,29 +1277,6 @@ describe('extractBotResponse', () => {
});
});
describe(deepToRaw, () => {
it('should convert reactive fields to raw in data with circular structure', () => {
const data = reactive({
foo: reactive({ bar: {} }),
bazz: {},
});
data.foo.bar = data;
data.bazz = data;
const raw = deepToRaw(data);
expect(isReactive(data)).toBe(true);
expect(isReactive(data.foo)).toBe(true);
expect(isReactive(data.foo.bar)).toBe(true);
expect(isReactive(data.bazz)).toBe(true);
expect(isReactive(raw)).toBe(false);
expect(isReactive(raw.foo)).toBe(false);
expect(isReactive(raw.foo.bar)).toBe(false);
expect(isReactive(raw.bazz)).toBe(false);
});
});
describe(mergeStartData, () => {
it('should return unchanged execution response if start data is empty', () => {
const response = createTestWorkflowExecutionResponse({

View File

@@ -14,7 +14,6 @@ import {
type RelatedExecution,
} from 'n8n-workflow';
import type { LogEntry, LogEntrySelection, LogTreeCreationContext } from './logs.types';
import { isProxy, isReactive, isRef, toRaw } from 'vue';
import { CHAT_TRIGGER_NODE_TYPE, MANUAL_CHAT_TRIGGER_NODE_TYPE } from '@/constants';
import { type ChatMessage } from '@n8n/chat/types';
import get from 'lodash/get';
@@ -312,44 +311,6 @@ export function findSelectedLogEntry(
}
}
export function deepToRaw<T>(sourceObj: T): T {
const seen = new WeakMap();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const objectIterator = (input: any): any => {
if (seen.has(input)) {
return input;
}
if (input !== null && typeof input === 'object') {
seen.set(input, true);
}
if (Array.isArray(input)) {
return input.map((item) => objectIterator(item));
}
if (isRef(input) || isReactive(input) || isProxy(input)) {
return objectIterator(toRaw(input));
}
if (
input !== null &&
typeof input === 'object' &&
Object.getPrototypeOf(input) === Object.prototype
) {
return Object.keys(input).reduce((acc, key) => {
acc[key as keyof typeof acc] = objectIterator(input[key]);
return acc;
}, {} as T);
}
return input;
};
return objectIterator(sourceObj);
}
export function flattenLogEntries(
entries: LogEntry[],
collapsedEntryIds: Record<string, boolean>,