Преглед изворни кода

[MERGE #5058 @thomasmo] Initial commit for BGParseManager

Merge pull request #5058 from thomasmo:bgparse_intro

Initial commit for BGParseManager.
BGParseManager is responsible for handling and managing requests to parse scripts on a background thread. This work is tracked by the BGParseWorkItem with a cookie, and is managed by BackgroundJobProcessor. The results of the background work is marshaled across threads through bytecode serialization.More details in the header file for BGParseManager.

This work can be controlled by a new config flag, BgParse; additional tracing at runtime is also managed by the same flag;
Thomas Moore (CHAKRA) пре 7 година
родитељ
комит
ccafb8ebaf

+ 1 - 1
bin/ChakraCore/ChakraCoreDllFunc.cpp

@@ -84,7 +84,7 @@ static BOOL AttachProcess(HANDLE hmod)
 
 static void DetachProcess()
 {
-    ThreadBoundThreadContextManager::DestroyAllContextsAndEntries();
+    ThreadBoundThreadContextManager::DestroyAllContextsAndEntries(false /*shouldDeleteCurrentTlsEntry*/);
 
     // In JScript, we never unload except for when the app shuts down
     // because DllCanUnloadNow always returns S_FALSE. As a result

+ 56 - 23
lib/Common/Common/Jobs.cpp

@@ -484,6 +484,24 @@ namespace JsUtil
     // BackgroundJobProcessor
     // -------------------------------------------------------------------------------------------------------------------------
 
+    ParallelThreadData::ParallelThreadData(AllocationPolicyManager* policyManager) :
+        threadHandle(0),
+        isWaitingForJobs(false),
+        canDecommit(true),
+        currentJob(nullptr),
+        threadStartedOrClosing(false),
+        backgroundPageAllocator(policyManager, Js::Configuration::Global.flags, PageAllocatorType_BGJIT,
+        (AutoSystemInfo::Data.IsLowMemoryProcess() ?
+            PageAllocator::DefaultLowMaxFreePageCount :
+            PageAllocator::DefaultMaxFreePageCount)),
+        threadArena(nullptr),
+        processor(nullptr),
+        parser(nullptr),
+        pse(nullptr),
+        scriptContextBG(nullptr)
+    {
+    }
+
     void BackgroundJobProcessor::InitializeThreadCount()
     {
         if (CONFIG_FLAG(ForceMaxJitThreadCount))
@@ -868,6 +886,12 @@ namespace JsUtil
             AutoCriticalSection lock(&criticalSection);
             // Managers must remove themselves. Hence, Close does not remove managers. So, not asserting on !IsClosed().
 
+            if (!HasManager(manager))
+            {
+                // Since this manager isn't owned by this processor, no need to remove and cleanup
+                return;
+            }
+
             managers.Unlink(manager);
             if(manager->numJobsAddedToProcessor == 0)
             {
@@ -1096,37 +1120,46 @@ namespace JsUtil
                     threadData->isWaitingForJobs = false;
                     continue;
                 }
+                else
+                {
+                    // Job found. Proceed with Processing
+                    Assert(numJobs != 0);
+                    --numJobs;
+                    threadData->currentJob = job;
 
-                Assert(numJobs != 0);
-                --numJobs;
-                threadData->currentJob = job;
-                criticalSection.Leave();
+                    JobManager *const manager = job->Manager();
+                    manager->JobProcessing(job);
 
-                const bool succeeded = Process(job, threadData);
+                    criticalSection.Leave();
 
-                criticalSection.Enter();
-                threadData->currentJob = 0;
-                JobManager *const manager = job->Manager();
-                JobProcessed(manager, job, succeeded); // the job may be deleted during this and should not be used afterwards
-                Assert(manager->numJobsAddedToProcessor != 0);
-                --manager->numJobsAddedToProcessor;
-                if(manager->isWaitable)
-                {
-                    WaitableJobManager *const waitableManager = static_cast<WaitableJobManager *>(manager);
-                    Assert(!(waitableManager->jobBeingWaitedUpon && waitableManager->isWaitingForQueuedJobs));
-                    if(waitableManager->jobBeingWaitedUpon == job)
+                    const bool succeeded = Process(job, threadData);
+
+                    criticalSection.Enter();
+                    threadData->currentJob = 0;
+                    
+                    JobProcessed(manager, job, succeeded); // the job may be deleted during this and should not be used afterwards
+                    Assert(manager->numJobsAddedToProcessor != 0);
+                    --manager->numJobsAddedToProcessor;
+                    if (manager->isWaitable)
                     {
-                        waitableManager->jobBeingWaitedUpon = 0;
-                        waitableManager->jobBeingWaitedUponProcessed.Set();
+                        WaitableJobManager *const waitableManager = static_cast<WaitableJobManager *>(manager);
+                        Assert(!(waitableManager->jobBeingWaitedUpon && waitableManager->isWaitingForQueuedJobs));
+                        if (waitableManager->jobBeingWaitedUpon == job)
+                        {
+                            waitableManager->jobBeingWaitedUpon = 0;
+                            waitableManager->jobBeingWaitedUponProcessed.Set();
+                        }
+                        else if (waitableManager->isWaitingForQueuedJobs && manager->numJobsAddedToProcessor == 0)
+                        {
+                            waitableManager->isWaitingForQueuedJobs = false;
+                            waitableManager->queuedJobsProcessed.Set();
+                        }
                     }
-                    else if(waitableManager->isWaitingForQueuedJobs && manager->numJobsAddedToProcessor == 0)
+                    if (manager->numJobsAddedToProcessor == 0)
                     {
-                        waitableManager->isWaitingForQueuedJobs = false;
-                        waitableManager->queuedJobsProcessed.Set();
+                        LastJobProcessed(manager); // the manager may be deleted during this and should not be used afterwards
                     }
                 }
-                if(manager->numJobsAddedToProcessor == 0)
-                    LastJobProcessed(manager); // the manager may be deleted during this and should not be used afterwards
             }
             criticalSection.Leave();
 

+ 27 - 17
lib/Common/Common/Jobs.h

@@ -7,9 +7,14 @@
 // Undefine name #define in OS headers
 #undef AddJob
 #undef GetJob
+#include "Memory/AutoPtr.h"
 
 class Parser;
 class CompileScriptException;
+namespace Js
+{
+    class ScriptContext;
+}
 
 namespace JsUtil
 {
@@ -98,7 +103,11 @@ namespace JsUtil
     public:
         JobProcessor *Processor() const;
 
-    protected:
+    protected:        
+        // Called by the job processor (inside the lock) right before the job starts being processed. Note that the job
+        // has been removed from the queue at this point.
+        virtual void JobProcessing(Job* job) {}
+
         // Called by the job processor (outside the lock) to process a job. A job manager may choose to return false to indicate
         // a failure. Throwing OutOfMemoryException or OperationAbortedException also indicate a processing failure.
         // 'pageAllocator' will be null if the job is being processed in the foreground.
@@ -336,6 +345,9 @@ namespace JsUtil
         // Must be called from inside the lock
         virtual bool RemoveJob(Job *const job);
 
+        // Must be called from inside the lock
+        template<class Fn> void ForEachJob(Fn fn);
+
         template<class TJobManager, class TJobHolder>
         void AddJobAndProcessProactively(TJobManager *const jobManager, const TJobHolder holder);
 
@@ -426,23 +438,9 @@ namespace JsUtil
         BackgroundJobProcessor *processor;
         Parser *parser;
         CompileScriptException *pse;
+        Js::ScriptContext *scriptContextBG;
 
-        ParallelThreadData(AllocationPolicyManager* policyManager) :
-            threadHandle(0),
-            isWaitingForJobs(false),
-            canDecommit(true),
-            currentJob(nullptr),
-            threadStartedOrClosing(false),
-            backgroundPageAllocator(policyManager, Js::Configuration::Global.flags, PageAllocatorType_BGJIT,
-                                    (AutoSystemInfo::Data.IsLowMemoryProcess() ?
-                                     PageAllocator::DefaultLowMaxFreePageCount :
-                                     PageAllocator::DefaultMaxFreePageCount)),
-            threadArena(nullptr),
-            processor(nullptr),
-            parser(nullptr),
-            pse(nullptr)
-            {
-            }
+        ParallelThreadData(AllocationPolicyManager* policyManager);
 
         PageAllocator* const GetPageAllocator() { return &backgroundPageAllocator; }
         bool CanDecommit() const { return canDecommit; }
@@ -560,4 +558,16 @@ namespace JsUtil
         static void CALLBACK ThreadServiceCallback(void * callbackData);
     };
 #endif
+
+    // Functions defined in header so that templates can be used in other classes
+    template<class Fn>
+    void JobProcessor::ForEachJob(Fn fn)
+    {
+        AutoOptionalCriticalSection lock(GetCriticalSection());
+        bool shouldContinue = true;
+        for (Job *curJob = jobs.Head(); curJob != nullptr && shouldContinue; curJob = curJob->Next())
+        {
+            shouldContinue = fn(curJob);
+        }
+    }
 }

+ 5 - 0
lib/Common/Common/Tick.cpp

@@ -857,6 +857,11 @@ namespace Js {
         }
     }
 
+    double Tick::ToMilliseconds() const
+    {
+        return ToMicroseconds() / 1000.f;
+    }
+
     int TickDelta::ToMilliseconds() const
     {
         if (*this == Infinite())

+ 1 - 0
lib/Common/Common/Tick.h

@@ -40,6 +40,7 @@ namespace Js {
     // Properties
     public:
                 uint64          ToMicroseconds() const;
+                double          ToMilliseconds() const;
         static  Tick            FromMicroseconds(uint64 luTick);
         static  Tick            FromQPC(uint64 luQPCTick);
 

+ 2 - 0
lib/Common/ConfigFlagsList.h

@@ -30,6 +30,7 @@ PHASE(All)
         PHASE(ScanAhead)
         PHASE_DEFAULT_OFF(ParallelParse)
         PHASE(EarlyReferenceErrors)
+        PHASE(BgParse)
     PHASE(ByteCode)
         PHASE(CachedScope)
         PHASE(StackFunc)
@@ -954,6 +955,7 @@ FLAGR (NumberSet, BailOutByteCode     , "Byte code location to insert BailOut. U
 #endif
 FLAGNR(Boolean, Benchmark             , "Disable security code which introduce variability in benchmarks", false)
 FLAGR (Boolean, BgJit                 , "Background JIT. Disable to force heuristic-based foreground JITting. (default: true)", true)
+FLAGR (Boolean, BgParse               , "Background Parse. Disable to force all parsing to occur on UI thread. (default: false)", false)
 FLAGNR(Number,  BgJitDelay            , "Delay to wait for speculative jitting before starting script execution", DEFAULT_CONFIG_BgJitDelay)
 FLAGNR(Number,  BgJitDelayFgBuffer    , "When speculatively jitting in the foreground thread, do so for (BgJitDelay - BgJitDelayBuffer) milliseconds", DEFAULT_CONFIG_BgJitDelayFgBuffer)
 FLAGNR(Number,  BgJitPendingFuncCap   , "Disable delay if pending function count larger then cap", DEFAULT_CONFIG_BgJitPendingFuncCap)

+ 23 - 0
lib/Common/Core/ConfigParser.cpp

@@ -304,6 +304,29 @@ void ConfigParser::ParseRegistryKey(HKEY hk, CmdLineArgsParser &parser)
         }
     }
 
+    // BgParse feature control
+    //     0 - Disable BgParse
+    //     1 - Enable BgParse
+    dwValue = 0;
+    dwSize = sizeof(dwValue);
+    if (NOERROR == RegGetValueW(hk, nullptr, _u("EnableBgParse"), RRF_RT_DWORD, nullptr, (LPBYTE)&dwValue, &dwSize))
+    {
+        Js::ConfigFlagsTable &configFlags = Js::Configuration::Global.flags;
+        configFlags.Enable(Js::BgParseFlag);
+        if (dwValue == 0)
+        {
+            configFlags.SetAsBoolean(Js::BgParseFlag, false);
+        }
+        else if (dwValue == 1)
+        {
+            configFlags.SetAsBoolean(Js::BgParseFlag, true);
+        }
+
+#if ENABLE_DEBUG_CONFIG_OPTIONS
+        Output::Print(_u("BgParse controlled by registry: %u\n"), dwValue);
+#endif
+    }
+
     // Spectre mitigation feature control
     // This setting allows enabling\disabling spectre mitigations
     //     0 - Disable Spectre mitigations

+ 469 - 0
lib/Parser/BGParseManager.cpp

@@ -0,0 +1,469 @@
+//-------------------------------------------------------------------------------------------------------
+// Copyright (C) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
+//-------------------------------------------------------------------------------------------------------
+
+#include "ParserPch.h"
+
+#include "Memory/AutoPtr.h"
+#include "Common/Event.h"
+#include "Base/ThreadContextTlsEntry.h"
+#include "Base/ThreadBoundThreadContextManager.h"
+#include "Base/Utf8SourceInfo.h"
+#include "BGParseManager.h"
+#include "Base/ScriptContext.h"
+#include "ByteCodeSerializer.h"
+
+// Global, process singleton
+BGParseManager* BGParseManager::s_BGParseManager = nullptr;
+DWORD           BGParseManager::s_lastCookie = 0;
+CriticalSection BGParseManager::s_staticMemberLock;
+
+// Static member management
+BGParseManager* BGParseManager::GetBGParseManager()
+{
+    AutoCriticalSection lock(&s_staticMemberLock);
+    if (s_BGParseManager == nullptr)
+    {        
+        AUTO_NESTED_HANDLED_EXCEPTION_TYPE(ExceptionType_DisableCheck);
+        s_BGParseManager = HeapNew(BGParseManager);
+        s_BGParseManager->Processor()->AddManager(s_BGParseManager);
+    }
+    return s_BGParseManager;
+}
+
+void BGParseManager::DeleteBGParseManager()
+{
+    AutoCriticalSection lock(&s_staticMemberLock);
+    if (s_BGParseManager != nullptr)
+    {
+        BGParseManager* manager = s_BGParseManager;
+        s_BGParseManager = nullptr;
+        HeapDelete(manager);
+    }
+}
+
+DWORD BGParseManager::GetNextCookie()
+{
+    AutoCriticalSection lock(&s_staticMemberLock);
+    return ++s_lastCookie;
+}
+
+
+// Note: runs on any thread
+BGParseManager::BGParseManager()
+    : JsUtil::WaitableJobManager(ThreadBoundThreadContextManager::GetSharedJobProcessor())
+{
+}
+
+// Note: runs on any thread
+BGParseManager::~BGParseManager()
+{
+    // First, remove the manager from the JobProcessor so that any remaining jobs
+    // are moved back into this manager's processed workitem list
+    Processor()->RemoveManager(this);
+
+    Assert(this->workitemsProcessing.IsEmpty());
+
+    // Now, free all remaining processed jobs
+    BGParseWorkItem *item = (BGParseWorkItem*)this->workitemsProcessed.Head();
+    while (item != nullptr)
+    {
+        // Get the next item first so that the current item
+        // can be safely removed and freed
+        BGParseWorkItem* temp = item;
+        item = (BGParseWorkItem*)item->Next();
+
+        this->workitemsProcessed.Unlink(temp);
+        HeapDelete(temp);
+    }
+}
+
+// Returns the BGParseWorkItem that matches the provided cookie
+// Note: runs on any thread
+BGParseWorkItem* BGParseManager::FindJob(DWORD dwCookie, bool waitForResults)
+{
+    AutoOptionalCriticalSection autoLock(Processor()->GetCriticalSection());
+
+    Assert(dwCookie != 0);
+    BGParseWorkItem* matchedWorkitem = nullptr;
+
+    // First, look among processed jobs
+    for (BGParseWorkItem *item = this->workitemsProcessed.Head(); item != nullptr && matchedWorkitem == nullptr; item = (BGParseWorkItem*)item->Next())
+    {
+        if (item->GetCookie() == dwCookie)
+        {
+            matchedWorkitem = item;
+        }
+    }
+
+    if (matchedWorkitem == nullptr)
+    {
+        // Then, look among processing jobs
+        for (BGParseWorkItem *item = this->workitemsProcessing.Head(); item != nullptr && matchedWorkitem == nullptr; item = (BGParseWorkItem*)item->Next())
+        {
+            if (item->GetCookie() == dwCookie)
+            {
+                matchedWorkitem = item;
+            }
+        }
+
+        // Lastly, look among queued jobs
+        if (matchedWorkitem == nullptr)
+        {
+            Processor()->ForEachJob([&](JsUtil::Job * job) {
+                if (job->Manager() == this)
+                {
+                    BGParseWorkItem* workitem = (BGParseWorkItem*)job;
+                    if (workitem->GetCookie() == dwCookie)
+                    {
+                        matchedWorkitem = workitem;
+                        return false;
+                    }
+                }
+                return true;
+            });
+        }
+
+        // Since this job isn't already processed and the caller needs the results, create an event
+        // that the caller can wait on for results to complete
+        if (waitForResults && matchedWorkitem != nullptr)
+        {
+            // TODO: Is it possible for one event to be shared to reduce the number of heap allocations?
+            matchedWorkitem->CreateCompletionEvent();
+        }
+    }
+
+    return matchedWorkitem;
+}
+
+// Creates a new job to parse the provided script on a background thread
+// Note: runs on any thread
+HRESULT BGParseManager::QueueBackgroundParse(LPCUTF8 pszSrc, size_t cbLength, char16 *fullPath, DWORD* dwBgParseCookie)
+{
+    HRESULT hr = S_OK;
+    if (cbLength > 0)
+    {
+        BGParseWorkItem* workitem;
+        {
+            AUTO_NESTED_HANDLED_EXCEPTION_TYPE(ExceptionType_DisableCheck);
+            workitem = HeapNew(BGParseWorkItem, this, (const byte *)pszSrc, cbLength, fullPath);
+        }
+
+        // Add the job to the processor
+        {
+            AutoOptionalCriticalSection autoLock(Processor()->GetCriticalSection());
+            Processor()->AddJob(workitem, false /*prioritize*/);
+        }
+
+        (*dwBgParseCookie) = workitem->GetCookie();
+
+        if (PHASE_TRACE1(Js::BgParsePhase))
+        {
+            Js::Tick now = Js::Tick::Now();
+            Output::Print(
+                _u("[BgParse: Start -- cookie: %04d on thread 0x%X at %.2f ms -- %s]\n"),
+                workitem->GetCookie(),
+                ::GetCurrentThreadId(),
+                now.ToMilliseconds(),
+                fullPath
+            );
+        }
+    }
+    else
+    {
+        hr = E_INVALIDARG;
+    }
+
+    return hr;
+}
+
+// Returns the data provided when the parse was queued
+// Note: runs on any thread, but the buffer lifetimes are not guaranteed after parse results are returned
+HRESULT BGParseManager::GetInputFromCookie(DWORD cookie, LPCUTF8* ppszSrc, size_t* pcbLength)
+{
+    HRESULT hr = E_FAIL;
+
+    // Find the job associated with this cookie
+    BGParseWorkItem* workitem = FindJob(cookie, false);
+    if (workitem != nullptr)
+    {
+        (*ppszSrc) = workitem->GetScriptSrc();
+        (*pcbLength) = workitem->GetScriptLength();
+        hr = S_OK;
+    }
+
+    return hr;
+}
+
+// Deserializes the background parse results into this thread
+// Note: *must* run on a UI/Execution thread with an available ScriptContext
+HRESULT BGParseManager::GetParseResults(Js::ScriptContext* scriptContextUI, DWORD cookie, LPCUTF8 pszSrc, SRCINFO const * pSrcInfo, Js::ParseableFunctionInfo** ppFunc, CompileScriptException* pse, size_t& srcLength)
+{
+    // TODO: Is there a way to cache the environment from which serialization begins to
+    // determine whether or not deserialization will succeed? Specifically, being able
+    // to assert/compare the flags used during background parse with the flags expected
+    // from the UI thread?
+
+    HRESULT hr = E_FAIL;
+
+    // Find the job associated with this cookie
+    BGParseWorkItem* workitem = FindJob(cookie, true);
+    if (workitem != nullptr)
+    {
+        // Synchronously wait for the job to complete
+        workitem->WaitForCompletion();
+
+        Field(Js::FunctionBody*) functionBody = nullptr;
+        hr = workitem->GetParseHR();
+        if (hr == S_OK)
+        {
+            srcLength = workitem->GetParseSourceLength();
+            hr = Js::ByteCodeSerializer::DeserializeFromBuffer(
+                scriptContextUI,
+                0, // flags
+                (const byte *)pszSrc,
+                pSrcInfo,
+                workitem->GetReturnBuffer(),
+                nullptr, // nativeModule
+                &functionBody
+            );
+        }
+
+        *ppFunc = functionBody;
+        workitem->TransferCSE(pse);
+    }
+
+    if (PHASE_TRACE1(Js::BgParsePhase))
+    {
+        Js::Tick now = Js::Tick::Now();
+        Output::Print(
+            _u("[BgParse: End   -- cookie: %04d on thread 0x%X at %.2f ms -- hr: 0x%X]\n"),
+            workitem->GetCookie(),
+            ::GetCurrentThreadId(),
+            now.ToMilliseconds(),
+            hr
+        );
+    }
+
+    return hr;
+}
+
+// Overloaded function called by JobProcessor to do work
+// Note: runs on background thread
+bool BGParseManager::Process(JsUtil::Job *const job, JsUtil::ParallelThreadData *threadData) 
+{
+#if ENABLE_BACKGROUND_JOB_PROCESSOR
+    Assert(job->Manager() == this);
+
+    // Create script context on this thread
+    ThreadContext* threadContext = ThreadBoundThreadContextManager::EnsureContextForCurrentThread();
+
+    // If there is no script context created for this thread yet, create it now
+    if (threadData->scriptContextBG == nullptr)
+    {
+        threadData->scriptContextBG = Js::ScriptContext::New(threadContext);
+        threadData->scriptContextBG->Initialize();
+        threadData->canDecommit = true;
+    }
+    
+    // Parse the workitem's data
+    BGParseWorkItem* workItem = (BGParseWorkItem*)job;
+    workItem->ParseUTF8Core(threadData->scriptContextBG);
+
+    return true;
+#else
+    Assert(!"BGParseManager does not work without ThreadContext");
+    return false;
+#endif
+}
+
+// Callback before the provided job will be processed
+// Note: runs on any thread
+void BGParseManager::JobProcessing(JsUtil::Job* job)
+{
+    Assert(job->Manager() == this);
+    Assert(Processor()->GetCriticalSection()->IsLocked());
+
+    this->workitemsProcessing.LinkToEnd((BGParseWorkItem*)job);
+}
+
+// Callback after the provided job was processed. succeeded is true if the job
+// was executed as well.
+// Note: runs on any thread
+void BGParseManager::JobProcessed(JsUtil::Job *const job, const bool succeeded)
+{
+    Assert(job->Manager() == this);
+    Assert(Processor()->GetCriticalSection()->IsLocked());
+
+    BGParseWorkItem* workItem = (BGParseWorkItem*)job;
+    if (succeeded)
+    {
+        Assert(this->workitemsProcessing.Contains(workItem));
+        this->workitemsProcessing.Unlink(workItem);
+    }
+
+    this->workitemsProcessed.LinkToEnd(workItem);
+    workItem->JobProcessed();
+}
+
+// Define needed for jobs.inl
+// Note: Runs on any thread
+BGParseWorkItem* BGParseManager::GetJob(BGParseWorkItem* workitem)
+{
+    Assert(!"BGParseManager::WasAddedToJobProcessor");
+    return nullptr;
+}
+
+// Define needed for jobs.inl
+// Note: Runs on any thread
+bool BGParseManager::WasAddedToJobProcessor(JsUtil::Job *const job) const
+{
+    Assert(!"BGParseManager::WasAddedToJobProcessor");
+    return true;
+}
+
+
+// Note: runs on any thread
+BGParseWorkItem::BGParseWorkItem(
+    BGParseManager* manager,
+    const byte* pszScript,
+    size_t cbScript,
+    char16 *fullPath
+    )
+    : JsUtil::Job(manager),
+    script(pszScript),
+    cb(cbScript),
+    path(nullptr),
+    parseHR(S_OK),
+    parseSourceLength(0),
+    bufferReturn(nullptr),
+    bufferReturnBytes(0),
+    complete(nullptr)
+{
+    this->cookie = BGParseManager::GetNextCookie();
+
+    Assert(fullPath != nullptr);
+    this->path = SysAllocString(fullPath);
+}
+
+BGParseWorkItem::~BGParseWorkItem()
+{
+    SysFreeString(this->path);
+    if (this->complete != nullptr)
+    {
+        HeapDelete(this->complete);
+    }
+}
+
+void BGParseWorkItem::TransferCSE(CompileScriptException* pse)
+{
+    this->cse.CopyInto(pse);
+}
+
+// This function parses the input data cached in BGParseWorkItem and caches the
+// seralized bytecode
+// Note: runs on BackgroundJobProcessor thread
+// Note: All exceptions are caught by BackgroundJobProcessor
+void BGParseWorkItem::ParseUTF8Core(Js::ScriptContext* scriptContext)
+{
+    if (PHASE_TRACE1(Js::BgParsePhase))
+    {
+        Js::Tick now = Js::Tick::Now();
+        Output::Print(
+            _u("[BgParse: Parse -- cookie: %04d on thread 0x%X at %.2f ms]\n"),
+            GetCookie(),
+            ::GetCurrentThreadId(),
+            now.ToMilliseconds()
+        );
+    }
+
+    Js::AutoDynamicCodeReference dynamicFunctionReference(scriptContext);
+    SourceContextInfo* sourceContextInfo = scriptContext->GetSourceContextInfo(this->cookie, nullptr);
+    if (sourceContextInfo == nullptr)
+    {
+        sourceContextInfo = scriptContext->CreateSourceContextInfo(this->cookie, this->path, wcslen(this->path), nullptr);
+    }
+
+    SRCINFO si = {
+        /* sourceContextInfo   */ sourceContextInfo,
+        /* dlnHost             */ 0,
+        /* ulColumnHost        */ 0,
+        /* lnMinHost           */ 0,
+        /* ichMinHost          */ 0,
+        /* ichLimHost          */ (ULONG)0,
+        /* ulCharOffset        */ 0,
+        /* mod                 */ 0,
+        /* grfsi               */ 0
+    };
+
+    // Currently always called from a try-catch
+    ENTER_PINNED_SCOPE(Js::Utf8SourceInfo, sourceInfo);
+    sourceInfo = Js::Utf8SourceInfo::NewWithNoCopy(scriptContext, (LPUTF8)this->script, (int32)this->cb, static_cast<int32>(this->cb), &si, false);
+    LEAVE_PINNED_SCOPE();
+
+    // what's a better name for "fIsOriginalUtf8Source?" what if i want to say "isutf8source" but not "isoriginal"?
+    charcount_t cchLength = 0;
+    uint sourceIndex = 0;
+    Js::ParseableFunctionInfo * func = nullptr;
+    this->parseHR = scriptContext->CompileUTF8Core(sourceInfo, &si, true, this->script, this->cb, fscrGlobalCode, &this->cse, cchLength, this->parseSourceLength, sourceIndex, &func);
+    if (this->parseHR == S_OK)
+    {
+        BEGIN_TEMP_ALLOCATOR(tempAllocator, scriptContext, _u("BGParseWorkItem"));
+        Js::FunctionBody *functionBody = func->GetFunctionBody();
+        this->parseHR = Js::ByteCodeSerializer::SerializeToBuffer(
+            scriptContext,
+            tempAllocator,
+            (DWORD)this->cb,
+            this->script,
+            functionBody,
+            functionBody->GetHostSrcInfo(),
+            true,
+            &this->bufferReturn,
+            &this->bufferReturnBytes,
+            0
+        );
+        END_TEMP_ALLOCATOR(tempAllocator, scriptContext);
+        Assert(this->parseHR == S_OK);
+    }
+    else
+    {
+        Assert(this->cse.ei.bstrSource != nullptr);
+        Assert(func == nullptr);
+    }
+}
+
+void BGParseWorkItem::CreateCompletionEvent()
+{
+    Assert(this->complete == nullptr);
+    this->complete = HeapNew(Event, false);
+}
+
+// Upon notification of job processed, set the event for those waiting for this job to complete
+void BGParseWorkItem::JobProcessed()
+{
+    if (this->complete != nullptr)
+    {
+        this->complete->Set();
+    }
+}
+
+// Wait for this job to finish processing
+void BGParseWorkItem::WaitForCompletion()
+{
+    if (this->complete != nullptr)
+    {
+        if (PHASE_TRACE1(Js::BgParsePhase))
+        {
+            Js::Tick now = Js::Tick::Now();
+            Output::Print(
+                _u("[BgParse: Wait -- cookie: %04d on thread 0x%X at %.2f ms]\n"),
+                GetCookie(),
+                ::GetCurrentThreadId(),
+                now.ToMilliseconds()
+            );
+        }
+
+        this->complete->Wait();
+    }
+}

+ 133 - 0
lib/Parser/BGParseManager.h

@@ -0,0 +1,133 @@
+//-------------------------------------------------------------------------------------------------------
+// Copyright (C) Microsoft. All rights reserved.
+// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information.
+//-------------------------------------------------------------------------------------------------------
+#pragma once
+
+// This files contains the declarations of BGParseManager and BGParseWorkItem and build upon the Job and
+// JobManager classes that do work on background threads. This enables the host to offload parser work
+// from the UI and execution that doesn't have strict thread dependencies. Thus, both classes are
+// multi-threaded; please see the definition of each function for expectations of which thread executes
+// the function.
+//
+// There are up to 3 threads involved per background parse. Here are the relevant BGParseManager functions
+// called from these three threads:
+//
+//      Background/Network          JobProcessor                UI/Executing 
+//      Thread                      Thread                      Thread
+//              |                       |                           |
+//      QueueBackgroundParse            |                           |
+//              |                       |                           |
+//              |                   Process                         |
+//              |                       |                           |
+//              |                       |                       GetParseResults
+//              .                       .                           .
+// Note that the thread queueing the work can also be the UIT thread. Also, note that GetParseResults may
+// block the calling thread until the JobProcessor thread finishes processing the BGParseWorkItem that
+// contains the results.
+
+
+// Forward Declarations
+class BGParseWorkItem;
+namespace Js
+{
+    struct Utf8SourceInfo;
+    typedef void * Var;
+    class JavascriptFunction;
+}
+
+
+// BGParseManager is the primary interface for background parsing. It uses a cookie to publicly track the data
+// involved per parse request.
+class BGParseManager sealed : public JsUtil::WaitableJobManager
+{
+public:
+    BGParseManager();
+    ~BGParseManager();
+
+    static BGParseManager* GetBGParseManager();
+    static void DeleteBGParseManager();
+    static DWORD GetNextCookie();
+
+    HRESULT QueueBackgroundParse(LPCUTF8 pszSrc, size_t cbLength, char16 *fullPath, DWORD* dwBgParseCookie);
+    HRESULT GetInputFromCookie(DWORD cookie, LPCUTF8* ppszSrc, size_t* pcbLength);
+    HRESULT GetParseResults(
+        Js::ScriptContext* scriptContextUI,
+        DWORD cookie,
+        LPCUTF8 pszSrc,
+        SRCINFO const * pSrcInfo,
+        Js::ParseableFunctionInfo** ppFunc,
+        CompileScriptException* pse,
+        size_t& srcLength
+    );
+
+    virtual bool Process(JsUtil::Job *const job, JsUtil::ParallelThreadData *threadData) override;
+    virtual void JobProcessed(JsUtil::Job *const job, const bool succeeded) override;
+    virtual void JobProcessing(JsUtil::Job* job) override;
+
+    // Defines needed for jobs.inl
+    BGParseWorkItem* GetJob(BGParseWorkItem* workitem);
+    bool WasAddedToJobProcessor(JsUtil::Job *const job) const;
+
+private:
+    BGParseWorkItem * FindJob(DWORD dwCookie, bool waitForResults);
+
+    // BGParseWorkItem job can be in one of 3 states, based on which linked list it is in:
+    // - queued - JobProcessor::jobs
+    // - processing - BGParseManager::workitemsProcessing
+    // - processed - BGParseManager::workitemsProcessed
+    JsUtil::DoublyLinkedList<BGParseWorkItem> workitemsProcessing;
+    JsUtil::DoublyLinkedList<BGParseWorkItem> workitemsProcessed;
+
+    static DWORD s_lastCookie;
+    static BGParseManager* s_BGParseManager;
+    static CriticalSection s_staticMemberLock;
+};
+
+// BGParseWorkItem is a helper class to BGParseManager that caches the input data from the calling thread
+// to parse on the background thread and caches serialized bytecode so that bytecode can be deserialized
+// on the appropriate thread.
+class BGParseWorkItem sealed : public JsUtil::Job
+{
+public:
+    BGParseWorkItem(
+        BGParseManager* manager,
+        const byte* script,
+        size_t cb,
+        char16 *fullPath
+    );
+    ~BGParseWorkItem();
+
+    void ParseUTF8Core(Js::ScriptContext* scriptContext);
+    void TransferCSE(CompileScriptException* pse);
+
+    void CreateCompletionEvent();
+    void WaitForCompletion();
+    void JobProcessed();
+
+    DWORD GetCookie() const { return cookie; }
+    const byte* GetScriptSrc() const { return script; }
+    size_t GetScriptLength() const { return cb; }
+    byte * GetReturnBuffer() const{ return bufferReturn; }
+    HRESULT GetParseHR() const { return parseHR; }
+    size_t GetParseSourceLength() const { return parseSourceLength; }
+
+private:
+    // This cookie is the public identifier for this parser work
+    DWORD cookie;
+
+    // Input data
+    const byte* script;
+    size_t cb;
+    BSTR path;
+
+    // Parse state
+    CompileScriptException cse;
+    HRESULT parseHR;
+    size_t parseSourceLength;
+    Event* complete; 
+
+    // Output data
+    byte * bufferReturn;
+    DWORD  bufferReturnBytes;
+};

+ 1 - 0
lib/Parser/CMakeLists.txt

@@ -1,6 +1,7 @@
 add_library (Chakra.Parser OBJECT
     Alloc.cpp
     BackgroundParser.cpp
+    BGParseManager.cpp
     CaseInsensitive.cpp
     CharClassifier.cpp
     CharSet.cpp

+ 2 - 0
lib/Parser/Chakra.Parser.vcxproj

@@ -65,11 +65,13 @@
     </ClCompile>
     <ClCompile Include="$(MSBuildThisFileDirectory)TextbookBoyerMoore.cpp" />
     <ClCompile Include="$(MSBuildThisFileDirectory)ptree.cpp" />
+    <ClCompile Include="$(MSBuildThisFileDirectory)BGParseManager.cpp" />
     <None Include="HashFunc.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="Alloc.h" />
     <ClInclude Include="BackgroundParser.h" />
+    <ClInclude Include="BGParseManager.h" />
     <ClInclude Include="CaseInsensitive.h" />
     <ClInclude Include="CharClassifier.h" />
     <ClInclude Include="CharMap.h" />

+ 1 - 0
lib/Parser/Parser.h

@@ -53,3 +53,4 @@ namespace UnifiedRegex {
 #include "Parse.h"
 
 #include "BackgroundParser.h"
+#include "BGParseManager.h"

+ 14 - 0
lib/Parser/screrror.cpp

@@ -237,6 +237,20 @@ void CompileScriptException::Free()
     }
 }
 
+void CompileScriptException::CopyInto(CompileScriptException* pse)
+{
+    ScriptException::CopyInto(pse);
+
+    pse->line = this->line;
+    pse->ichMinLine = this->ichMinLine;
+    pse->hasLineNumberInfo = this->hasLineNumberInfo;
+
+    if (this->bstrLine)
+    {
+        pse->bstrLine = SysAllocStringLen(this->bstrLine, SysStringLen(this->bstrLine));
+    }
+}
+
 HRESULT  CompileScriptException::ProcessError(IScanner * pScan, HRESULT hr, ParseNode * pnodeBase)
 {
     // fill in the ScriptException structure

+ 2 - 0
lib/Parser/screrror.h

@@ -70,6 +70,8 @@ public:
         Free();
     }
 
+    void CopyInto(CompileScriptException* cse);
+
     HRESULT  ProcessError(IScanner * pScan, HRESULT hr, ParseNode * pnodeBase);
 
     friend class ActiveScriptError;

+ 55 - 0
lib/Runtime/Base/ScriptContext.cpp

@@ -2070,6 +2070,61 @@ namespace Js
         return parseTree;
     }
 
+    HRESULT ScriptContext::CompileUTF8Core(
+        __in Js::Utf8SourceInfo* utf8SourceInfo,
+        __in SRCINFO *srcInfo,
+        __in BOOL fOriginalUTF8Code,
+        __in LPCUTF8 pszSrc,
+        __in size_t cbLength,
+        __in ULONG grfscr,
+        __in CompileScriptException *pse,
+        __inout charcount_t& cchLength,
+        __out size_t& srcLength,
+        __out uint& sourceIndex,
+        __deref_out Js::ParseableFunctionInfo ** func
+    )
+    {
+        HRESULT hr = E_FAIL;
+        Parser ps(this);
+        (*func) = nullptr;
+
+        ParseNodeProg * parseTree = nullptr;
+        SourceContextInfo * sourceContextInfo = srcInfo->sourceContextInfo;
+        if (fOriginalUTF8Code)
+        {
+            hr = ps.ParseUtf8Source(&parseTree, pszSrc, cbLength, grfscr, pse, &sourceContextInfo->nextLocalFunctionId,
+                sourceContextInfo);
+            cchLength = ps.GetSourceIchLim();
+
+            // Correcting total number of characters.
+            utf8SourceInfo->SetCchLength(cchLength);
+        }
+        else
+        {
+            hr = ps.ParseCesu8Source(&parseTree, pszSrc, cbLength, grfscr, pse, &sourceContextInfo->nextLocalFunctionId,
+                sourceContextInfo);
+        }
+        utf8SourceInfo->SetParseFlags(grfscr);
+        srcLength = ps.GetSourceLength();
+
+        if (SUCCEEDED(hr))
+        {
+            bool isCesu8 = !fOriginalUTF8Code;
+            sourceIndex = this->SaveSourceNoCopy(utf8SourceInfo, cchLength, isCesu8);
+            hr = GenerateByteCode(parseTree, grfscr, this, func, sourceIndex, this->IsForceNoNative(), &ps, pse);
+            utf8SourceInfo->SetByteCodeGenerationFlags(grfscr);
+        }
+#ifdef ENABLE_SCRIPT_DEBUGGING
+        else if (this->IsScriptContextInDebugMode() && !utf8SourceInfo->GetIsLibraryCode() && !utf8SourceInfo->IsInDebugMode())
+        {
+            // In case of syntax error, if we are in debug mode, put the utf8SourceInfo into debug mode.
+            utf8SourceInfo->SetInDebugMode(true);
+        }
+#endif
+
+        return hr;
+    }
+
     JavascriptFunction* ScriptContext::LoadScript(const byte* script, size_t cb,
         SRCINFO const * pSrcInfo, CompileScriptException * pse, Utf8SourceInfo** ppSourceInfo,
         const char16 *rootDisplayName, LoadScriptFlag loadScriptFlag, Js::Var scriptSource)

+ 15 - 0
lib/Runtime/Base/ScriptContext.h

@@ -18,6 +18,7 @@ using namespace PlatformAgnostic;
 
 class NativeCodeGenerator;
 class BackgroundParser;
+class BGParseManager;
 struct IActiveScriptDirect;
 namespace Js
 {
@@ -1278,6 +1279,20 @@ private:
             const char16 *rootDisplayName, LoadScriptFlag loadScriptFlag,
             Js::Var scriptSource = nullptr);
 
+        HRESULT CompileUTF8Core(
+            __in Js::Utf8SourceInfo* utf8SourceInfo,
+            __in SRCINFO *srcInfo,
+            __in BOOL fOriginalUTF8Code,
+            __in LPCUTF8 pszSrc,
+            __in size_t cbLength,
+            __in ULONG grfscr,
+            __in CompileScriptException *pse,
+            __inout charcount_t& cchLength,
+            __out size_t& srcLength,
+            __out uint& sourceIndex,
+            __deref_out Js::ParseableFunctionInfo ** func
+        );
+
         ArenaAllocator* GeneralAllocator() { return &generalAllocator; }
 
 #ifdef ENABLE_BASIC_TELEMETRY

+ 15 - 2
lib/Runtime/Base/ThreadBoundThreadContextManager.cpp

@@ -139,6 +139,8 @@ void ThreadBoundThreadContextManager::DestroyAllContexts()
         entries.Remove(currentEntry);
         ThreadContextTLSEntry::CleanupThread();
 
+        BGParseManager::DeleteBGParseManager();
+
 #if ENABLE_BACKGROUND_JOB_PROCESSOR
         if (s_sharedJobProcessor != NULL)
         {
@@ -158,10 +160,14 @@ void ThreadBoundThreadContextManager::DestroyAllContexts()
 #endif
 }
 
-void ThreadBoundThreadContextManager::DestroyAllContextsAndEntries()
+void ThreadBoundThreadContextManager::DestroyAllContextsAndEntries(bool shouldDeleteCurrentTlsEntry)
 {
     AutoCriticalSection lock(ThreadContext::GetCriticalSection());
 
+    // When shouldDeleteCurrentTlsEntry is true, the comparison in the while loop will always be true, so
+    // every entry in the list will be deleted.
+    ThreadContextTLSEntry* currentThreadEntry = shouldDeleteCurrentTlsEntry ? nullptr : ThreadContextTLSEntry::GetEntryForCurrentThread();
+
     while (!entries.Empty())
     {
         ThreadContextTLSEntry * entry = entries.Head();
@@ -184,9 +190,16 @@ void ThreadBoundThreadContextManager::DestroyAllContextsAndEntries()
             HeapDelete(threadContext);
         }
 
-        ThreadContextTLSEntry::Delete(entry);
+        if (currentThreadEntry != entry)
+        {
+            // Note: This deletes the ThreadContextTLSEntry but does not remove its pointer
+            // from the thread's TLS
+            ThreadContextTLSEntry::Delete(entry);
+        }
     }
 
+    BGParseManager::DeleteBGParseManager();
+
 #if ENABLE_BACKGROUND_JOB_PROCESSOR
     if (s_sharedJobProcessor != NULL)
     {

+ 1 - 1
lib/Runtime/Base/ThreadBoundThreadContextManager.h

@@ -21,7 +21,7 @@ public:
     static ThreadContext * EnsureContextForCurrentThread();
     static void DestroyContextAndEntryForCurrentThread();
     static void DestroyAllContexts();
-    static void DestroyAllContextsAndEntries();
+    static void DestroyAllContextsAndEntries(bool shouldDeleteCurrentTlsEntry);
     static JsUtil::JobProcessor * GetSharedJobProcessor();
 private:
     static EntryList entries;

+ 4 - 0
lib/Runtime/ByteCode/ByteCodeSerializer.cpp

@@ -4027,6 +4027,10 @@ public:
     {
         auto topFunction = ReadInt32(functions, &functionCount);
         firstFunctionId = sourceInfo->GetSrcInfo()->sourceContextInfo->nextLocalFunctionId;
+        // this needs to be addressed somehow...need to be able to merge sourcecontexts?
+        // it looks like in chakra!Parser::ParseFncDecl, nextLocalFunctionId is what the next function number is, then it is incremented
+        // so, if we start with a new/clean ID of 0, then add n to it, then the next
+        //sourceInfo->GetSrcInfo()->sourceContextInfo->nextLocalFunctionId += functionCount * 2;
         sourceInfo->GetSrcInfo()->sourceContextInfo->nextLocalFunctionId += functionCount;
         sourceInfo->EnsureInitialized(functionCount);
         sourceInfo->GetSrcInfo()->sourceContextInfo->EnsureInitialized();