2
0

bench.js 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. import { dirname, resolve } from 'node:path';
  2. import { fileURLToPath } from 'node:url';
  3. import { htmlIsEqual, getTests } from '@markedjs/testutils';
  4. import { marked } from '../lib/marked.esm.js';
  5. const __dirname = dirname(fileURLToPath(import.meta.url));
  6. /**
  7. * Load specs
  8. */
  9. export async function load() {
  10. const dir = resolve(__dirname, './specs/commonmark');
  11. const sections = await getTests(dir);
  12. let specs = [];
  13. for (const section in sections) {
  14. specs = specs.concat(sections[section].specs);
  15. }
  16. return specs;
  17. }
  18. /**
  19. * Run all benchmarks
  20. */
  21. export async function runBench(options) {
  22. options = options || {};
  23. const specs = await load();
  24. const tests = {};
  25. marked.setOptions({
  26. gfm: false,
  27. breaks: false,
  28. pedantic: false,
  29. });
  30. if (options.marked) {
  31. marked.setOptions(options.marked);
  32. }
  33. tests.marked = marked.parse;
  34. try {
  35. tests.commonmark = await (async() => {
  36. const { Parser, HtmlRenderer } = await import('commonmark');
  37. const parser = new Parser();
  38. const writer = new HtmlRenderer();
  39. return function(text) {
  40. return writer.render(parser.parse(text));
  41. };
  42. })();
  43. } catch(e) {
  44. console.error('Could not bench commonmark. (Error: %s)', e.message);
  45. }
  46. try {
  47. tests['markdown-it'] = await (async() => {
  48. const MarkdownIt = (await import('markdown-it')).default;
  49. const md = new MarkdownIt();
  50. return md.render.bind(md);
  51. })();
  52. } catch(e) {
  53. console.error('Could not bench markdown-it. (Error: %s)', e.message);
  54. }
  55. await bench(tests, specs);
  56. }
  57. export async function bench(tests, specs) {
  58. const stats = {};
  59. for (const name in tests) {
  60. stats[name] = {
  61. elapsed: 0n,
  62. correct: 0,
  63. };
  64. }
  65. console.log();
  66. for (let i = 0; i < specs.length; i++) {
  67. const spec = specs[i];
  68. process.stdout.write(
  69. `${((i * 100) / specs.length).toFixed(1).padStart(5)}% ${i
  70. .toString()
  71. .padStart(specs.length.toString().length)} of ${specs.length}\r`,
  72. );
  73. for (const name in tests) {
  74. const test = tests[name];
  75. const before = process.hrtime.bigint();
  76. for (let n = 0; n < 1e3; n++) {
  77. await test(spec.markdown);
  78. }
  79. const after = process.hrtime.bigint();
  80. stats[name].elapsed += after - before;
  81. stats[name].correct += (await htmlIsEqual(
  82. spec.html,
  83. await test(spec.markdown),
  84. ))
  85. ? 1
  86. : 0;
  87. }
  88. }
  89. for (const name in tests) {
  90. const ms = prettyElapsedTime(stats[name].elapsed);
  91. const percent = ((stats[name].correct / specs.length) * 100).toFixed(2);
  92. console.log(`${name} completed in ${ms}ms and passed ${percent}%`);
  93. }
  94. const percentSlower = ((
  95. prettyElapsedTime(stats.marked.elapsed)
  96. / prettyElapsedTime(stats.commonmark.elapsed)
  97. ) - 1) * 100;
  98. console.log(`${Math.round(percentSlower)}% slower than commonmark`);
  99. }
  100. /**
  101. * Argument Parsing
  102. */
  103. function parseArg(argv) {
  104. argv = argv.slice(2);
  105. const options = {};
  106. const orphans = [];
  107. function getArg() {
  108. let arg = argv.shift();
  109. if (arg.indexOf('--') === 0) {
  110. // e.g. --opt
  111. arg = arg.split('=');
  112. if (arg.length > 1) {
  113. // e.g. --opt=val
  114. argv.unshift(arg.slice(1).join('='));
  115. }
  116. arg = arg[0];
  117. } else if (arg[0] === '-') {
  118. if (arg.length > 2) {
  119. // e.g. -abc
  120. argv = arg
  121. .substring(1)
  122. .split('')
  123. .map((ch) => `-${ch}`)
  124. .concat(argv);
  125. arg = argv.shift();
  126. } else {
  127. // e.g. -a
  128. }
  129. } else {
  130. // e.g. foo
  131. }
  132. return arg;
  133. }
  134. const defaults = marked.getDefaults();
  135. while (argv.length) {
  136. const arg = getArg();
  137. if (arg.indexOf('--') === 0) {
  138. const opt = camelize(arg.replace(/^--(no-)?/, ''));
  139. if (!(opt in defaults)) {
  140. continue;
  141. }
  142. options.marked = options.marked || {};
  143. if (arg.indexOf('--no-') === 0) {
  144. options.marked[opt] = typeof defaults[opt] !== 'boolean' ? null : false;
  145. } else {
  146. options.marked[opt] =
  147. typeof defaults[opt] !== 'boolean' ? argv.shift() : true;
  148. }
  149. } else {
  150. orphans.push(arg);
  151. }
  152. }
  153. if (orphans.length > 0) {
  154. console.error();
  155. console.error('The following arguments are not used:');
  156. orphans.forEach((arg) => console.error(` ${arg}`));
  157. console.error();
  158. }
  159. return options;
  160. }
  161. /**
  162. * Helpers
  163. */
  164. function camelize(text) {
  165. return text.replace(/(\w)-(\w)/g, (_, a, b) => a + b.toUpperCase());
  166. }
  167. /**
  168. * Main
  169. */
  170. export default async function main(argv) {
  171. const opt = parseArg(argv);
  172. await runBench(opt);
  173. }
  174. /**
  175. * returns time to millisecond granularity
  176. * @param hrtimeElapsed {bigint}
  177. */
  178. function prettyElapsedTime(hrtimeElapsed) {
  179. return Number(hrtimeElapsed / 1_000_000n);
  180. }
  181. process.title = 'marked bench';
  182. main(process.argv.slice());