diff --git a/.gitignore b/.gitignore index f9c6002d8..038ab34c8 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,10 @@ packages/x-markdown/src/plugins/version/token.json packages/x-markdown/src/version/version.ts packages/x-sdk/src/version/version.ts + +# Playwright (from benchmark) +packages/x-markdown/test-results/ +packages/x-markdown/src/XMarkdown/__benchmark__/playwright-report/ +packages/x-markdown/src/XMarkdown/__benchmark__/blob-report/ +packages/x-markdown/src/XMarkdown/__benchmark__/playwright/.cache/ +packages/x-markdown/src/XMarkdown/__benchmark__/playwright/.auth/ diff --git a/packages/x-markdown/package.json b/packages/x-markdown/package.json index e33162b4c..8f41f45d5 100644 --- a/packages/x-markdown/package.json +++ b/packages/x-markdown/package.json @@ -18,7 +18,10 @@ "version": "tsx scripts/generate-version.ts", "test:dekko": "tsx ./tests/dekko/index.test.ts", "clean": "rm -rf es lib coverage plugins dist themes", - "test:package-diff": "antd-tools run package-diff" + "test:package-diff": "antd-tools run package-diff", + "token:meta": "tsx scripts/generate-token-meta.ts", + "token:statistic": "tsx scripts/collect-token-statistic.ts", + "benchmark": "cd src/XMarkdown/__benchmark__ && node scripts/run-benchmark.js" }, "sideEffects": false, "main": "lib/index.js", @@ -54,16 +57,29 @@ "marked": "^15.0.12" }, "devDependencies": { + "@playwright/experimental-ct-react": "^1.56.1", + "@playwright/test": "^1.48.2", "@types/dompurify": "^3.0.5", "@types/lodash.throttle": "^4.1.9", + "@types/markdown-it": "^14.1.2", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@types/react-syntax-highlighter": "^15.5.13", "@umijs/mako": "^0.11.10", "antd": "^6.0.1", "glob": "^11.0.0", + "markdown-it": "^14.0.0", + "markdown-it-katex": "^2.0.3", + "marked-katex-extension": "^5.1.6", "react": "^19.0.0", - "react-dom": "^19.0.0" + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "rehype-katex": "^7.0.1", + "rehype-raw": "^7.0.0", + "remark-gfm": "^4.0.1", + "remark-math": "^6.0.0", + "streamdown": "^1.4.0", + "vite": "^5.0.6" }, "peerDependencies": { "react": ">=18.0.0", diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/README.md b/packages/x-markdown/src/XMarkdown/__benchmark__/README.md new file mode 100644 index 000000000..7d0b30ab1 --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/README.md @@ -0,0 +1,122 @@ +# Markdown 渲染器性能基准测试 + +本基准测试用于比较不同 Markdown 渲染器在流式渲染场景下的性能表现。 + +## 支持的渲染器 + +- **marked** - 流行的 Markdown 解析器 +- **markdown-it** - 可配置的 Markdown 解析器 +- **react-markdown** - React组件形式的 Markdown 渲染器 +- **x-markdown** - 本项目的高性能 Markdown 渲染器 +- **streamdown** - 流式 Markdown 渲染器 + +## 使用方法 + +### 一键运行完整基准测试 + +````bash +# 在项目根目录下运行 +cd packages/x-markdown/benchmark + +# 一键运行完整基准测试(推荐) +npm run benchmark + + +## 生成的报告 + +运行完成后,会在 `test-results/` 目录下生成以下报告: + +### 主要报告 + +- `benchmark-report.html` - 标准性能报告 +- `benchmark-results.json` - 原始JSON数据 + +### 增强版报告(新增) + +- `benchmark-comparison.html` - 详细对比报告,包含所有渲染器的完整对比 +- `benchmark-historical.html` - 历史趋势报告,支持多次运行的数据对比 +- `benchmark-history.json` - 历史数据存储 + +## 报告内容 + +### 标准报告包含 + +- 各渲染器的渲染时长 +- 平均FPS和帧率稳定性 +- 内存使用峰值和增量 +- 系统信息 +- 交互式图表对比 + +### 增强版报告包含 + +- **性能排名** - 自动计算最快渲染器、最省内存渲染器等 +- **历史趋势** - 多次运行的性能变化趋势 +- **综合评分** - 基于多维度指标的综合性能评分 +- **详细对比表** - 所有指标的完整对比表格 + +## 示例输出 + +运行后会看到类似以下的输出: + +``` +🚀 开始运行完整的 Markdown 渲染器性能基准测试... + +📦 检查并安装依赖... +🌐 安装Playwright浏览器... +🧹 清理旧的测试结果... +🏃 运行所有渲染器的性能测试... + +📊 Benchmark Results Table +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +┌─────────┬────────────────┬──────────────┬──────────┬─────────────┬──────────────┬──────────────┬─────────────┐ +│ (index) │ Renderer │ Duration(ms) │ Avg FPS │ StdDev FPS │ Avg Memory… │ Memory Del… │ Total Frames│ +├─────────┼────────────────┼──────────────┼──────────┼─────────────┼──────────────┼──────────────┼─────────────┤ +│ 0 │ 'marked' │ 5234 │ '45.2' │ '12.34' │ '25.43' │ '2.15' │ 234 │ +│ 1 │ 'markdown-it' │ 6123 │ '38.7' │ '15.21' │ '28.91' │ '3.42' │ 198 │ +│ 2 │ 'react-markdown'│ 7891 │ '32.1' │ '18.45' │ '35.67' │ '5.23' │ 167 │ +│ 3 │ 'x-markdown' │ 4456 │ '52.8' │ '8.92' │ '22.34' │ '1.89' │ 267 │ +│ 4 │ 'streamdown' │ 3987 │ '58.3' │ '6.78' │ '20.12' │ '1.23' │ 289 │ +└─────────┴────────────────┴──────────────┴──────────┴─────────────┴──────────────┴──────────────┴─────────────┘ +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + +📊 HTML报告已生成: test-results/benchmark-report.html +📊 JSON报告已生成: test-results/benchmark-results.json +📊 增强版报告生成完成! + 📈 历史对比报告: test-results/benchmark-historical.html + 🔍 详细对比报告: test-results/benchmark-comparison.html +``` + +## 性能指标说明 + +- **渲染时长** - 完成整个文档渲染的总时间(毫秒) +- **平均FPS** - 渲染过程中的平均帧率 +- **FPS标准差** - 帧率稳定性指标,越小越稳定 +- **内存峰值** - 渲染过程中的最大内存使用量 +- **内存增量** - 渲染过程中新增的内存使用量 +- **总帧数** - 渲染过程中生成的总帧数 + +## 注意事项 + +1. **运行环境** - 建议在性能较好的机器上运行以获得准确结果 +2. **浏览器缓存** - 每次运行前会自动清理浏览器缓存 +3. **多次运行** - 默认运行3次取平均值以提高准确性 +4. **系统资源** - 运行期间请关闭其他占用资源的程序 + +## 故障排除 + +如果遇到问题,请检查: + +1. **依赖安装** - 确保所有依赖已正确安装 +2. **浏览器支持** - 确保已安装Chromium浏览器 +3. **内存限制** - 确保系统有足够的可用内存 +4. **网络连接** - 首次运行需要下载浏览器组件 + +## 自定义测试 + +可以通过修改 `performance.spec.tsx` 文件来自定义测试: + +- 调整 `RUN_COUNT` 改变运行次数 +- 修改 `renderers` 数组添加或移除渲染器 +- 调整 `updateInterval` 改变流式更新频率 +- 更换测试文档 `test.md` +```` diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/components/MarkdownRenderer.tsx b/packages/x-markdown/src/XMarkdown/__benchmark__/components/MarkdownRenderer.tsx new file mode 100644 index 000000000..40c919152 --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/components/MarkdownRenderer.tsx @@ -0,0 +1,77 @@ +import MarkdownIt from 'markdown-it'; +// @ts-ignore - benchmark only, ignore type checking +import markdownItKatex from 'markdown-it-katex'; +import { Marked } from 'marked'; +import markedKatex from 'marked-katex-extension'; +import React, { FC } from 'react'; +import ReactMarkdown from 'react-markdown'; +import rehypeKatex from 'rehype-katex'; +import rehypeRaw from 'rehype-raw'; +import remarkGfm from 'remark-gfm'; +import remarkMath from 'remark-math'; +import { Streamdown } from 'streamdown'; +import getLatexPlugin from '../../../plugins/Latex'; +import XMarkdown from '../../index'; + +type MarkdownRendererProps = { + md: string; + hasNextChunk?: boolean; +}; + +const md = new MarkdownIt(); +// benchmark only: bypass TS check +md.use(markdownItKatex); +const marked = new Marked(markedKatex({ throwOnError: false })); + +const MarkedRenderer: FC = (props) => ( +
+); + +const MarkdownItRenderer: FC = (props) => { + return ( + // biome-ignore lint/security/noDangerouslySetInnerHtml: benchmark only +
+ ); +}; + +const ReactMarkdownRenderer: FC = (props) => ( +
+ + {props.md} + +
+); + +const XMarkdownRenderer: FC = (props) => ( +
+ + {props.md} + +
+); + +const StreamdownRenderer: FC = (props) => ( +
+ + {props.md} + +
+); + +const Empty = () =>
; + +export { + MarkedRenderer, + MarkdownItRenderer, + ReactMarkdownRenderer, + XMarkdownRenderer, + StreamdownRenderer, + Empty, +}; diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/playwright-ct.config.ts b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright-ct.config.ts new file mode 100644 index 000000000..4ee782170 --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright-ct.config.ts @@ -0,0 +1,40 @@ +import { defineConfig, devices } from '@playwright/experimental-ct-react'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './', + /* The base directory, relative to the config file, for snapshot files created with toMatchSnapshot and toHaveScreenshot. */ + snapshotDir: './__snapshots__', + /* Maximum time one test can run for. */ + timeout: 10 * 1000, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + /* Show browser window */ + headless: false, + + /* Port to use for Playwright component endpoint. */ + ctPort: 3100, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/.cache/index.html b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/.cache/index.html new file mode 100644 index 000000000..e3871863b --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/.cache/index.html @@ -0,0 +1,12 @@ + + + + + + Testing Page + + + +
+ + diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/index.html b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/index.html new file mode 100644 index 000000000..610ddf8a4 --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/index.html @@ -0,0 +1,12 @@ + + + + + + Testing Page + + +
+ + + diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/index.tsx b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/index.tsx new file mode 100644 index 000000000..ac6de14bf --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/playwright/index.tsx @@ -0,0 +1,2 @@ +// Import styles, initialize component theme here. +// import '../src/common.css'; diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/scripts/run-benchmark.js b/packages/x-markdown/src/XMarkdown/__benchmark__/scripts/run-benchmark.js new file mode 100644 index 000000000..d6383baec --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/scripts/run-benchmark.js @@ -0,0 +1,52 @@ +#!/usr/bin/env node +const { spawn } = require('child_process'); +const fs = require('fs'); +const path = require('path'); + +async function runFullBenchmark() { + console.log('🚀 Starting Streaming Markdown renderer performance benchmark...\n'); + + // Install Playwright browsers + console.log('🌐 Installing Playwright browsers...'); + await runCommand('npx', ['playwright', 'install', 'chromium']); + + // Clean up old test results + console.log('🧹 Cleaning up old test results...'); + const testResultsDir = path.join(__dirname, '../test-results'); + if (fs.existsSync(testResultsDir)) { + fs.rmSync(testResultsDir, { recursive: true, force: true }); + } + + // Run the full benchmark suite + console.log('🏃 Running performance tests for all renderers...'); + const configPath = path.join(__dirname, '..', 'playwright-ct.config.ts'); + await runCommand('npx', ['playwright', 'test', '-c', configPath, '--reporter=line'], { + cwd: path.join(__dirname, '..'), + }); + + console.log('\n✅ Benchmark completed successfully!'); + console.log('📊 Report locations:'); + console.log(` HTML Report: ${path.join(__dirname, '../test-results/benchmark-report.html')}`); + console.log(` JSON Data: ${path.join(__dirname, '../test-results/benchmark-results.json')}`); +} + +function runCommand(command, args, options = {}) { + return new Promise((resolve, reject) => { + const process = spawn(command, args, { + stdio: 'inherit', + ...options, + }); + + process.on('close', (code) => { + if (code === 0) { + resolve(); + } else { + reject(new Error(`process error, exit: ${code}`)); + } + }); + + process.on('error', reject); + }); +} + +runFullBenchmark().catch(console.error); diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/tests/benchmark.config.ts b/packages/x-markdown/src/XMarkdown/__benchmark__/tests/benchmark.config.ts new file mode 100644 index 000000000..e60df1dfa --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/tests/benchmark.config.ts @@ -0,0 +1,40 @@ +/** + * 流式 Markdown 渲染性能测试配置 + * 所有可配置参数集中管理 + */ + +export const TEXT_CATEGORIES = { + short: { min: 0, max: 280, name: '短文本' }, + medium: { min: 280, max: 2000, name: '中文本' }, + long: { min: 2000, max: 20000, name: '长文本' }, +} as const; + +export const BENCHMARK_CONFIG = { + // 分块渲染配置 + CHUNK_SIZE: 6, // 每次渲染的字符数 + UPDATE_INTERVAL: 50, // 每块之间的间隔时间(ms) + RUN_COUNT: 5, // 每个测试用例的运行次数 + + // 测试文本长度配置 + TEST_TEXT_LENGTHS: { + short: 250, // 短文本字符数 + medium: 1500, // 中文本字符数 + long: 8000, // 长文本字符数 + }, + + // 超时配置 + TIMEOUT: 600_000, // 单个测试用例超时时间(ms) + + // 调试配置 + DEBUG: { + ENABLE_TRACING: true, // 是否启用性能追踪 + ENABLE_SCREENSHOTS: false, // 是否启用截图 + ENABLE_SNAPSHOTS: false, // 是否启用快照 + }, +} as const; + +// 支持的渲染器列表 +export const RENDERERS = ['react-markdown', 'x-markdown', 'streamdown'] as const; + +// 测试文件路径 +export const TEST_FILE_PATH = 'test.md'; diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/tests/performance.spec.tsx b/packages/x-markdown/src/XMarkdown/__benchmark__/tests/performance.spec.tsx new file mode 100644 index 000000000..ecb3a89b5 --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/tests/performance.spec.tsx @@ -0,0 +1,528 @@ +import { test } from '@playwright/experimental-ct-react'; +import fs from 'fs'; +import path from 'path'; +import React from 'react'; +import { + Empty, + MarkdownItRenderer, + MarkedRenderer, + ReactMarkdownRenderer, + StreamdownRenderer, + XMarkdownRenderer, +} from '../components/MarkdownRenderer'; +import { BENCHMARK_CONFIG, RENDERERS, TEST_FILE_PATH, TEXT_CATEGORIES } from './benchmark.config'; + +// --- 类型定义和配置 --- +interface BenchmarkResult { + name: string; + textLength: number; + textType: 'short' | 'medium' | 'long'; + duration: number; + fcp: number; // 新增:FCP (First Contentful Paint) + avgFPS: number; + stdDevFPS: number; + maxMemory: number; + avgAvgMemory: number; + memoryDelta: number; + systemInfo: { + userAgent: string; + deviceMemory: number; + hardwareConcurrency: number; + }; + timeline: { + fps: number[]; + memory: number[]; + timestamps: number[]; + }; +} + +interface RunResult { + duration: number; + fcp: number; + avgFPS: number; + minFPS: number; + maxFPS: number; + maxMemory: number; + avgMemory: number; + totalFrames: number; + fpsSamples: number[]; + memorySamples: number[]; + timestamps: number[]; +} + +const fullText = fs.readFileSync(path.resolve(__dirname, TEST_FILE_PATH), 'utf-8'); +const { CHUNK_SIZE, UPDATE_INTERVAL, RUN_COUNT, TEST_TEXT_LENGTHS } = BENCHMARK_CONFIG; + +// 根据文本长度生成测试文本 +function generateTextByLength(length: number): string { + if (length <= fullText.length) { + return fullText.substring(0, length); + } + + // 如果需要的文本长度超过现有文本,则重复内容 + let result = ''; + while (result.length < length) { + result += fullText; + } + return result.substring(0, length); +} + +// 获取不同长度的测试文本 +const testTexts = { + short: generateTextByLength(TEST_TEXT_LENGTHS.short), + medium: generateTextByLength(TEST_TEXT_LENGTHS.medium), + long: generateTextByLength(TEST_TEXT_LENGTHS.long), +}; + +const getRenderer = (name: string, md = '') => { + switch (name) { + case 'marked': { + return ; + } + case 'markdown-it': { + return ; + } + case 'react-markdown': { + return ; + } + case 'x-markdown': { + return ; + } + case 'streamdown': { + return ; + } + default: { + return ; + } + } +}; + +interface PerformanceWindow extends Window { + fpsSamples: number[]; + memorySamples: number[]; + timestamps: number[]; + startTime: number; + lastFrameTime: number; + initialMemory: number; + fcpTime: number; +} + +interface PerformanceMemory { + usedJSHeapSize: number; +} + +interface ExtendedPerformance extends Performance { + memory?: PerformanceMemory; +} + +/** + * 在浏览器环境中注入性能跟踪脚本 + */ +async function injectPerformanceTracker(page: any) { + await page.evaluate(() => { + const perfWindow = window as unknown as PerformanceWindow; + const perf = performance as ExtendedPerformance; + + perfWindow.fpsSamples = []; + perfWindow.memorySamples = []; + perfWindow.timestamps = []; + perfWindow.startTime = performance.now(); + perfWindow.lastFrameTime = performance.now(); + perfWindow.initialMemory = perf.memory?.usedJSHeapSize || 0; + perfWindow.fcpTime = 0; + + const trackFPS = () => { + const now = performance.now(); + const frameTime = now - perfWindow.lastFrameTime; + if (frameTime > 0) { + const fps = 1000 / frameTime; + perfWindow.fpsSamples.push(fps); + perfWindow.timestamps.push(now - perfWindow.startTime); + + if (perf.memory) { + perfWindow.memorySamples.push(perf.memory.usedJSHeapSize); + } + } + perfWindow.lastFrameTime = now; + requestAnimationFrame(trackFPS); + }; + + // FCP 测量逻辑 + const observer = new MutationObserver(() => { + const containers = document.querySelectorAll('.markdown-container'); + const hasContent = Array.from(containers).some( + (container) => + container.children.length > 0 || + (container.textContent && container.textContent.trim().length > 0), + ); + + if (perfWindow.fcpTime === 0 && hasContent) { + perfWindow.fcpTime = performance.now(); + observer.disconnect(); + } + }); + + // 立即开始观察,确保能捕获到首次内容渲染 + observer.observe(document.body, { childList: true, subtree: true, characterData: true }); + + // 设置超时保护,防止FCP永远不触发 + setTimeout(() => { + if (perfWindow.fcpTime === 0) { + perfWindow.fcpTime = performance.now(); + observer.disconnect(); + } + }, 1000); + + requestAnimationFrame(trackFPS); + }); +} + +/** + * 单次运行的测量逻辑 + */ +async function measureSingleRun({ + page, + name, + browserName, + component, + testText, + textType, +}: { + name: string; + page: any; + component: any; + browserName: string; + testText: string; + textType: 'short' | 'medium' | 'long'; +}): Promise { + await page.addInitScript(() => { + Object.defineProperty(document, 'hidden', { value: false, writable: true }); + if ('caches' in window) { + caches.keys().then((keys) => + keys.forEach((key) => { + caches.delete(key); + }), + ); + } + if ('gc' in window) { + (window as any).gc(); + } + + // 确保 performance.memory 可用 + if (!('memory' in performance)) { + (performance as any).memory = { + usedJSHeapSize: 0, + }; + } + }); + + await page.context().tracing.start({ + screenshots: false, // 禁用截图以减少开销 + snapshots: false, // 禁用快照以减少开销 + title: `Markdown_Stream_Perf_${browserName}_${name}_${textType}`, + }); + + // 确保浏览器有performance.memory API + await page.addInitScript(() => { + if (!('memory' in performance)) { + Object.defineProperty(performance, 'memory', { + value: { + usedJSHeapSize: 0, + totalJSHeapSize: 0, + jsHeapSizeLimit: 0, + }, + writable: true, + }); + } + }); + + await injectPerformanceTracker(page); + + // 优化:等待第一次渲染完成,确保初始的 FPS 采样已启动 + await page.waitForTimeout(500); + + const startTime = await page.evaluate(() => (window as any).startTime); + + // 3. 流式渲染过程 + let currentText = ''; + for (let i = 0; i < testText.length; i += CHUNK_SIZE) { + currentText = testText.substring(0, i + CHUNK_SIZE); + + // 关键优化:使用 setProps/update 触发更新 + await component.update(getRenderer(name, currentText)); + + // 模拟网络延迟/流速 + await page.waitForTimeout(UPDATE_INTERVAL); + } + + // 4. 等待内容完全稳定(例如,代码高亮等异步任务完成) + // 使用更健壮的方式来等待内容渲染完成 + try { + // 等待markdown容器出现 + await page.locator('.markdown-container').waitFor({ state: 'attached', timeout: 5000 }); + + // 等待内容非空 + await page.waitForFunction( + () => { + const containers = document.querySelectorAll('.markdown-container'); + if (containers.length === 0) return false; + + // 检查任意一个容器是否有内容 + return Array.from(containers).some( + (container) => + container.children.length > 0 || + (container.textContent && container.textContent.trim().length > 0), + ); + }, + { timeout: 10000 }, + ); + + // 额外等待一小段时间确保异步渲染完成 + await page.waitForTimeout(500); + } catch (error) { + console.warn(`Warning: Content stabilization wait failed for ${name}:`, error); + // 打印当前页面状态用于调试 + const debugInfo = await page.evaluate(() => ({ + bodyContent: document.body?.innerHTML || 'empty', + bodyText: document.body?.textContent?.trim() || 'empty', + hasChildren: document.body?.children.length || 0, + containers: document.querySelectorAll('.markdown-container').length, + url: window.location.href, + })); + console.warn(`Debug info for ${name}:`, debugInfo); + // 即使等待失败也继续,避免测试完全中断 + } + + const endTime = await page.evaluate(() => performance.now()); + const totalDuration = endTime - startTime; + + // 5. 停止跟踪并收集指标 + const finalMetrics = await page.evaluate((duration: number) => { + const perfWindow = window as unknown as PerformanceWindow; + const validFpsSamples = perfWindow.fpsSamples.filter((fps: number) => fps > 1 && fps < 120); + + const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0); + + return { + duration, + fcp: Math.max(0, perfWindow.fcpTime - perfWindow.startTime), + avgFPS: validFpsSamples.length > 0 ? sum(validFpsSamples) / validFpsSamples.length : 0, + minFPS: validFpsSamples.length > 0 ? Math.min(...validFpsSamples) : 0, + maxFPS: validFpsSamples.length > 0 ? Math.max(...validFpsSamples) : 0, + maxMemory: perfWindow.memorySamples.length > 0 ? Math.max(...perfWindow.memorySamples) : 0, + avgMemory: + perfWindow.memorySamples.length > 0 + ? sum(perfWindow.memorySamples) / perfWindow.memorySamples.length + : 0, + totalFrames: perfWindow.fpsSamples.length || 0, + fpsSamples: validFpsSamples, + memorySamples: perfWindow.memorySamples, + timestamps: perfWindow.timestamps, + }; + }, totalDuration); + + await page.context().tracing.stop({ + path: `test-results/trace-${name}-${Date.now()}.zip`, + }); + + return finalMetrics as RunResult; +} + +async function measure({ + page, + name, + browserName, + mount, + textType, +}: { + name: string; + page: any; + mount: any; + browserName: string; + textType: 'short' | 'medium' | 'long'; +}): Promise { + const testText = testTexts[textType]; + const textLength = testText.length; + + console.log( + `\n📊 ${browserName} · ${name} · ${TEXT_CATEGORIES[textType].name} · ${RUN_COUNT} 轮测试`, + ); + + const component = await mount(getRenderer(name)); + const runs: RunResult[] = []; + for (let i = 0; i < RUN_COUNT; i++) { + console.log(` Iteration ${i + 1}/${RUN_COUNT}`); + const result = await measureSingleRun({ + name, + page, + browserName, + component, + testText, + textType, + }); + runs.push(result); + + await page.evaluate(() => { + if ('gc' in window) (window as any).gc(); + }); + await page.waitForTimeout(1000); + } + + // 计算聚合统计值 + const avg = (key: keyof RunResult) => + runs.reduce((sum, r) => sum + (r[key] as number), 0) / runs.length; + + const avgDuration = avg('duration'); + const avgFCP = avg('fcp'); + const avgFPS = avg('avgFPS'); + const avgMaxMemory = avg('maxMemory'); + const avgAvgMemory = avg('avgMemory'); + + // 标准差计算 - 添加保护避免除以0 + const fpsValues = runs.map((r) => r.avgFPS).filter((fps) => fps > 0); + const meanFPS = avgFPS; + const stdDevFPS = + fpsValues.length > 0 + ? Math.sqrt(fpsValues.reduce((sum, fps) => sum + (fps - meanFPS) ** 2, 0) / fpsValues.length) + : 0; + + // 收集系统信息 + const systemInfo = await page.evaluate(() => ({ + userAgent: navigator.userAgent, + deviceMemory: (navigator as any).deviceMemory || 0, + hardwareConcurrency: navigator.hardwareConcurrency || 0, + })); + + // 内存增量计算 (使用第一次运行的初始内存) + const initialMemory = runs[0].memorySamples[0] || 0; + const memoryDelta = Math.max(0, avgMaxMemory - initialMemory); + + return { + name, + textLength, + textType, + duration: avgDuration, + fcp: avgFCP, + avgFPS, + stdDevFPS, + maxMemory: avgMaxMemory, + avgAvgMemory, + memoryDelta, + systemInfo, + timeline: { + fps: runs[0].fpsSamples, + memory: runs[0].memorySamples, + timestamps: runs[0].timestamps, + }, + }; +} + +test.describe('Streaming Markdown Benchmark', async () => { + const results: Array = []; + + // 为每个文本长度类别创建测试组 + for (const textType of ['short', 'medium', 'long'] as const) { + test.describe(`${TEXT_CATEGORIES[textType].name}测试`, () => { + for (const rendererName of RENDERERS) { + test(`${rendererName}-${textType}`, async ({ page, mount, browserName }) => { + try { + test.setTimeout(BENCHMARK_CONFIG.TIMEOUT * RUN_COUNT); + const result = await measure({ + name: rendererName, + page, + mount, + browserName, + textType, + }); + results.push(result); + } catch (error) { + console.error(`Error in ${rendererName}-${textType}:`, error); + results.push({ + name: rendererName, + textLength: testTexts[textType].length, + textType, + duration: 0, + fcp: 0, + avgFPS: 0, + stdDevFPS: 0, + maxMemory: 0, + avgAvgMemory: 0, + memoryDelta: 0, + systemInfo: { userAgent: '', deviceMemory: 0, hardwareConcurrency: 0 }, + timeline: { fps: [], memory: [], timestamps: [] }, + }); + } + }); + } + }); + } + + test.afterAll(async () => { + if (results.length === 0) return; + + console.log('\n\n========================================================================'); + console.log('✅ 流式 Markdown 渲染 Benchmark 结果 (按文本长度分类)'); + console.log('========================================================================'); + + // 按文本类型分组显示结果 + const groupedResults = { + short: results.filter((r) => r.textType === 'short'), + medium: results.filter((r) => r.textType === 'medium'), + long: results.filter((r) => r.textType === 'long'), + }; + + for (const [type, typeResults] of Object.entries(groupedResults)) { + if (typeResults.length === 0) continue; + + console.log( + `\n📊 ${TEXT_CATEGORIES[type as keyof typeof TEXT_CATEGORIES].name} (${typeResults[0].textLength} 字符)`, + ); + console.log('='.repeat(50)); + + console.table( + typeResults.map((r) => ({ + Renderer: r.name, + 'Duration(s) ↓': (r.duration / 1000).toFixed(2), // 总渲染时间,越低越好 + 'FCP(s) ↓': (r.fcp / 1000).toFixed(2), // 首次内容绘制,越低越好 + 'Avg FPS ↑': r.avgFPS.toFixed(1), // 平均帧率,越高越好 + 'StdDev FPS ↓': r.stdDevFPS.toFixed(2), // 帧率标准差,越低越流畅 + 'Memory Delta(MB) ↓': (r.memoryDelta / 1024 / 1024).toFixed(2), // 内存增量,越低越好 + 'Avg Memory(MB) ↓': (r.avgAvgMemory / 1024 / 1024).toFixed(2), // 平均内存 + })), + ); + } + + // 显示跨文本长度的性能对比 + console.log('\n\n📈 跨文本长度性能对比'); + console.log('='.repeat(50)); + + const comparisonData = []; + for (const renderer of RENDERERS) { + const rendererResults = results.filter((r) => r.name === renderer); + if (rendererResults.length === 0) continue; + + const shortResult = rendererResults.find((r) => r.textType === 'short'); + const mediumResult = rendererResults.find((r) => r.textType === 'medium'); + const longResult = rendererResults.find((r) => r.textType === 'long'); + + comparisonData.push({ + Renderer: renderer, + '短文本(ms)': shortResult ? Math.round(shortResult.duration) : '-', + '中文本(ms)': mediumResult ? Math.round(mediumResult.duration) : '-', + '长文本(ms)': longResult ? Math.round(longResult.duration) : '-', + 性能衰减: + longResult && shortResult + ? `${((longResult.duration / shortResult.duration - 1) * 100).toFixed(1)}%` + : '-', + }); + } + + console.table(comparisonData); + + console.log('\nℹ️ 提示:'); + console.log(` - 分块大小: ${CHUNK_SIZE} 字符`); + console.log(` - 模拟流速: ${UPDATE_INTERVAL} ms/块`); + console.log(` - 测试配置: ${RUN_COUNT} 次运行取平均值`); + console.log(' - 性能分析: 关注 **FCP** (用户等待时间) 和 **StdDev FPS** (卡顿程度)。'); + }); +}); diff --git a/packages/x-markdown/src/XMarkdown/__benchmark__/tests/test.md b/packages/x-markdown/src/XMarkdown/__benchmark__/tests/test.md new file mode 100644 index 000000000..86d78c2e3 --- /dev/null +++ b/packages/x-markdown/src/XMarkdown/__benchmark__/tests/test.md @@ -0,0 +1,478 @@ +# Performance Test Document - Complex Markdown Structure Benchmark + +## 1. Introduction and Overview + +This document is specifically designed to test the performance of Markdown parsers, containing various complex Markdown structures and large amounts of content. By parsing this document, we can comprehensively evaluate the parser's performance metrics when handling large-scale, complex structures. + +### 1.1 Test Objectives + +- Evaluate the parser's ability to handle large files +- Test parsing efficiency of complex nested structures +- Verify support for various Markdown extended syntax +- Measure memory usage and CPU utilization + +### 1.2 Document Structure Description + +This document contains the following main sections: + +1. Multi-level heading structure +2. Large text content +3. Complex table structures +4. Various code blocks and syntax highlighting +5. Nested list structures +6. Quotes and annotations +7. Links and images +8. Mathematical formulas (if supported) + +## 2. Technical Specifications and Standards + +### 2.1 Markdown Standard Support + +| Feature | Standard Support | Extended Support | Notes | +| ------------- | ---------------- | ---------------- | ----------------------------- | +| Basic Syntax | ✅ | ✅ | Fully supported | +| Tables | ✅ | ✅ | Support alignment and nesting | +| Code Blocks | ✅ | ✅ | Support syntax highlighting | +| Task Lists | ❌ | ✅ | Requires extended support | +| Math Formulas | ❌ | ✅ | Requires MathJax or KaTeX | +| Flowcharts | ❌ | ✅ | Requires Mermaid support | +| Footnotes | ❌ | ✅ | Requires extended syntax | + +### 2.2 Performance Benchmark Requirements + +```yaml +performance: + parsing: + max_time: 1000ms + max_memory: 50MB + rendering: + max_time: 2000ms + max_memory: 100MB + file_size: + min: 100KB + max: 1MB +``` + +## 3. Detailed Content Sections + +### 3.1 Long-form Content Test + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. + +Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. + +Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. + +### 3.2 Nested Structure Test + +#### 3.2.1 Three-level Nested Headings + +##### 3.2.1.1 Four-level Nested Headings + +###### 3.2.1.1.1 Five-level Nested Headings + +This is a deeply nested heading structure used to test the parser's ability to handle deep-level headings. + +#### 3.2.2 List Nesting Test + +1. First-level list item + - Second-level unordered list + - Third-level unordered list + 1. Fourth-level ordered list + - Fifth-level mixed list + - Sixth-level deep nesting + - Seventh-level extreme nesting + - Eighth-level test + - Ninth-level boundary test + - Tenth-level maximum nesting + +2. Complex list item content test + - **Bold text** in lists + - _Italic text_ in lists + - `Code snippets` in lists + - [Link text](https://example.com) in lists + - ![Image alt](https://via.placeholder.com/50x50) in lists + +### 3.3 Code Block Test + +#### 3.3.1 Multi-language Code Highlighting + +```javascript +// JavaScript code example +class PerformanceTest { + constructor(name) { + this.name = name; + this.startTime = Date.now(); + } + + measureTime() { + const endTime = Date.now(); + return endTime - this.startTime; + } + + async runTests() { + const results = []; + for (let i = 0; i < 1000; i++) { + results.push(await this.singleTest(i)); + } + return results; + } +} + +const test = new PerformanceTest('markdown-parser'); +test.runTests().then(console.log); +``` + +```python +# Python code example +import asyncio +import time +from typing import List, Dict + +class MarkdownBenchmark: + def __init__(self, file_path: str): + self.file_path = file_path + self.results = [] + + async def parse_file(self) -> Dict[str, float]: + start_time = time.time() + # Simulate complex markdown parsing process + with open(self.file_path, 'r', encoding='utf-8') as f: + content = f.read() + # Complex parsing logic would be here + parsed = self._complex_parse(content) + + end_time = time.time() + return { + 'duration': end_time - start_time, + 'file_size': len(content), + 'parsed_length': len(parsed) + } + + def _complex_parse(self, content: str) -> str: + # Simulate complex parsing process + return content.upper() + +if __name__ == "__main__": + benchmark = MarkdownBenchmark("test.md") + asyncio.run(benchmark.parse_file()) +``` + +```sql +-- SQL query example +SELECT + p.id, + p.title, + p.content, + p.created_at, + COUNT(c.id) as comment_count, + AVG(r.rating) as avg_rating +FROM + posts p +LEFT JOIN + comments c ON p.id = c.post_id +LEFT JOIN + ratings r ON p.id = r.post_id +WHERE + p.status = 'published' + AND p.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) +GROUP BY + p.id +ORDER BY + avg_rating DESC, + comment_count DESC +LIMIT 100; +``` + +```json +{ + "benchmark": { + "name": "markdown-parser-performance", + "version": "1.0.0", + "tests": [ + { + "name": "parsing-speed", + "iterations": 1000, + "expected": { + "max_time_ms": 1000, + "max_memory_mb": 50 + } + }, + { + "name": "rendering-speed", + "iterations": 100, + "expected": { + "max_time_ms": 2000, + "max_memory_mb": 100 + } + } + ], + "metrics": { + "file_size": "1MB", + "complexity": "high", + "structures": ["tables", "code", "lists", "quotes", "math"] + } + } +} +``` + +#### 3.3.2 Line Number Display Test + +```javascript {1,3-5} +// This line shows line numbers +function test() { + // These lines also show line numbers + const a = 1; + const b = 2; + return a + b; // This line does not show line numbers +} +``` + +### 3.4 Math Formula Test (if supported) + +#### 3.4.1 Inline Formulas + +This is an inline math formula: $E=mc^2$, it should render correctly. +Inline formula with Greek: $\alpha + \beta = \gamma$. +Inline fraction: $\frac{a}{b} + \frac{c}{d} = \frac{ad+bc}{bd}$. + +#### 3.4.2 Block Formulas + +$$ +\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi} +$$ + +$$ +\begin{align} +\frac{d}{dx}\left( \int_{0}^{x} f(u)\,du\right) &= f(x) \\ +\frac{d}{dx}\left( \int_{a(x)}^{b(x)} f(u)\,du\right) &= f(b(x))b'(x) - f(a(x))a'(x) +\end{align} +$$ + +$$ +\sum_{i=1}^{n} i = \frac{n(n+1)}{2} +$$ + +$$ +\mathbf{A} = \begin{pmatrix} +1 & 2 & 3 \\ +4 & 5 & 6 \\ +7 & 8 & 9 +\end{pmatrix} +$$ + +$$ +\lim_{x \to 0} \frac{\sin x}{x} = 1 +$$ + +### 3.5 Task List Test + +#### 3.5.1 Basic Task Lists + +- [x] Completed task +- [ ] Incomplete task +- [x] Another completed task +- [ ] Todo item 1 +- [ ] Todo item 2 + +#### 3.5.2 Nested Task Lists + +- [x] Main task + - [x] Subtask 1 + - [ ] Subtask 2 + - [x] Sub-subtask 1 + - [ ] Sub-subtask 2 +- [ ] Independent task + +### 3.6 Table Complexity Test + +#### 3.6.1 Basic Complex Table + +| Header1 | Header2 | Header3 | Header4 | Header5 | +| --------------- | -------- | -------- | ------------------ | --------------------------- | +| Cell1 | Cell2 | Cell3 | Cell4 | Cell5 | +| Merge cell test | | | Span three columns | | +| New row1 | **Bold** | _Italic_ | `Code` | [Link](https://example.com) | + +#### 3.6.2 Nested Content Table + +| Feature | Description | Example | Status | +| ---------- | ----------------------- | ---------------------------------------------- | ------ | +| Images | Support image insertion | ![Example](https://via.placeholder.com/100x50) | ✅ | +| Math | Support LaTeX | $E=mc^2$ | ⚠️ | +| Flowcharts | Support Mermaid | `mermaid
graph LR
A-->B` | ❌ | +| Tables | Support complex tables | As shown above | ✅ | + +#### 3.6.3 Large Data Table + +| ID | Name | Type | Size | Created | Modified | Permissions | Owner | Group | Notes | +| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | +| 1 | document.md | file | 1024KB | 2024-01-01 10:00:00 | 2024-01-02 11:30:00 | 644 | user1 | group1 | Main document | +| 2 | image.png | file | 2048KB | 2024-01-01 10:30:00 | 2024-01-01 10:30:00 | 755 | user2 | group2 | Example image | +| 3 | folder | dir | 4096KB | 2024-01-01 09:00:00 | 2024-01-03 14:20:00 | 755 | user1 | group1 | Project folder | +| 4 | script.js | file | 512KB | 2024-01-02 15:00:00 | 2024-01-02 15:30:00 | 700 | user3 | group3 | Config file | +| 5 | data.json | file | 256KB | 2024-01-03 09:15:00 | 2024-01-03 09:15:00 | 644 | user1 | group1 | Data file | + +### 3.7 Quote and Annotation Test + +> This is a quote block used to test quote format parsing. +> +> Quotes can contain multi-line content and support internal formatting: +> +> - **Bold text** in quotes +> - _Italic text_ in quotes +> - `Code snippets` in quotes +> +> > Nested quote +> > +> > > Three-level nested quote +> > > +> > > This is the deepest quote content, containing [links](https://example.com) and **formatted text**. + +### 3.8 Link and Image Test + +#### 3.8.1 Various Link Formats + +- [Inline link](https://example.com) +- [Link with title](https://example.com 'Link title') +- [Relative link](../README.md) +- [Anchor link](#introduction-and-overview) +- +- [Reference link][reference] + +[reference]: https://example.com 'Reference link' + +#### 3.8.2 Image Test + +![Regular image](https://via.placeholder.com/200x100) ![Image with alt](https://via.placeholder.com/200x100 'Image title') ![Small icon](https://via.placeholder.com/16x16) + +## 4. Repetitive Content Test (increasing file size) + +### 4.1 Repeated Paragraph 1 + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +### 4.2 Repeated Paragraph 2 + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. + +### 4.3 Repeated Paragraph 3 + +Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. + +### 4.4 Repeated Paragraph 4 + +Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. + +### 4.5 Repeated Paragraph 5 + +Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. + +## 5. Large Table Data + +### 5.1 Performance Test Data Table + +| Test Case | File Size | Parse Time | Memory Usage | CPU Usage | Status | +| ---------------- | --------- | ---------- | ------------ | --------- | ------ | +| Small file | 1KB | 5ms | 2MB | 5% | ✅ | +| Medium file | 10KB | 25ms | 5MB | 15% | ✅ | +| Large file | 100KB | 150ms | 15MB | 45% | ✅ | +| Extra large file | 1MB | 1200ms | 50MB | 85% | ⚠️ | +| Extreme file | 10MB | 15000ms | 200MB | 95% | ❌ | + +### 5.2 Feature Support Matrix + +| Parser | Basic Syntax | Tables | Code Highlighting | Math Formulas | Flowcharts | Footnotes | Task Lists | +| --- | --- | --- | --- | --- | --- | --- | --- | +| ParserA | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| ParserB | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| ParserC | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| ParserD | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ✅ | + +## 6. Complex Nested Structure Test + +### 6.1 Code Blocks in Lists + +1. Code block in ordered list + + ```python + def test_function(): + print("This is a code block in an ordered list") + return True + ``` + +2. Another code block + ```javascript + const listItemCode = () => { + console.log('JavaScript code in list item'); + }; + ``` + +### 6.2 Lists in Quotes + +> Lists in quotes: +> +> 1. First item +> - Subitem 1 +> - Subitem 2 +> 2. Second item +> - Subitem A +> - Subitem B + +### 6.3 Code in Tables + +| Language | Example Code | Description | +| ---------- | ----------------------------------- | --------------- | +| Python | `print("Hello World")` | Simple output | +| JavaScript | `console.log("Hello World")` | Console output | +| Java | `System.out.println("Hello World")` | Standard output | + +## 7. Boundary Test Cases + +### 7.1 Special Character Test + +#### 7.1.1 Escape Characters + +\*This is not italic\* \*\*This is not bold\*\* \`This is not code\` \[This is not a link\](https://example.com) + +#### 7.1.2 HTML Entities + +
HTML entity test
+© 2024 Copyright +™ Trademark symbol +& " ' + +### 7.2 Extreme Format Test + +**Bold text with _italic_ text** _Italic text with **bold** text_ ~~Strikethrough text with **bold** text~~ + +### 7.3 Link Nesting Test + +- [Link with `code`](https://example.com) +- [Link with **bold**](https://example.com) +- [Link with _italic_](https://example.com) + +## 8. Summary + +This document contains rich Markdown structures and content for comprehensive testing of Markdown parser performance. By parsing this document containing various complex elements, we can effectively evaluate the parser's performance in real-world application scenarios. + +### 8.1 Test Points Summary + +1. **Parsing Speed**: Document size is approximately 1MB, contains large amounts of complex structures +2. **Memory Usage**: Requires processing large amounts of nested structures and formatted content +3. **Feature Completeness**: Tests support for various Markdown extended syntax +4. **Error Handling**: Includes boundary cases and special character tests + +### 8.2 Performance Metrics + +- Total document characters: ~15,000 characters +- Maximum heading level: 6 levels +- Number of tables: 10 +- Number of code blocks: 15 +- Maximum list nesting level: 10 levels +- Number of images: 5 +- Number of links: 20 + +--- + +_This document was generated in 2024, specifically for Markdown parser performance testing._ diff --git a/packages/x/docs/x-markdown/demo/streaming/animation.tsx b/packages/x/docs/x-markdown/demo/streaming/animation.tsx index f8f3011dc..662234509 100644 --- a/packages/x/docs/x-markdown/demo/streaming/animation.tsx +++ b/packages/x/docs/x-markdown/demo/streaming/animation.tsx @@ -6,8 +6,6 @@ import { useMarkdownTheme } from '../_utils'; import '@ant-design/x-markdown/themes/light.css'; import '@ant-design/x-markdown/themes/dark.css'; -const { Text } = Typography; - const text = ` # Ant Design X: The Ultimate AI Conversation UI Framework @@ -61,6 +59,8 @@ Based on the RICH interaction paradigm, we provide many atomic components for di > Ant Design X is more than just a component library—it's a complete solution for building the next generation of AI-powered applications. Start building today and create experiences that delight your users. `; +const { Text } = Typography; + const App = () => { const [enableAnimation, setEnableAnimation] = React.useState(true); const [hasNextChunk, setHasNextChunk] = React.useState(true);