Skip to content

feat(report): each time, append the report individually instead of appending in bulk to avoid excessive memory usage #854

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

Merged
merged 9 commits into from
Jun 25, 2025
1 change: 0 additions & 1 deletion apps/chrome-extension/src/extension/misc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
MinusOutlined,
WarningOutlined,
} from '@ant-design/icons';
import React from 'react';

export const iconForStatus = (status: string): JSX.Element => {
switch (status) {
Expand Down
163 changes: 127 additions & 36 deletions apps/report/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import './App.less';
import './index.less';

import { CaretRightOutlined } from '@ant-design/icons';
import { Button, ConfigProvider, Empty } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { Alert, Button, ConfigProvider, Empty } from 'antd';
import { useCallback, useEffect, useRef, useState } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';

import { antiEscapeScriptTag } from '@midscene/shared/utils';
Expand All @@ -23,7 +23,7 @@ import type {

let globalRenderCount = 1;

export function Visualizer(props: VisualizerProps): JSX.Element {
function Visualizer(props: VisualizerProps): JSX.Element {
const { dumps } = props;

const executionDump = useExecutionDump((store: StoreState) => store.dump);
Expand Down Expand Up @@ -251,43 +251,134 @@ export function Visualizer(props: VisualizerProps): JSX.Element {
);
}

// Main App component using Visualizer
const App = () => {
const dumpElements = document.querySelectorAll(
'script[type="midscene_web_dump"]',
);
const reportDump: ExecutionDumpWithPlaywrightAttributes[] = [];

Array.from(dumpElements)
.filter((el) => {
const textContent = el.textContent;
if (!textContent) {
console.warn('empty content in script tag', el);
}
return !!textContent;
})
.forEach((el) => {
const attributes: Record<string, any> = {};
Array.from(el.attributes).forEach((attr) => {
const { name, value } = attr;
const valueDecoded = decodeURIComponent(value);
if (name.startsWith('playwright_')) {
attributes[attr.name] = valueDecoded;
export function App() {
function getDumpElements(): ExecutionDumpWithPlaywrightAttributes[] {
const dumpElements = document.querySelectorAll(
'script[type="midscene_web_dump"]',
);
const reportDump: ExecutionDumpWithPlaywrightAttributes[] = [];
Array.from(dumpElements)
.filter((el) => {
const textContent = el.textContent;
if (!textContent) {
console.warn('empty content in script tag', el);
}
return !!textContent;
})
.forEach((el) => {
const attributes: Record<string, any> = {};
Array.from(el.attributes).forEach((attr) => {
const { name, value } = attr;
const valueDecoded = decodeURIComponent(value);
if (name.startsWith('playwright_')) {
attributes[attr.name] = valueDecoded;
}
});
const content = antiEscapeScriptTag(el.textContent || '');
try {
const jsonContent = JSON.parse(content);
jsonContent.attributes = attributes;
reportDump.push(jsonContent);
} catch (e) {
console.error(el);
console.error('failed to parse json content', e);
}
});
return reportDump;
}

const [reportDump, setReportDump] = useState<
ExecutionDumpWithPlaywrightAttributes[]
>([]);
const [error, setError] = useState<string | null>(null);

const loadDumpElements = useCallback(() => {
const dumpElements = document.querySelectorAll(
'script[type="midscene_web_dump"]',
);
if (
dumpElements.length === 1 &&
dumpElements[0].textContent?.trim() === ''
) {
setError('There is no dump data to display.');
setReportDump([]);
return;
}
setError(null);
setReportDump(getDumpElements());
}, []);

useEffect(() => {
// Check if document is already loaded
const loadDumps = () => {
console.log('Loading dump elements...');
loadDumpElements();
};

// If DOM is already loaded (React mounts after DOMContentLoaded in most cases)
if (
document.readyState === 'complete' ||
document.readyState === 'interactive'
) {
// Use a small timeout to ensure all scripts are parsed
setTimeout(loadDumps, 0);
} else {
// Wait for DOM content to be fully loaded
document.addEventListener('DOMContentLoaded', loadDumps);
}

const content = antiEscapeScriptTag(el.textContent || '');
try {
const jsonContent = JSON.parse(content);
jsonContent.attributes = attributes;
reportDump.push(jsonContent);
} catch (e) {
console.error(el);
console.error('failed to parse json content', e);
// Set up a MutationObserver to detect if dump scripts are added after initial load
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
const addedNodes = Array.from(mutation.addedNodes);
const hasDumpScripts = addedNodes.some(
(node) =>
node.nodeType === Node.ELEMENT_NODE &&
node.nodeName === 'SCRIPT' &&
(node as HTMLElement).getAttribute('midscene_type') ===
'web_dump',
);

if (hasDumpScripts) {
loadDumps();
break;
}
}
}
});

return <Visualizer dumps={reportDump} />;
};
// Start observing the document with the configured parameters
observer.observe(document.body, { childList: true, subtree: true });

// Safety fallback in case other methods fail
const fallbackTimer = setTimeout(loadDumps, 3000);

export default App;
return () => {
document.removeEventListener('DOMContentLoaded', loadDumps);
observer.disconnect();
clearTimeout(fallbackTimer);
};
}, [loadDumpElements]);

if (error) {
return (
<div
style={{
width: '100%',
height: '100%',
padding: '100px',
boxSizing: 'border-box',
}}
>
<Alert
message="Midscene.js - Error"
description={error}
type="error"
showIcon
/>
</div>
);
}
return <Visualizer dumps={reportDump} />;
}
63 changes: 4 additions & 59 deletions apps/report/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,67 +1,12 @@
import { escapeScriptTag } from '@midscene/shared/utils';
import { Alert } from 'antd';
import ReactDOM from 'react-dom/client';
import { Visualizer } from './App';
import type { ExecutionDumpWithPlaywrightAttributes } from './types';
import { App } from './App';

const rootEl = document.getElementById('root');

if (rootEl) {
const root = ReactDOM.createRoot(rootEl);

const dumpElements = document.querySelectorAll(
'script[type="midscene_web_dump"]',
);
if (dumpElements.length === 1 && dumpElements[0].textContent?.trim() === '') {
const errorPanel = (
<div
style={{
width: '100%',
height: '100%',
padding: '100px',
boxSizing: 'border-box',
}}
>
<Alert
message="Midscene.js - Error"
description="There is no dump data to display."
type="error"
showIcon
/>
</div>
);
root.render(errorPanel);
} else {
const reportDump: ExecutionDumpWithPlaywrightAttributes[] = [];
Array.from(dumpElements)
.filter((el) => {
const textContent = el.textContent;
if (!textContent) {
console.warn('empty content in script tag', el);
}
return !!textContent;
})
.forEach((el) => {
const attributes: Record<string, any> = {};
Array.from(el.attributes).forEach((attr) => {
const { name, value } = attr;
const valueDecoded = decodeURIComponent(value);
if (name.startsWith('playwright_')) {
attributes[attr.name] = valueDecoded;
}
});

const content = escapeScriptTag(el.textContent || '');
try {
const jsonContent = JSON.parse(content);
jsonContent.attributes = attributes;
reportDump.push(jsonContent);
} catch (e) {
console.error(el);
console.error('failed to parse json content', e);
}
});

root.render(<Visualizer dumps={reportDump} />);
}
root.render(<App />);
} else {
console.error('no root element found');
}
2 changes: 1 addition & 1 deletion apps/report/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GroupedActionDump } from '@midscene/core';
import type { AnimationScript } from '@midscene/visualizer/playground';
import type { AnimationScript } from '@midscene/visualizer';

// Core visualization types
export interface ExecutionDumpWithPlaywrightAttributes
Expand Down
14 changes: 8 additions & 6 deletions apps/report/template/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
<html>
<head>
<title>Report - Midscene.js</title>
<link rel="icon" type="image/png" sizes="32x32" href="https://lf3-static.bytednsdoc.com/obj/eden-cn/vhaeh7vhabf/favicon-32x32.png">
<link
rel="icon"
type="image/png"
sizes="32x32"
href="https://lf3-static.bytednsdoc.com/obj/eden-cn/vhaeh7vhabf/favicon-32x32.png"
/>
</head>
<body>
<div id="dump-container" style="display: none;">
{{dump}}
</div>
<!-- it should be replaced by the actual content -->
<div id="<%= mountId %>" style="height: 100vh;width: 100vw;"></div>
<div id="<%= mountId %>" style="height: 100vh; width: 100vw"></div>
</body>
</html>
</html>
Loading