OurBigBook logoOurBigBook Docs OurBigBook logoOurBigBook.comSite Source code
marked/test/bench.js
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { htmlIsEqual, getTests } from '@markedjs/testutils';

import { marked } from '../lib/marked.esm.js';

const __dirname = dirname(fileURLToPath(import.meta.url));

/**
 * Load specs
 */
export async function load() {
  const dir = resolve(__dirname, './specs/commonmark');
  const sections = await getTests(dir);
  let specs = [];

  for (const section in sections) {
    specs = specs.concat(sections[section].specs);
  }

  return specs;
}

/**
 * Run all benchmarks
 */
export async function runBench(options) {
  options = options || {};
  const specs = await load();
  const tests = {};

  marked.setOptions({
    gfm: false,
    breaks: false,
    pedantic: false,
  });
  if (options.marked) {
    marked.setOptions(options.marked);
  }
  tests.marked = marked.parse;

  try {
    tests.commonmark = await (async() => {
      const { Parser, HtmlRenderer } = await import('commonmark');
      const parser = new Parser();
      const writer = new HtmlRenderer();
      return function(text) {
        return writer.render(parser.parse(text));
      };
    })();
  } catch(e) {
    console.error('Could not bench commonmark. (Error: %s)', e.message);
  }

  try {
    tests['markdown-it'] = await (async() => {
      const MarkdownIt = (await import('markdown-it')).default;
      const md = new MarkdownIt();
      return md.render.bind(md);
    })();
  } catch(e) {
    console.error('Could not bench markdown-it. (Error: %s)', e.message);
  }

  await bench(tests, specs);
}

export async function bench(tests, specs) {
  const stats = {};
  for (const name in tests) {
    stats[name] = {
      elapsed: 0n,
      correct: 0,
    };
  }

  console.log();
  for (let i = 0; i < specs.length; i++) {
    const spec = specs[i];
    process.stdout.write(
      `${((i * 100) / specs.length).toFixed(1).padStart(5)}% ${i
        .toString()
        .padStart(specs.length.toString().length)} of ${specs.length}\r`,
    );
    for (const name in tests) {
      const test = tests[name];
      const before = process.hrtime.bigint();
      for (let n = 0; n < 1e3; n++) {
        await test(spec.markdown);
      }
      const after = process.hrtime.bigint();
      stats[name].elapsed += after - before;
      stats[name].correct += (await htmlIsEqual(
        spec.html,
        await test(spec.markdown),
      ))
        ? 1
        : 0;
    }
  }

  for (const name in tests) {
    const ms = prettyElapsedTime(stats[name].elapsed);
    const percent = ((stats[name].correct / specs.length) * 100).toFixed(2);
    console.log(`${name} completed in ${ms}ms and passed ${percent}%`);
  }

  const percentSlower = ((
    prettyElapsedTime(stats.marked.elapsed)
    / prettyElapsedTime(stats.commonmark.elapsed)
  ) - 1) * 100;

  console.log(`${Math.round(percentSlower)}% slower than commonmark`);
}

/**
 * Argument Parsing
 */
function parseArg(argv) {
  argv = argv.slice(2);

  const options = {};
  const orphans = [];

  function getArg() {
    let arg = argv.shift();

    if (arg.indexOf('--') === 0) {
      // e.g. --opt
      arg = arg.split('=');
      if (arg.length > 1) {
        // e.g. --opt=val
        argv.unshift(arg.slice(1).join('='));
      }
      arg = arg[0];
    } else if (arg[0] === '-') {
      if (arg.length > 2) {
        // e.g. -abc
        argv = arg
          .substring(1)
          .split('')
          .map((ch) => `-${ch}`)
          .concat(argv);
        arg = argv.shift();
      } else {
        // e.g. -a
      }
    } else {
      // e.g. foo
    }

    return arg;
  }

  const defaults = marked.getDefaults();

  while (argv.length) {
    const arg = getArg();
    if (arg.indexOf('--') === 0) {
      const opt = camelize(arg.replace(/^--(no-)?/, ''));
      if (!(opt in defaults)) {
        continue;
      }
      options.marked = options.marked || {};
      if (arg.indexOf('--no-') === 0) {
        options.marked[opt] = typeof defaults[opt] !== 'boolean' ? null : false;
      } else {
        options.marked[opt] =
          typeof defaults[opt] !== 'boolean' ? argv.shift() : true;
      }
    } else {
      orphans.push(arg);
    }
  }

  if (orphans.length > 0) {
    console.error();
    console.error('The following arguments are not used:');
    orphans.forEach((arg) => console.error(`  ${arg}`));
    console.error();
  }

  return options;
}

/**
 * Helpers
 */
function camelize(text) {
  return text.replace(/(\w)-(\w)/g, (_, a, b) => a + b.toUpperCase());
}

/**
 * Main
 */
export default async function main(argv) {
  const opt = parseArg(argv);

  await runBench(opt);
}

/**
 * returns time to millisecond granularity
 * @param hrtimeElapsed {bigint}
 */
function prettyElapsedTime(hrtimeElapsed) {
  return Number(hrtimeElapsed / 1_000_000n);
}

process.title = 'marked bench';
main(process.argv.slice());