Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
20 changes: 18 additions & 2 deletions packages/x-markdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
122 changes: 122 additions & 0 deletions packages/x-markdown/src/XMarkdown/__benchmark__/README.md
Original file line number Diff line number Diff line change
@@ -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`
````
Original file line number Diff line number Diff line change
@@ -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<MarkdownRendererProps> = (props) => (
<div
className="markdown-container"
// biome-ignore lint/security/noDangerouslySetInnerHtml: benchmark only
dangerouslySetInnerHTML={{ __html: marked.parse(props.md) as string }}
/>
);

const MarkdownItRenderer: FC<MarkdownRendererProps> = (props) => {
return (
// biome-ignore lint/security/noDangerouslySetInnerHtml: benchmark only
<div className="markdown-container" dangerouslySetInnerHTML={{ __html: md.render(props.md) }} />
);
};

const ReactMarkdownRenderer: FC<MarkdownRendererProps> = (props) => (
<div className="markdown-container">
<ReactMarkdown rehypePlugins={[rehypeRaw, rehypeKatex]} remarkPlugins={[remarkGfm, remarkMath]}>
{props.md}
</ReactMarkdown>
</div>
);

const XMarkdownRenderer: FC<MarkdownRendererProps> = (props) => (
<div className="markdown-container">
<XMarkdown
streaming={{ hasNextChunk: props?.hasNextChunk, enableAnimation: true }}
config={{ extensions: getLatexPlugin() }}
>
{props.md}
</XMarkdown>
</div>
);

const StreamdownRenderer: FC<MarkdownRendererProps> = (props) => (
<div className="markdown-container">
<Streamdown rehypePlugins={[rehypeRaw, rehypeKatex]} remarkPlugins={[remarkGfm, remarkMath]}>
{props.md}
</Streamdown>
</div>
);

const Empty = () => <div />;

export {
MarkedRenderer,
MarkdownItRenderer,
ReactMarkdownRenderer,
XMarkdownRenderer,
StreamdownRenderer,
Empty,
};
Original file line number Diff line number Diff line change
@@ -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'] },
},
],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
<script type="module" crossorigin src="/assets/index-BiIEcWSo.js"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Page</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="./index.tsx"></script>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Import styles, initialize component theme here.
// import '../src/common.css';
Original file line number Diff line number Diff line change
@@ -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);
Loading
Loading