Skip to content

Commit bee80dd

Browse files
authored
fix(core): lazy parsing report content to improve performance (#1005)
* fix(core): lazy parsing report content to improve performance * fix(core): lazy parsing report content to improve performance * chore(core): fix lint * chore(core): fix lint
1 parent fff9332 commit bee80dd

File tree

7 files changed

+172
-191
lines changed

7 files changed

+172
-191
lines changed

apps/report/rsbuild.config.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -99,22 +99,28 @@ const copyReportTemplate = () => ({
9999
},
100100
});
101101

102+
let tooManyCases = allTestData;
103+
for (let i = 0; i < 3; i++) {
104+
tooManyCases = tooManyCases.concat(tooManyCases);
105+
}
106+
102107
export default defineConfig({
103108
html: {
104109
template: './template/index.html',
105110
inject: 'body',
106111
tags:
107112
process.env.NODE_ENV === 'development'
108-
? allTestData.map((item) => ({
113+
? tooManyCases.map((item, index) => ({
109114
tag: 'script',
110115
attrs: {
111116
type: 'midscene_web_dump',
112-
playwright_test_name: item.data.groupName,
113117
playwright_test_description: item.data.groupDescription,
114-
playwright_test_id: '8465e854a4d9a753cc87-1f096ece43c67754f95a',
118+
playwright_test_id: `id-${index}`,
115119
playwright_test_title: 'test open new tab',
116120
playwright_test_status: 'passed',
117-
playwright_test_duration: '44274',
121+
playwright_test_duration: Math.round(
122+
Math.random() * 100000,
123+
).toString(),
118124
},
119125
children: JSON.stringify(item.data),
120126
}))

apps/report/src/App.tsx

Lines changed: 89 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import './App.less';
22

33
import { Alert, ConfigProvider, Empty } from 'antd';
4-
import { useCallback, useEffect, useRef, useState } from 'react';
4+
import { useEffect, useRef, useState } from 'react';
55
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
66

77
import { antiEscapeScriptTag } from '@midscene/shared/utils';
@@ -10,11 +10,11 @@ import DetailPanel from './components/detail-panel';
1010
import DetailSide from './components/detail-side';
1111
import GlobalHoverPreview from './components/global-hover-preview';
1212
import Sidebar from './components/sidebar';
13-
import { useExecutionDump } from './components/store';
13+
import { type DumpStoreType, useExecutionDump } from './components/store';
1414
import Timeline from './components/timeline';
1515
import type {
16-
ExecutionDumpWithPlaywrightAttributes,
17-
StoreState,
16+
PlaywrightTaskAttributes,
17+
PlaywrightTasks,
1818
VisualizerProps,
1919
} from './types';
2020

@@ -23,41 +23,31 @@ let globalRenderCount = 1;
2323
function Visualizer(props: VisualizerProps): JSX.Element {
2424
const { dumps } = props;
2525

26-
const executionDump = useExecutionDump((store: StoreState) => store.dump);
26+
const executionDump = useExecutionDump((store: DumpStoreType) => store.dump);
2727
const executionDumpLoadId = useExecutionDump(
28-
(store: StoreState) => store._executionDumpLoadId,
28+
(store) => store._executionDumpLoadId,
2929
);
3030

31-
const setReplayAllMode = useExecutionDump(
32-
(store: StoreState) => store.setReplayAllMode,
33-
);
31+
const setReplayAllMode = useExecutionDump((store) => store.setReplayAllMode);
3432
const replayAllScripts = useExecutionDump(
35-
(store: StoreState) => store.allExecutionAnimation,
36-
);
37-
const insightWidth = useExecutionDump(
38-
(store: StoreState) => store.insightWidth,
39-
);
40-
const insightHeight = useExecutionDump(
41-
(store: StoreState) => store.insightHeight,
42-
);
43-
const replayAllMode = useExecutionDump(
44-
(store: StoreState) => store.replayAllMode,
45-
);
46-
const setGroupedDump = useExecutionDump(
47-
(store: StoreState) => store.setGroupedDump,
33+
(store) => store.allExecutionAnimation,
4834
);
35+
const insightWidth = useExecutionDump((store) => store.insightWidth);
36+
const insightHeight = useExecutionDump((store) => store.insightHeight);
37+
const replayAllMode = useExecutionDump((store) => store.replayAllMode);
38+
const setGroupedDump = useExecutionDump((store) => store.setGroupedDump);
4939
const sdkVersion = useExecutionDump((store) => store.sdkVersion);
5040
const modelName = useExecutionDump((store) => store.modelName);
5141
const modelDescription = useExecutionDump((store) => store.modelDescription);
52-
const reset = useExecutionDump((store: StoreState) => store.reset);
42+
const reset = useExecutionDump((store) => store.reset);
5343
const [mainLayoutChangeFlag, setMainLayoutChangeFlag] = useState(0);
5444
const mainLayoutChangedRef = useRef(false);
55-
const dump = useExecutionDump((store: StoreState) => store.dump);
45+
const dump = useExecutionDump((store) => store.dump);
5646
const [proModeEnabled, setProModeEnabled] = useState(false);
5747

5848
useEffect(() => {
59-
if (dumps) {
60-
setGroupedDump(dumps[0]);
49+
if (dumps?.[0]) {
50+
setGroupedDump(dumps[0].get(), dumps[0].attributes);
6151
}
6252
return () => {
6353
reset();
@@ -150,10 +140,6 @@ function Visualizer(props: VisualizerProps): JSX.Element {
150140
<div className="page-side">
151141
<Sidebar
152142
dumps={dumps}
153-
selectedDump={executionDump}
154-
onDumpSelect={(dump) => {
155-
setGroupedDump(dump);
156-
}}
157143
proModeEnabled={proModeEnabled}
158144
onProModeChange={setProModeEnabled}
159145
replayAllScripts={replayAllScripts}
@@ -243,11 +229,11 @@ function Visualizer(props: VisualizerProps): JSX.Element {
243229
}
244230

245231
export function App() {
246-
function getDumpElements(): ExecutionDumpWithPlaywrightAttributes[] {
232+
function getDumpElements(): PlaywrightTasks[] {
247233
const dumpElements = document.querySelectorAll(
248234
'script[type="midscene_web_dump"]',
249235
);
250-
const reportDump: ExecutionDumpWithPlaywrightAttributes[] = [];
236+
const reportDump: PlaywrightTasks[] = [];
251237
Array.from(dumpElements)
252238
.filter((el) => {
253239
const textContent = el.textContent;
@@ -257,65 +243,95 @@ export function App() {
257243
return !!textContent;
258244
})
259245
.forEach((el) => {
260-
const attributes: Record<string, any> = {};
246+
const attributes: PlaywrightTaskAttributes = {
247+
playwright_test_name: '',
248+
playwright_test_description: '',
249+
playwright_test_id: '',
250+
playwright_test_title: '',
251+
playwright_test_status: '',
252+
playwright_test_duration: '',
253+
};
261254
Array.from(el.attributes).forEach((attr) => {
262255
const { name, value } = attr;
263256
const valueDecoded = decodeURIComponent(value);
264257
if (name.startsWith('playwright_')) {
265-
attributes[attr.name] = valueDecoded;
258+
attributes[attr.name as keyof PlaywrightTaskAttributes] =
259+
valueDecoded;
266260
}
267261
});
268-
const content = antiEscapeScriptTag(el.textContent || '');
269-
try {
270-
const jsonContent = JSON.parse(content);
271-
jsonContent.attributes = attributes;
272-
reportDump.push(jsonContent);
273-
} catch (e) {
274-
console.error(el);
275-
console.error('failed to parse json content', e);
276-
}
262+
263+
// Lazy loading: Store raw content and parse only when get() is called
264+
let cachedJsonContent: any = null;
265+
let isParsed = false;
266+
267+
reportDump.push({
268+
get: () => {
269+
if (!isParsed) {
270+
try {
271+
console.time('parse_dump');
272+
const content = antiEscapeScriptTag(el.textContent || '');
273+
cachedJsonContent = JSON.parse(content);
274+
console.timeEnd('parse_dump');
275+
cachedJsonContent.attributes = attributes;
276+
isParsed = true;
277+
} catch (e) {
278+
console.error(el);
279+
console.error('failed to parse json content', e);
280+
// Return a fallback object to prevent crashes
281+
cachedJsonContent = {
282+
attributes,
283+
error: 'Failed to parse JSON content',
284+
};
285+
isParsed = true;
286+
}
287+
}
288+
return cachedJsonContent;
289+
},
290+
attributes: attributes,
291+
});
277292
});
278293
return reportDump;
279294
}
280295

281-
const [reportDump, setReportDump] = useState<
282-
ExecutionDumpWithPlaywrightAttributes[]
283-
>([]);
296+
const [reportDump, setReportDump] = useState<PlaywrightTasks[]>([]);
284297
const [error, setError] = useState<string | null>(null);
285298

286299
const dumpsLoadedRef = useRef(false);
287300

288-
const loadDumpElements = useCallback(() => {
289-
const currentElements = document.querySelectorAll(
290-
'script[type="midscene_web_dump"]',
291-
);
301+
useEffect(() => {
302+
// Check if document is already loaded
292303

293-
// If it has been loaded and the number of elements has not changed, skip it.
294-
if (
295-
dumpsLoadedRef.current &&
296-
currentElements.length === reportDump.length
297-
) {
298-
return;
299-
}
304+
const loadDumpElements = () => {
305+
const currentElements = document.querySelectorAll(
306+
'script[type="midscene_web_dump"]',
307+
);
300308

301-
dumpsLoadedRef.current = true;
302-
if (
303-
currentElements.length === 1 &&
304-
currentElements[0].textContent?.trim() === ''
305-
) {
306-
setError('There is no dump data to display.');
307-
setReportDump([]);
308-
return;
309-
}
310-
setError(null);
311-
setReportDump(getDumpElements());
312-
}, [reportDump.length]);
309+
// If it has been loaded and the number of elements has not changed, skip it.
310+
if (
311+
dumpsLoadedRef.current &&
312+
currentElements.length === reportDump.length
313+
) {
314+
return;
315+
}
316+
317+
dumpsLoadedRef.current = true;
318+
if (
319+
currentElements.length === 1 &&
320+
currentElements[0].textContent?.trim() === ''
321+
) {
322+
setError('There is no dump data to display.');
323+
setReportDump([]);
324+
return;
325+
}
326+
setError(null);
327+
const dumpElements = getDumpElements();
328+
setReportDump(dumpElements);
329+
};
313330

314-
useEffect(() => {
315-
// Check if document is already loaded
316331
const loadDumps = () => {
317-
console.log('Loading dump elements...');
332+
console.time('loading_dump');
318333
loadDumpElements();
334+
console.timeEnd('loading_dump');
319335
};
320336

321337
// If DOM is already loaded (React mounts after DOMContentLoaded in most cases)
@@ -330,39 +346,10 @@ export function App() {
330346
document.addEventListener('DOMContentLoaded', loadDumps);
331347
}
332348

333-
// Set up a MutationObserver to detect if dump scripts are added after initial load
334-
const observer = new MutationObserver((mutations) => {
335-
for (const mutation of mutations) {
336-
if (mutation.type === 'childList') {
337-
const addedNodes = Array.from(mutation.addedNodes);
338-
const hasDumpScripts = addedNodes.some(
339-
(node) =>
340-
node.nodeType === Node.ELEMENT_NODE &&
341-
node.nodeName === 'SCRIPT' &&
342-
(node as HTMLElement).getAttribute('type') ===
343-
'midscene_web_dump',
344-
);
345-
346-
if (hasDumpScripts) {
347-
loadDumps();
348-
break;
349-
}
350-
}
351-
}
352-
});
353-
354-
// Start observing the document with the configured parameters
355-
observer.observe(document.body, { childList: true, subtree: true });
356-
357-
// Safety fallback in case other methods fail
358-
const fallbackTimer = setTimeout(loadDumps, 3000);
359-
360349
return () => {
361350
document.removeEventListener('DOMContentLoaded', loadDumps);
362-
observer.disconnect();
363-
clearTimeout(fallbackTimer);
364351
};
365-
}, [loadDumpElements]);
352+
}, []);
366353

367354
if (error) {
368355
return (

0 commit comments

Comments
 (0)