benchmark.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. #!/usr/bin/env php
  2. <?php
  3. /*
  4. * This file is part of the league/commonmark package.
  5. *
  6. * (c) Colin O'Dell <colinodell@gmail.com>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. require_once __DIR__ . '/../../vendor/autoload.php';
  12. use League\CommonMark\CommonMarkConverter;
  13. use League\CommonMark\Environment\Environment;
  14. use League\CommonMark\Extension\Attributes\AttributesExtension;
  15. use League\CommonMark\Extension\Autolink\AutolinkExtension;
  16. use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
  17. use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
  18. use League\CommonMark\Extension\DescriptionList\DescriptionListExtension;
  19. use League\CommonMark\Extension\DisallowedRawHtml\DisallowedRawHtmlExtension;
  20. use League\CommonMark\Extension\ExternalLink\ExternalLinkExtension;
  21. use League\CommonMark\Extension\Footnote\FootnoteExtension;
  22. use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
  23. use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
  24. use League\CommonMark\Extension\Mention\MentionExtension;
  25. use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
  26. use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
  27. use League\CommonMark\Extension\Table\Table;
  28. use League\CommonMark\Extension\Table\TableExtension;
  29. use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
  30. use League\CommonMark\Extension\TaskList\TaskListExtension;
  31. use League\CommonMark\GithubFlavoredMarkdownConverter;
  32. use League\CommonMark\MarkdownConverter;
  33. use Michelf\Markdown;
  34. use Michelf\MarkdownExtra;
  35. $config = [
  36. 'exec' => array_shift($argv),
  37. 'md' => sprintf('%s/sample.md', __DIR__),
  38. 'iterations' => 20,
  39. 'parser' => [],
  40. 'csv' => false,
  41. 'flags' => [
  42. 'isolate' => false,
  43. 'memory' => false,
  44. ],
  45. ];
  46. $usage = function (array $config, string $format, ...$args) {
  47. $errmsg = vsprintf("Error: {$format}", $args);
  48. fwrite(
  49. STDERR,
  50. <<<EOD
  51. {$errmsg}:
  52. Usage: {$config['exec']} [options] [flags]
  53. Options:
  54. --md file default: sample.md
  55. --iterations num default: 20
  56. --parser name default: all
  57. --csv fd|file default: disabled
  58. Flags:
  59. -isolate default: off
  60. -memory default: off, implies isolate
  61. EOD
  62. );
  63. exit(1);
  64. };
  65. while ($key = array_shift($argv)) {
  66. switch ($key[0]) {
  67. case '-': switch ($key[1]) {
  68. case '-':
  69. $key = substr($key, 2);
  70. if (!isset($config[$key])) {
  71. $usage($config, 'invalid option %s', $key);
  72. }
  73. if (is_array($config[$key])) {
  74. $config[$key][] = array_shift($argv);
  75. } else {
  76. $config[$key] = array_shift($argv);
  77. }
  78. break;
  79. default:
  80. $key = substr($key, 1);
  81. if (!isset($config['flags'][$key])) {
  82. $usage($config, 'invalid flag %s', $key);
  83. }
  84. $config['flags'][$key] = true;
  85. } break;
  86. default: $usage($config, $key);
  87. }
  88. }
  89. $config['iterations'] = max($config['iterations'], 20);
  90. if ($config['md'] !== '-' && !file_exists($config['md'])) {
  91. $usage($config, 'cannot read input %s', $config['md']);
  92. }
  93. if ($config['flags']['memory']) {
  94. $config['flags']['isolate'] = true;
  95. }
  96. if ($config['flags']['isolate'] && !function_exists('proc_open')) {
  97. $usage($config, 'isolation requires proc_open');
  98. }
  99. if ($config['csv'] !== false) {
  100. $stream = ctype_digit($config['csv']) ?
  101. "php://fd/{$config['csv']}" :
  102. realpath($config['csv']);
  103. $fd = @fopen(
  104. $stream,
  105. $config['exec'] === 'exec' ?
  106. 'w' : 'w+'
  107. );
  108. if (!is_resource($fd)) {
  109. $usage(
  110. $config,
  111. 'cannot fopen(%s) for writing',
  112. $config['csv']
  113. );
  114. }
  115. define('CSVOUT', $fd);
  116. if ($config['exec'] !== 'exec') {
  117. fputcsv(
  118. CSVOUT,
  119. ['Name', 'CPU', 'MEM']
  120. );
  121. fflush(CSVOUT);
  122. }
  123. }
  124. if ($config['md'] === '-') {
  125. $config['md'] = stream_get_contents(STDIN);
  126. } else {
  127. $config['md'] = file_get_contents($config['md']);
  128. }
  129. $parsers = [
  130. 'CommonMark' => function ($markdown) {
  131. $parser = new CommonMarkConverter();
  132. $parser->convert($markdown);
  133. },
  134. 'CommonMark GFM' => function ($markdown) {
  135. $parser = new GithubFlavoredMarkdownConverter();
  136. $parser->convert($markdown);
  137. },
  138. 'CommonMark All Extensions' => function ($markdown) {
  139. $environment = new Environment([
  140. 'default_attributes' => [
  141. Table::class => [
  142. 'class' => 'table',
  143. ],
  144. ],
  145. 'external_link' => [
  146. 'internal_hosts' => 'www.example.com',
  147. 'open_in_new_window' => true,
  148. 'html_class' => 'external-link',
  149. 'nofollow' => '',
  150. 'noopener' => 'external',
  151. 'noreferrer' => 'external',
  152. ],
  153. 'mentions' => [
  154. 'github_handle' => [
  155. 'prefix' => '@',
  156. 'pattern' => '[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)',
  157. 'generator' => 'https://github.com/%s',
  158. ],
  159. ],
  160. ]);
  161. $environment->addExtension(new CommonMarkCoreExtension());
  162. $environment->addExtension(new AttributesExtension());
  163. $environment->addExtension(new AutolinkExtension());
  164. $environment->addExtension(new DefaultAttributesExtension());
  165. $environment->addExtension(new DescriptionListExtension());
  166. $environment->addExtension(new DisallowedRawHtmlExtension());
  167. $environment->addExtension(new ExternalLinkExtension());
  168. $environment->addExtension(new FootnoteExtension());
  169. $environment->addExtension(new FrontMatterExtension());
  170. $environment->addExtension(new HeadingPermalinkExtension());
  171. $environment->addExtension(new MentionExtension());
  172. $environment->addExtension(new SmartPunctExtension());
  173. $environment->addExtension(new StrikethroughExtension());
  174. $environment->addExtension(new TableExtension());
  175. $environment->addExtension(new TableOfContentsExtension());
  176. $environment->addExtension(new TaskListExtension());
  177. $parser = new MarkdownConverter($environment);
  178. $parser->convert($markdown);
  179. },
  180. 'PHP Markdown' => function ($markdown) {
  181. Markdown::defaultTransform($markdown);
  182. },
  183. 'PHP Markdown Extra' => function ($markdown) {
  184. MarkdownExtra::defaultTransform($markdown);
  185. },
  186. 'Parsedown' => function ($markdown) {
  187. $parser = new Parsedown();
  188. $parser->text($markdown);
  189. },
  190. 'cebe/markdown' => function ($markdown) {
  191. $parser = new \cebe\markdown\Markdown();
  192. $parser->parse($markdown);
  193. },
  194. 'cebe/markdown gfm' => function ($markdown) {
  195. $parser = new \cebe\markdown\GithubMarkdown();
  196. $parser->parse($markdown);
  197. },
  198. 'cebe/markdown extra' => function ($markdown) {
  199. $parser = new \cebe\markdown\MarkdownExtra();
  200. $parser->parse($markdown);
  201. },
  202. ];
  203. if (extension_loaded('cmark')) {
  204. $parsers['krakjoe/cmark'] = function ($markdown) {
  205. \CommonMark\Render\HTML(
  206. \CommonMark\Parse($markdown)
  207. );
  208. };
  209. }
  210. if (count($config['parser'])) {
  211. $parsers = array_filter($parsers, function ($parser) use ($config) {
  212. return array_search($parser, $config['parser']) > -1;
  213. }, ARRAY_FILTER_USE_KEY);
  214. }
  215. $exec = function (array $config, string $parser) use ($parsers): array {
  216. $parse = $parsers[$parser];
  217. $start = microtime(true);
  218. for ($i = 0; $i < $config['iterations']; $i++) {
  219. echo '.';
  220. $parse($config['md']);
  221. }
  222. $end = microtime(true);
  223. $cpu = ($end - $start) * 1000;
  224. $cpu /= $config['iterations'];
  225. $cpu = round($cpu, 2);
  226. if ($config['flags']['memory']) {
  227. $mem = memory_get_peak_usage();
  228. $mem /= 1024;
  229. } else {
  230. $mem = 0;
  231. }
  232. if ($config['csv']) {
  233. fputcsv(
  234. CSVOUT,
  235. [$parser, $cpu, $mem]
  236. );
  237. fflush(CSVOUT);
  238. }
  239. return [$cpu, $mem];
  240. };
  241. if ($config['exec'] === 'exec') {
  242. vfprintf(
  243. STDERR,
  244. '%.2f %d',
  245. $exec($config, array_shift($config['parser']))
  246. );
  247. exit(0);
  248. }
  249. $run = function (array $config, string $parser) use ($exec): array {
  250. if ($config['flags']['isolate']) {
  251. $bin = str_replace(' ', '\ ', realpath($config['exec']));
  252. $argv =
  253. '--exec exec ' .
  254. "--parser \"{$parser}\" " .
  255. '--md - ' .
  256. "--iterations {$config['iterations']}";
  257. if ($config['csv']) {
  258. $argv .= " --csv {$config['csv']}";
  259. }
  260. if ($config['flags']['memory']) {
  261. $argv .= ' -memory';
  262. }
  263. $proc = proc_open("{$bin} {$argv}", [
  264. 0 => ['pipe', 'r'],
  265. 1 => STDOUT,
  266. 2 => ['pipe', 'w'],
  267. ], $pipes);
  268. if (is_resource($proc)) {
  269. fwrite($pipes[0], $config['md']);
  270. fclose($pipes[0]);
  271. $result =
  272. stream_get_contents($pipes[2]);
  273. fclose($pipes[2]);
  274. if (proc_close($proc) !== 0) {
  275. fprintf(STDERR, 'failed to close process%s', PHP_EOL);
  276. exit(1);
  277. }
  278. } else {
  279. fprintf(STDERR, 'failed to open process%s', PHP_EOL);
  280. exit(1);
  281. }
  282. return explode(" ", $result);
  283. }
  284. return $exec($config, $parser);
  285. };
  286. $display = function (array $config, string $title, array $fmt, array $results, string $formatName, string $formatResult): void {
  287. $space = $config['iterations'] - 7;
  288. $position = 1;
  289. $top = 0;
  290. $diff = 0;
  291. vprintf($title, $fmt);
  292. asort($results);
  293. foreach ($results as $name => $result) {
  294. if (!$top) {
  295. $top = $result;
  296. } else {
  297. $diff = $top - $result;
  298. }
  299. printf(
  300. "\t%d) $formatName $formatResult % {$space}s%s",
  301. $position++,
  302. $name,
  303. $result,
  304. $diff ?
  305. sprintf($formatResult, $diff) :
  306. null,
  307. PHP_EOL
  308. );
  309. }
  310. };
  311. if (extension_loaded('xdebug')) {
  312. fwrite(STDERR, 'The xdebug extension is loaded, this can significantly skew benchmarks. Disable it for accurate results. For xdebug 3, prefix your command with "XDEBUG_MODE=off"' . PHP_EOL . PHP_EOL);
  313. }
  314. printf(
  315. 'Running Benchmarks (%s), %d Implementations, %d Iterations:%s',
  316. $config['flags']['isolate'] ?
  317. 'Isolated' : 'No Isolation',
  318. count($parsers),
  319. $config['iterations'],
  320. PHP_EOL
  321. );
  322. $cpu = [];
  323. $mem = [];
  324. foreach ($parsers as $name => $parser) {
  325. printf("\t%- 30s ", $name);
  326. [$cpu[$name], $mem[$name]] =
  327. $run($config, $name);
  328. echo PHP_EOL;
  329. usleep(300000);
  330. }
  331. $display(
  332. $config,
  333. '%sBenchmark Results, CPU:%s',
  334. [PHP_EOL, PHP_EOL],
  335. $cpu,
  336. '%- 26s',
  337. '% 6.2f ms'
  338. );
  339. if (!$config['flags']['memory']) {
  340. exit(0);
  341. }
  342. $display(
  343. $config,
  344. '%sBenchmark Results, Peak Memory:%s',
  345. [PHP_EOL, PHP_EOL],
  346. $mem,
  347. '%- 26s',
  348. '% 6d kB'
  349. );