1import { readFileSync, createReadStream, readdirSync } from 'node:fs';
2import Benchmark from 'benchmark';
3import { loadTreeConstructionTestData } from 'parse5-test-utils/dist/generate-parsing-tests.js';
4import { loadSAXParserTestData } from 'parse5-test-utils/dist/load-sax-parser-test-data.js';
5import { treeAdapters, WritableStreamStub, finished } from 'parse5-test-utils/dist/common.js';
6import * as parse5 from '../../packages/parse5/dist/index.js';
7import { ParserStream as parse5Stream } from '../../packages/parse5-parser-stream/dist/index.js';
8import * as parse5Upstream from 'parse5';
9
10const hugePagePath = new URL('../../test/data/huge-page/huge-page.html', import.meta.url);
11const treeConstructionPath = new URL('../../test/data/html5lib-tests/tree-construction', import.meta.url);
12const saxPath = new URL('../../test/data/sax/', import.meta.url);
13
14//HACK: https://github.com/bestiejs/benchmark.js/issues/51
15/* global workingCopy, WorkingCopyParserStream, upstreamParser, hugePage, microTests, runMicro, runPages, files */
16global.workingCopy = parse5;
17global.WorkingCopyParserStream = parse5Stream;
18global.upstreamParser = parse5Upstream;
19
20// Huge page data
21global.hugePage = readFileSync(hugePagePath).toString();
22
23// Micro data
24global.microTests = loadTreeConstructionTestData(treeConstructionPath, treeAdapters.default)
25    .filter(
26        (test) =>
27            //NOTE: this test caused a stack overflow in parse5 v1.x
28            test.input !== '<button><p><button>'
29    )
30    .map((test) => ({
31        html: test.input,
32        fragmentContext: test.fragmentContext,
33    }));
34
35global.runMicro = function (parser) {
36    for (const test of microTests) {
37        if (test.fragmentContext) {
38            parser.parseFragment(test.fragmentContext, test.html);
39        } else {
40            parser.parse(test.html);
41        }
42    }
43};
44
45// Pages data
46const pages = loadSAXParserTestData().map((test) => test.src);
47
48global.runPages = function (parser) {
49    for (const page of pages) {
50        parser.parse(page);
51    }
52};
53
54// Stream data
55global.files = readdirSync(saxPath).map((dirName) => new URL(`${dirName}/src.html`, saxPath).pathname);
56
57// Utils
58function getHz(suite, testName) {
59    for (let i = 0; i < suite.length; i++) {
60        if (suite[i].name === testName) {
61            return suite[i].hz;
62        }
63    }
64}
65
66function runBench({ name, workingCopyFn, upstreamFn, defer = false }) {
67    const suite = new Benchmark.Suite(name);
68
69    suite
70        .add('Working copy', workingCopyFn, { defer })
71        .add('Upstream', upstreamFn, { defer })
72        .on('start', () => console.log(name))
73        .on('cycle', (event) => console.log(String(event.target)))
74        .on('complete', () => {
75            const workingCopyHz = getHz(suite, 'Working copy');
76            const upstreamHz = getHz(suite, 'Upstream');
77
78            if (workingCopyHz > upstreamHz) {
79                console.log(`Working copy is ${(workingCopyHz / upstreamHz).toFixed(2)}x faster.\n`);
80            } else {
81                console.log(`Working copy is ${(upstreamHz / workingCopyHz).toFixed(2)}x slower.\n`);
82            }
83        })
84        .run();
85}
86
87// Benchmarks
88runBench({
89    name: 'parse5 regression benchmark - MICRO',
90    workingCopyFn: () => runMicro(workingCopy),
91    upstreamFn: () => runMicro(upstreamParser),
92});
93
94runBench({
95    name: 'parse5 regression benchmark - HUGE',
96    workingCopyFn: () => workingCopy.parse(hugePage),
97    upstreamFn: () => upstreamParser.parse(hugePage),
98});
99
100runBench({
101    name: 'parse5 regression benchmark - PAGES',
102    workingCopyFn: () => runPages(workingCopy),
103    upstreamFn: () => runPages(upstreamParser),
104});
105
106runBench({
107    name: 'parse5 regression benchmark - STREAM',
108    defer: true,
109    workingCopyFn: async (deferred) => {
110        const parsePromises = files.map((fileName) => {
111            const stream = createReadStream(fileName, 'utf8');
112            const parserStream = new WorkingCopyParserStream();
113
114            stream.pipe(parserStream);
115            return finished(parserStream);
116        });
117
118        await Promise.all(parsePromises);
119        deferred.resolve();
120    },
121    upstreamFn: async (deferred) => {
122        const parsePromises = files.map(async (fileName) => {
123            const stream = createReadStream(fileName, 'utf8');
124            const writable = new WritableStreamStub();
125
126            stream.pipe(writable);
127
128            await finished(writable);
129
130            upstreamParser.parse(writable.writtenData);
131        });
132
133        await Promise.all(parsePromises);
134        deferred.resolve();
135    },
136});
137