Case Study: Optimizing CommonMark Markdown Parser with Blackfire.io
- fully support the entire CommonMark spec
- match the behavior of the JS reference implementation
- be well-written and super-extensible so that others can add their own functionality.
| Library | Avg. Parse Time | File/Class Count |
|---|---|---|
| Parsedown 1.6.0 | 2 ms | 1 |
| PHP Markdown 1.5.0 | 4 ms | 4 |
| PHP Markdown Extra 1.5.0 | 7 ms | 6 |
| CommonMark 0.12.0 | 46 ms | 117 |
Profiling with Blackfire

Optimization 1

public function parse(ContextInterface $context, Cursor $cursor)
{
// Iterate through every single character in the current line
while (($character = $cursor->getCharacter()) !== null) {
// Check to see whether this character is a special Markdown character
// If so, let it try to parse this part of the string
foreach ($matchingParsers as $parser) {
if ($res = $parser->parse($context, $inlineParserContext)) {
continue 2;
}
}
// If no parser could handle this character, then it must be a plain text character
// Add this character to the current line of text
$lastInline->append($character);
}
}
public function parse(ContextInterface $context, Cursor $cursor)
{
// Iterate through every single character in the current line
while (($character = $cursor->getCharacter()) !== null) {
// Check to see whether this character is a special Markdown character
// If so, let it try to parse this part of the string
foreach ($matchingParsers as $parser) {
if ($res = $parser->parse($context, $inlineParserContext)) {
continue 2;
}
}
// If no parser could handle this character, then it must be a plain text character
// NEW: Attempt to match multiple non-special characters at once.
// We use a dynamically-created regex which matches text from
// the current position until it hits a special character.
$text = $cursor->match($this->environment->getInlineParserCharacterRegex());
// Add the matching text to the current line of text
$lastInline->append($character);
}
}


Optimization 2
class NewlineParser extends AbstractInlineParser {
public function getCharacters() {
return array("\n", " ");
}
public function parse(ContextInterface $context, InlineParserContext $inlineContext) {
if ($m = $inlineContext->getCursor()->match('/^ *\n/')) {
if (strlen($m) > 2) {
$inlineContext->getInlines()->add(new Newline(Newline::HARDBREAK));
return true;
} elseif (strlen($m) > 0) {
$inlineContext->getInlines()->add(new Newline(Newline::SOFTBREAK));
return true;
}
}
return false;
}
}

class NewlineParser extends AbstractInlineParser {
public function getCharacters() {
return array("\n");
}
public function parse(ContextInterface $context, InlineParserContext $inlineContext) {
$inlineContext->getCursor()->advance();
// Check previous text for trailing spaces
$spaces = 0;
$lastInline = $inlineContext->getInlines()->last();
if ($lastInline && $lastInline instanceof Text) {
// Count the number of spaces by using some `trim` logic
$trimmed = rtrim($lastInline->getContent(), ' ');
$spaces = strlen($lastInline->getContent()) - strlen($trimmed);
}
if ($spaces >= 2 ) {
$inlineContext->getInlines()->add(new Newline(Newline::HARDBREAK));
} else {
$inlineContext->getInlines()->add(new Newline(Newline::SOFTBREAK));
}
return true;
}
}

NewlineParser::parse()is now only called 1,704 times instead of 12,982 times (an 87% decrease)- General inline parsing time decreased by 61%
- Overall parsing speed improved by 23%
Comments
Post a Comment