Skip to content

Commit af8ecaf

Browse files
committed
Add benchmark runner
1 parent 33743cd commit af8ecaf

7 files changed

Lines changed: 365 additions & 1 deletion

File tree

.github/workflows/benchmark.yml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
name: Performance Benchmarks
2+
3+
on:
4+
# Run on pushes to main branches and feature branches with 'perf' in name
5+
push:
6+
branches: [ master, 'feature/*perf*' ]
7+
8+
# Allow manual triggering
9+
workflow_dispatch:
10+
inputs:
11+
reason:
12+
description: 'Reason for running benchmark'
13+
required: false
14+
default: 'Manual benchmark run'
15+
16+
# Run on release
17+
release:
18+
types: [published]
19+
20+
jobs:
21+
benchmark:
22+
runs-on: ubuntu-latest
23+
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@v4
27+
28+
- name: Use Node.js LTS
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: 'lts/*'
32+
33+
- name: Install dependencies
34+
run: npm ci
35+
36+
- name: Run benchmark
37+
run: |
38+
echo "Starting benchmark run..."
39+
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
40+
echo "Reason: ${{ github.event.inputs.reason }}"
41+
fi
42+
xvfb-run -a npm run benchmark

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
out
22
node_modules
33
.vscode-test
4-
*.vsix
4+
*.vsix
5+
benchmark-results-*.json

.vscode/launch.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,19 @@
2323
],
2424
"outFiles": [ "${workspaceRoot}/out/test/**/*.js" ],
2525
"preLaunchTask": "compileAndCopyTestResources"
26+
},
27+
{
28+
"name": "Launch Benchmarks",
29+
"type": "extensionHost",
30+
"request": "launch",
31+
"runtimeExecutable": "${execPath}",
32+
"args": [
33+
"--disable-extensions",
34+
"--extensionDevelopmentPath=${workspaceRoot}",
35+
"--extensionTestsPath=${workspaceRoot}/out/test/systemTests/benchmarkRunner.js"
36+
],
37+
"outFiles": [ "${workspaceRoot}/out/test/**/*.js" ],
38+
"preLaunchTask": "compileAndCopyTestResources"
2639
}
2740
]
2841
}

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[![OVSX](https://img.shields.io/open-vsx/v/darkriszty/markdown-table-prettify?color=success&label=Open%20VSX)](https://open-vsx.org/extension/darkriszty/markdown-table-prettify)
66
[![Docker image](https://img.shields.io/docker/v/darkriszty/prettify-md?color=success&label=Docker)](https://hub.docker.com/r/darkriszty/prettify-md/tags?page=1&ordering=last_updated)
77
[![NPM package](https://img.shields.io/npm/v/markdown-table-prettify?color=success)](https://www.npmjs.com/package/markdown-table-prettify)
8+
[![Benchmarks](https://github.com/darkriszty/MarkdownTablePrettify-VSCodeExt/actions/workflows/benchmark.yml/badge.svg)](https://github.com/darkriszty/MarkdownTablePrettify-VSCodeExt/actions/workflows/benchmark.yml)
89

910
Makes tables more readable for humans. Compatible with the Markdown writer plugin's table formatter feature in Atom.
1011

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"compile": "tsc -p ./",
7979
"pretest": "npm run compile",
8080
"test": "npx gulp copy-systemTest-resources && node ./out/test/index.js",
81+
"benchmark": "npm run compile && npx gulp copy-systemTest-resources && node ./out/test/systemTests/benchmarkTestRunner.js",
8182
"prettify-md": "node ./out/cli/index.js",
8283
"check-md": "node ./out/cli/index.js --check"
8384
},
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import * as vscode from 'vscode';
4+
import { getDistinctTestFileNames } from './systemTestFileReader';
5+
import { getDocumentPrettyfier } from '../../src/extension/prettyfierFactory';
6+
7+
interface BenchmarkResults {
8+
factoryCreation: {
9+
iterations: number;
10+
times: number[];
11+
average: number;
12+
median: number;
13+
min: number;
14+
max: number;
15+
totalDuration: number;
16+
};
17+
documentFormatting: {
18+
[size: string]: {
19+
files: string[];
20+
iterations: number;
21+
times: number[];
22+
average: number;
23+
median: number;
24+
min: number;
25+
max: number;
26+
totalDuration: number;
27+
};
28+
};
29+
overallDuration: number;
30+
timestamp: string;
31+
}
32+
33+
class PerformanceBenchmark {
34+
private results: BenchmarkResults;
35+
private testFiles: { name: string; size: 'small' | 'medium' | 'large' }[] = [];
36+
private overallStartTime: bigint = BigInt(0);
37+
38+
constructor() {
39+
this.results = {
40+
factoryCreation: {
41+
iterations: 0,
42+
times: [],
43+
average: 0,
44+
median: 0,
45+
min: 0,
46+
max: 0,
47+
totalDuration: 0
48+
},
49+
documentFormatting: {},
50+
overallDuration: 0,
51+
timestamp: new Date().toISOString()
52+
};
53+
}
54+
55+
async loadTestFiles(): Promise<void> {
56+
const resourcesDir = path.resolve(__dirname, "resources/");
57+
const files = fs.readdirSync(resourcesDir);
58+
const distinctTests = getDistinctTestFileNames(files);
59+
60+
for (const fileNameRoot of distinctTests) {
61+
// Determine file size based on content
62+
const inputPath = path.join(resourcesDir, `${fileNameRoot}-input.md`);
63+
if (fs.existsSync(inputPath)) {
64+
const content = fs.readFileSync(inputPath, 'utf-8');
65+
const size = this.categorizeFileSize(content);
66+
this.testFiles.push({ name: fileNameRoot, size });
67+
}
68+
}
69+
70+
console.log(`Loaded ${this.testFiles.length} test files:`);
71+
const sizeGroups = this.testFiles.reduce((acc, file) => {
72+
acc[file.size] = (acc[file.size] || 0) + 1;
73+
return acc;
74+
}, {} as Record<string, number>);
75+
console.log(` Small: ${sizeGroups.small || 0}`);
76+
console.log(` Medium: ${sizeGroups.medium || 0}`);
77+
console.log(` Large: ${sizeGroups.large || 0}`);
78+
}
79+
80+
private categorizeFileSize(content: string): 'small' | 'medium' | 'large' {
81+
const lines = content.split('\n').length;
82+
if (lines <= 20) return 'small';
83+
if (lines <= 50) return 'medium';
84+
return 'large';
85+
}
86+
87+
async benchmarkFactoryCreation(iterations: number = 100000): Promise<void> {
88+
console.log('📦 Testing factory creation overhead...');
89+
const times: number[] = [];
90+
const sectionStart = process.hrtime.bigint();
91+
92+
for (let i = 0; i < iterations; i++) {
93+
const start = process.hrtime.bigint();
94+
getDocumentPrettyfier();
95+
const end = process.hrtime.bigint();
96+
times.push(Number(end - start) / 1_000_000); // Convert to milliseconds
97+
}
98+
99+
const sectionEnd = process.hrtime.bigint();
100+
const totalDuration = Number(sectionEnd - sectionStart) / 1_000_000; // Convert to milliseconds
101+
102+
this.results.factoryCreation = {
103+
iterations,
104+
times,
105+
average: times.reduce((a, b) => a + b, 0) / times.length,
106+
median: this.calculateMedian(times),
107+
min: Math.min(...times),
108+
max: Math.max(...times),
109+
totalDuration
110+
};
111+
}
112+
113+
async benchmarkDocumentFormatting(): Promise<void> {
114+
console.log('📄 Testing document formatting by size...');
115+
116+
const sizeGroups = this.testFiles.reduce((acc, file) => {
117+
if (!acc[file.size]) acc[file.size] = [];
118+
acc[file.size].push(file);
119+
return acc;
120+
}, {} as Record<string, typeof this.testFiles>);
121+
122+
for (const [size, files] of Object.entries(sizeGroups)) {
123+
const iterations = this.getIterationsForSize(size as any);
124+
const times: number[] = [];
125+
const prettyfier = getDocumentPrettyfier();
126+
const sectionStart = process.hrtime.bigint();
127+
128+
for (let i = 0; i < iterations; i++) {
129+
const file = files[i % files.length];
130+
const document = await this.openDocument(`resources/${file.name}-input.md`);
131+
132+
const start = process.hrtime.bigint();
133+
prettyfier.provideDocumentFormattingEdits(document, {} as any, {} as any);
134+
const end = process.hrtime.bigint();
135+
136+
times.push(Number(end - start) / 1_000_000);
137+
}
138+
139+
const sectionEnd = process.hrtime.bigint();
140+
const totalDuration = Number(sectionEnd - sectionStart) / 1_000_000; // Convert to milliseconds
141+
142+
this.results.documentFormatting[size] = {
143+
files: files.map(f => f.name),
144+
iterations,
145+
times,
146+
average: times.reduce((a, b) => a + b, 0) / times.length,
147+
median: this.calculateMedian(times),
148+
min: Math.min(...times),
149+
max: Math.max(...times),
150+
totalDuration
151+
};
152+
}
153+
}
154+
155+
private async openDocument(relativePath: string): Promise<vscode.TextDocument> {
156+
const fullPath = path.resolve(__dirname, relativePath);
157+
const uri = vscode.Uri.file(fullPath);
158+
return await vscode.workspace.openTextDocument(uri);
159+
}
160+
161+
private getIterationsForSize(size: 'small' | 'medium' | 'large'): number {
162+
switch (size) {
163+
case 'small': return 15000;
164+
case 'medium': return 10000;
165+
case 'large': return 750;
166+
default: return 10000;
167+
}
168+
}
169+
170+
private calculateMedian(times: number[]): number {
171+
const sorted = [...times].sort((a, b) => a - b);
172+
const mid = Math.floor(sorted.length / 2);
173+
return sorted.length % 2 === 0
174+
? (sorted[mid - 1] + sorted[mid]) / 2
175+
: sorted[mid];
176+
}
177+
178+
async runFullBenchmark(): Promise<void> {
179+
console.log('🚀 Starting Performance Benchmark Suite');
180+
console.log(`\nUsing ${this.testFiles.length} real test files from system tests\n`);
181+
182+
this.overallStartTime = process.hrtime.bigint();
183+
184+
await this.benchmarkFactoryCreation();
185+
await this.benchmarkDocumentFormatting();
186+
187+
const overallEnd = process.hrtime.bigint();
188+
this.results.overallDuration = Number(overallEnd - this.overallStartTime) / 1_000_000; // Convert to milliseconds
189+
190+
this.printResults();
191+
this.saveResults();
192+
}
193+
194+
private printResults(): void {
195+
console.log('\n' + '='.repeat(100));
196+
console.log('📊 PERFORMANCE BENCHMARK RESULTS');
197+
console.log('='.repeat(100));
198+
199+
// Factory creation results
200+
const factory = this.results.factoryCreation;
201+
console.log(`\n🎯 Factory Creation:`);
202+
console.log(` Iterations: ${factory.iterations}`);
203+
console.log(` Average: ${factory.average.toFixed(3)}ms`);
204+
console.log(` Median: ${factory.median.toFixed(3)}ms`);
205+
console.log(` Min: ${factory.min.toFixed(3)}ms`);
206+
console.log(` Max: ${factory.max.toFixed(3)}ms`);
207+
console.log(` Total Duration: ${factory.totalDuration.toFixed(3)}ms`);
208+
209+
// Document formatting results
210+
for (const [size, results] of Object.entries(this.results.documentFormatting)) {
211+
console.log(`\n🎯 Document Formatting (${size}):`);
212+
const fileList = results.files.length <= 3
213+
? results.files.join(', ')
214+
: `${results.files.slice(0, 3).join(', ')}...`;
215+
console.log(` Test files: ${results.files.length} files (${fileList})`);
216+
console.log(` Iterations: ${results.iterations}`);
217+
console.log(` Average: ${results.average.toFixed(3)}ms`);
218+
console.log(` Median: ${results.median.toFixed(3)}ms`);
219+
console.log(` Min: ${results.min.toFixed(3)}ms`);
220+
console.log(` Max: ${results.max.toFixed(3)}ms`);
221+
console.log(` Total Duration: ${results.totalDuration.toFixed(3)}ms`);
222+
}
223+
224+
console.log('\n' + '='.repeat(100));
225+
console.log(`⏱️ OVERALL BENCHMARK DURATION: ${this.results.overallDuration.toFixed(3)}ms`);
226+
console.log('='.repeat(100));
227+
console.log('💡 TIP: Run this benchmark before and after code changes to measure improvements!');
228+
console.log('='.repeat(100));
229+
}
230+
231+
private saveResults(): void {
232+
// Skip file creation in CI environments
233+
const isCI = process.env.CI || process.env.GITHUB_ACTIONS;
234+
if (isCI) {
235+
console.log('\n📊 Running in CI - skipping file creation');
236+
return;
237+
}
238+
239+
const fileName = `benchmark-results-${this.results.timestamp.replace(/[:.]/g, '-')}.json`;
240+
const filePath = path.resolve(__dirname, '../../..', fileName);
241+
242+
// Create a copy of results without the "times" arrays
243+
const resultsToSave = {
244+
...this.results,
245+
factoryCreation: {
246+
...this.results.factoryCreation,
247+
times: undefined // Exclude times array
248+
},
249+
documentFormatting: Object.fromEntries(
250+
Object.entries(this.results.documentFormatting).map(([size, data]) => [
251+
size,
252+
{
253+
...data,
254+
times: undefined // Exclude times arrays
255+
}
256+
])
257+
)
258+
};
259+
260+
fs.writeFileSync(filePath, JSON.stringify(resultsToSave, null, 2));
261+
console.log(`\n💾 Results saved to: ${fileName}`);
262+
}
263+
}
264+
265+
// Standalone benchmark runner
266+
async function runBenchmark() {
267+
const benchmark = new PerformanceBenchmark();
268+
await benchmark.loadTestFiles();
269+
await benchmark.runFullBenchmark();
270+
}
271+
272+
// Export for potential use in other contexts
273+
export { PerformanceBenchmark, runBenchmark };
274+
275+
// VS Code test runner entry point (for benchmark-only execution)
276+
export async function run(): Promise<void> {
277+
console.log('Starting VS Code Performance Benchmark...\n');
278+
try {
279+
await runBenchmark();
280+
console.log('\n✅ Benchmark completed successfully!');
281+
} catch (error) {
282+
console.error('\n❌ Benchmark failed:', error);
283+
throw error;
284+
}
285+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import * as path from 'path';
2+
import { runTests } from '@vscode/test-electron';
3+
4+
async function main() {
5+
try {
6+
const extensionDevelopmentPath = path.resolve(__dirname, '../../');
7+
8+
const extensionTestsPath = path.resolve(__dirname, './benchmarkRunner.js');
9+
10+
await runTests({
11+
extensionDevelopmentPath,
12+
extensionTestsPath,
13+
launchArgs: ['--disable-extensions'] // Run without other extensions for cleaner benchmark
14+
});
15+
} catch (err) {
16+
console.error('Failed to run benchmark');
17+
process.exit(1);
18+
}
19+
}
20+
21+
main();

0 commit comments

Comments
 (0)