Changeset 277818 in webkit


Ignore:
Timestamp:
May 20, 2021, 12:40:04 PM (5 years ago)
Author:
Antti Koivisto
Message:

HTML parser should yield more aggressively
https://bugs.webkit.org/show_bug.cgi?id=224609
rdar://73458064

Reviewed by Darin Adler.

During page loading we may spend substantial amount of time in individual invocations of
the HTML parser. This can be a problem since it blocks rendering updates and so visual
page load progression.

Most of this time is not actually spend parsing, it rather goes into synchronous script
execution and DOM work triggered by scripts. This patch adds a more effective mechanism
where we may decide to yield the parser before executing a script.

  • dom/ScriptElement.h:
  • html/parser/HTMLDocumentParser.cpp:

(WebCore::HTMLDocumentParser::pumpTokenizerLoop):

  • html/parser/HTMLParserScheduler.cpp:

(WebCore::parserTimeLimit):
(WebCore::PumpSession::PumpSession):
(WebCore::HTMLParserScheduler::HTMLParserScheduler):
(WebCore::HTMLParserScheduler::shouldYieldBeforeExecutingScript):

Consider yielding before script execution after 16ms has elapsed and at least 256 tokens have been parsed.
Only yield for synchronous scripts.
Don't yield on very short inline scripts (this is an imperfect way to try to guess the execution cost).

  • html/parser/HTMLParserScheduler.h:

(WebCore::HTMLParserScheduler::shouldYieldBeforeToken):
(WebCore::HTMLParserScheduler::checkForYield):

Don't reset the token count, instead track the last yield check point.

  • html/parser/HTMLTreeBuilder.h:

(WebCore::HTMLTreeBuilder::scriptToProcess const):

Location:
trunk/Source/WebCore
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/Source/WebCore/ChangeLog

    r277816 r277818  
     12021-05-20  Antti Koivisto  <antti@apple.com>
     2
     3        HTML parser should yield more aggressively
     4        https://bugs.webkit.org/show_bug.cgi?id=224609
     5        rdar://73458064
     6
     7        Reviewed by Darin Adler.
     8
     9        During page loading we may spend substantial amount of time in individual invocations of
     10        the HTML parser. This can be a problem since it blocks rendering updates and so visual
     11        page load progression.
     12
     13        Most of this time is not actually spend parsing, it rather goes into synchronous script
     14        execution and DOM work triggered by scripts. This patch adds a more effective mechanism
     15        where we may decide to yield the parser before executing a script.
     16
     17        * dom/ScriptElement.h:
     18        * html/parser/HTMLDocumentParser.cpp:
     19        (WebCore::HTMLDocumentParser::pumpTokenizerLoop):
     20        * html/parser/HTMLParserScheduler.cpp:
     21        (WebCore::parserTimeLimit):
     22        (WebCore::PumpSession::PumpSession):
     23        (WebCore::HTMLParserScheduler::HTMLParserScheduler):
     24        (WebCore::HTMLParserScheduler::shouldYieldBeforeExecutingScript):
     25
     26        Consider yielding before script execution after 16ms has elapsed and at least 256 tokens have been parsed.
     27        Only yield for synchronous scripts.
     28        Don't yield on very short inline scripts (this is an imperfect way to try to guess the execution cost).
     29
     30        * html/parser/HTMLParserScheduler.h:
     31        (WebCore::HTMLParserScheduler::shouldYieldBeforeToken):
     32        (WebCore::HTMLParserScheduler::checkForYield):
     33
     34        Don't reset the token count, instead track the last yield check point.
     35
     36        * html/parser/HTMLTreeBuilder.h:
     37        (WebCore::HTMLTreeBuilder::scriptToProcess const):
     38
    1392021-05-20  Chris Dumez  <cdumez@apple.com>
    240
  • trunk/Source/WebCore/dom/ScriptElement.h

    r256808 r277818  
    5656
    5757    virtual bool hasAsyncAttribute() const = 0;
     58    virtual bool hasDeferAttribute() const = 0;
     59    virtual bool hasSourceAttribute() const = 0;
     60    virtual bool hasNoModuleAttribute() const = 0;
    5861
    5962    // XML parser calls these
     
    115118    virtual String forAttributeValue() const = 0;
    116119    virtual String eventAttributeValue() const = 0;
    117     virtual bool hasDeferAttribute() const = 0;
    118     virtual bool hasSourceAttribute() const = 0;
    119     virtual bool hasNoModuleAttribute() const = 0;
    120120    virtual ReferrerPolicy referrerPolicy() const = 0;
    121121
  • trunk/Source/WebCore/html/parser/HTMLDocumentParser.cpp

    r272622 r277818  
    259259    do {
    260260        if (UNLIKELY(isWaitingForScripts())) {
    261             if (mode == AllowYield && m_parserScheduler->shouldYieldBeforeExecutingScript(session))
     261            if (mode == AllowYield && m_parserScheduler->shouldYieldBeforeExecutingScript(m_treeBuilder->scriptToProcess(), session))
    262262                return true;
     263           
    263264            runScriptsForPausedTreeBuilder();
    264265            // If we're paused waiting for a script, we try to execute scripts before continuing.
  • trunk/Source/WebCore/html/parser/HTMLParserScheduler.cpp

    r254087 r277818  
    3232#include "HTMLDocumentParser.h"
    3333#include "Page.h"
    34 
    35 // defaultParserTimeLimit is the seconds the parser will run in one write() call
    36 // before yielding. Inline <script> execution can cause it to exceed the limit.
    37 // FIXME: We would like this value to be 0.2.
    38 static const double defaultParserTimeLimit = 0.500;
     34#include "ScriptElement.h"
    3935
    4036namespace WebCore {
    4137
    42 static double parserTimeLimit(Page* page)
     38static Seconds parserTimeLimit(Page* page)
    4339{
     40    // Always yield after exceeding this.
     41    constexpr auto defaultParserTimeLimit = 500_ms;
     42
    4443    // We're using the poorly named customHTMLTokenizerTimeDelay setting.
    4544    if (page && page->hasCustomHTMLTokenizerTimeDelay())
    46         return page->customHTMLTokenizerTimeDelay();
     45        return Seconds(page->customHTMLTokenizerTimeDelay());
    4746    return defaultParserTimeLimit;
    4847}
     
    6665    : NestingLevelIncrementer(nestingLevel)
    6766    , ActiveParserSession(document)
    68     // Setting processedTokens to INT_MAX causes us to check for yields
    69     // after any token during any parse where yielding is allowed.
    70     // At that time we'll initialize startTime.
    71     , processedTokens(INT_MAX)
    72     , didSeeScript(false)
    7367{
    7468}
     
    7872HTMLParserScheduler::HTMLParserScheduler(HTMLDocumentParser& parser)
    7973    : m_parser(parser)
    80     , m_parserTimeLimit(Seconds(parserTimeLimit(m_parser.document()->page())))
     74    , m_parserTimeLimit(parserTimeLimit(m_parser.document()->page()))
    8175    , m_continueNextChunkTimer(*this, &HTMLParserScheduler::continueNextChunkTimerFired)
    8276    , m_isSuspendedWithActiveTimer(false)
     
    10599}
    106100
    107 bool HTMLParserScheduler::shouldYieldBeforeExecutingScript(PumpSession& session)
     101bool HTMLParserScheduler::shouldYieldBeforeExecutingScript(const ScriptElement* scriptElement, PumpSession& session)
    108102{
    109103    // If we've never painted before and a layout is pending, yield prior to running
    110104    // scripts to give the page a chance to paint earlier.
    111105    RefPtr<Document> document = m_parser.document();
    112     bool needsFirstPaint = document->view() && !document->view()->hasEverPainted();
     106
    113107    session.didSeeScript = true;
     108
     109    if (!document->body())
     110        return false;
     111
     112    if (!document->haveStylesheetsLoaded())
     113        return false;
    114114
    115115    if (UNLIKELY(m_documentHasActiveParserYieldTokens))
    116116        return true;
    117117
    118     return needsFirstPaint && document->isLayoutTimerActive();
     118    auto elapsedTime = MonotonicTime::now() - session.startTime;
     119
     120    constexpr auto elapsedTimeLimit = 16_ms;
     121    // Require at least some new parsed content before yielding.
     122    constexpr auto tokenLimit = 256;
     123    // Don't yield on very short inline scripts. This is an imperfect way to try to guess the execution cost.
     124    constexpr auto inlineScriptLengthLimit = 1024;
     125
     126    if (elapsedTime < elapsedTimeLimit)
     127        return false;
     128    if (session.processedTokens < tokenLimit)
     129        return false;
     130
     131    if (scriptElement) {
     132        // Async and deferred scripts are not executed by the parser.
     133        if (scriptElement->hasAsyncAttribute() || scriptElement->hasDeferAttribute())
     134            return false;
     135        if (!scriptElement->hasSourceAttribute() && scriptElement->scriptContent().length() < inlineScriptLengthLimit)
     136            return false;
     137    }
     138
     139    return true;
    119140}
    120141
  • trunk/Source/WebCore/html/parser/HTMLParserScheduler.h

    r254087 r277818  
    3838class Document;
    3939class HTMLDocumentParser;
     40class ScriptElement;
    4041
    4142class ActiveParserSession {
     
    5354    ~PumpSession();
    5455
    55     unsigned processedTokens;
    56     MonotonicTime startTime;
    57     bool didSeeScript;
     56    unsigned processedTokens { 0 };
     57    unsigned processedTokensOnLastCheck { 0 };
     58    MonotonicTime startTime { MonotonicTime::now() };
     59    bool didSeeScript { false };
    5860};
    5961
     
    7375            return true;
    7476
    75         if (UNLIKELY(session.processedTokens > numberOfTokensBeforeCheckingForYield || session.didSeeScript))
     77        if (UNLIKELY(session.processedTokens > session.processedTokensOnLastCheck + numberOfTokensBeforeCheckingForYield || session.didSeeScript))
    7678            return checkForYield(session);
    7779
     
    7981        return false;
    8082    }
    81     bool shouldYieldBeforeExecutingScript(PumpSession&);
     83    bool shouldYieldBeforeExecutingScript(const ScriptElement*, PumpSession&);
    8284
    8385    void scheduleForResume();
     
    109111    bool checkForYield(PumpSession& session)
    110112    {
    111         session.processedTokens = 1;
     113        session.processedTokensOnLastCheck = session.processedTokens;
    112114        session.didSeeScript = false;
    113 
    114         // MonotonicTime::now() can be expensive. By delaying, we avoided calling
    115         // MonotonicTime::now() when constructing non-yielding PumpSessions.
    116         if (!session.startTime) {
    117             session.startTime = MonotonicTime::now();
    118             return false;
    119         }
    120115
    121116        Seconds elapsedTime = MonotonicTime::now() - session.startTime;
  • trunk/Source/WebCore/html/parser/HTMLTreeBuilder.h

    r254087 r277818  
    6767    // Must be called to take the parser-blocking script before calling the parser again.
    6868    RefPtr<ScriptElement> takeScriptToProcess(TextPosition& scriptStartPosition);
     69    const ScriptElement* scriptToProcess() const { return m_scriptToProcess.get(); }
    6970
    7071    std::unique_ptr<CustomElementConstructionData> takeCustomElementConstructionData() { return WTFMove(m_customElementToConstruct); }
Note: See TracChangeset for help on using the changeset viewer.