| //------------------------------------------------------------------------------------------------------- |
| // 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" |
| |
| #define BGPARSE_FLAGS (fscrGlobalCode | fscrWillDeferFncParse | fscrCanDeferFncParse | fscrCreateParserState) |
| |
| // Global, process singleton |
| BGParseManager* BGParseManager::s_BGParseManager = nullptr; |
| DWORD BGParseManager::s_lastCookie = 0; |
| DWORD BGParseManager::s_completed = 0; |
| DWORD BGParseManager::s_failed = 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; |
| } |
| |
| DWORD BGParseManager::IncCompleted() |
| { |
| AutoCriticalSection lock(&s_staticMemberLock); |
| return ++s_completed; |
| } |
| DWORD BGParseManager::IncFailed() |
| { |
| AutoCriticalSection lock(&s_staticMemberLock); |
| return ++s_failed; |
| } |
| |
| |
| // 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. Parameters have the following impact: |
| // - waitForResults: creates an event on the returned WorkItem that the caller can wait for |
| // - removeJob: removes the job from the list that contained it, and marks it for discard if it is processing |
| // Note: runs on any thread |
| BGParseWorkItem* BGParseManager::FindJob(DWORD dwCookie, bool waitForResults, bool removeJob) |
| { |
| Assert(dwCookie != 0); |
| Assert(!waitForResults || !removeJob); |
| |
| AutoOptionalCriticalSection autoLock(Processor()->GetCriticalSection()); |
| 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 (removeJob) |
| { |
| this->workitemsProcessed.Unlink(matchedWorkitem); |
| } |
| } |
| } |
| |
| 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; |
| if (removeJob) |
| { |
| this->workitemsProcessing.Unlink(matchedWorkitem); |
| // Since the job is still processing, it cannot be freed immediately. Mark it as discarded so |
| // that it can be freed later. |
| matchedWorkitem->Discard(); |
| } |
| } |
| } |
| |
| // 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; |
| }); |
| |
| if (removeJob && matchedWorkitem != nullptr) |
| { |
| Processor()->RemoveJob(matchedWorkitem); |
| } |
| } |
| |
| // 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, WCHAR** sourceUrl) |
| { |
| HRESULT hr = E_FAIL; |
| |
| // Find the job associated with this cookie |
| BGParseWorkItem* workitem = FindJob(cookie, false /*waitForResults*/, false /*removeJob*/); |
| if (workitem != nullptr) |
| { |
| if (ppszSrc != nullptr) |
| { |
| (*ppszSrc) = workitem->GetScriptSrc(); |
| } |
| if (pcbLength != nullptr) |
| { |
| (*pcbLength) = workitem->GetScriptLength(); |
| } |
| if (sourceUrl != nullptr) |
| { |
| (*sourceUrl) = workitem->GetScriptPath(); |
| } |
| |
| 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::FunctionBody** ppFunc, |
| CompileScriptException* pse, |
| size_t& srcLength, |
| Js::Utf8SourceInfo* utf8SourceInfo, |
| uint& sourceIndex) |
| { |
| // 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 /*waitForResults*/, false /*removeJob*/); |
| if (workitem != nullptr) |
| { |
| // Synchronously wait for the job to complete |
| workitem->WaitForCompletion(); |
| |
| Js::FunctionBody* functionBody = nullptr; |
| hr = workitem->DeserializeParseResults(scriptContextUI, pszSrc, pSrcInfo, utf8SourceInfo, &functionBody, srcLength, sourceIndex); |
| (*ppFunc) = functionBody; |
| if (pse != nullptr) |
| { |
| workitem->TransferCSE(pse); |
| } |
| |
| if (hr == S_OK) |
| { |
| BGParseManager::IncCompleted(); |
| } |
| else |
| { |
| BGParseManager::IncFailed(); |
| } |
| } |
| |
| 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 != nullptr ? workitem->GetCookie() : -1, |
| ::GetCurrentThreadId(), |
| now.ToMilliseconds(), |
| hr |
| ); |
| } |
| |
| return hr; |
| } |
| |
| // Finds and removes the workitem associated with the provided cookie. If the workitem is processed |
| // or not yet processed, the workitem is simply removed and freed. If the workitem is being processed, |
| // it is removed from the list and will be freed after the job is processed (with its script source |
| // buffer). |
| // Returns true when the caller should free the script source buffer. Otherwise, when false is returned, |
| // the workitem is responsible for freeing the script source buffer. |
| bool BGParseManager::DiscardParseResults(DWORD cookie, void* buffer) |
| { |
| BGParseWorkItem* workitem = FindJob(cookie, false /*waitForResults*/, true /*removeJob*/); |
| bool callerOwnsSourceBuffer = true; |
| if (workitem != nullptr) |
| { |
| Assert(buffer == workitem->GetScriptSrc()); |
| |
| if (!workitem->IsDiscarded()) |
| { |
| HeapDelete(workitem); |
| } |
| else |
| { |
| callerOwnsSourceBuffer = false; |
| } |
| } |
| |
| if (PHASE_TRACE1(Js::BgParsePhase)) |
| { |
| Js::Tick now = Js::Tick::Now(); |
| Output::Print( |
| _u("[BgParse: Discard -- cookie: %04d on thread 0x%X at %.2f ms, workitem: 0x%p, workitem owns buffer: %u]\n"), |
| cookie, |
| ::GetCurrentThreadId(), |
| now.ToMilliseconds(), |
| workitem, |
| !callerOwnsSourceBuffer |
| ); |
| } |
| |
| return callerOwnsSourceBuffer; |
| } |
| |
| // 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->workitemsProcessed.Contains(workItem)); |
| if (this->workitemsProcessing.Contains(workItem)) |
| { |
| // Move this processed workitem from the processing list to |
| // the processed list |
| this->workitemsProcessing.Unlink(workItem); |
| this->workitemsProcessed.LinkToEnd(workItem); |
| } |
| else |
| { |
| // If this workitem isn't in the processing queue, it should |
| // already be discarded |
| Assert(workItem->IsDiscarded()); |
| } |
| } |
| else |
| { |
| // When the manager is shutting down, workitems are processed through the JobProcessor |
| // without executing (i.e., !succeeded). So, mark it for discard so that it can be freed. |
| workItem->Discard(); |
| } |
| |
| workItem->JobProcessed(succeeded); |
| } |
| |
| // 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), |
| discarded(false) |
| { |
| this->cookie = BGParseManager::GetNextCookie(); |
| |
| Assert(fullPath != nullptr); |
| this->path = SysAllocString(fullPath); |
| } |
| |
| BGParseWorkItem::~BGParseWorkItem() |
| { |
| SysFreeString(this->path); |
| if (this->complete != nullptr) |
| { |
| HeapDelete(this->complete); |
| } |
| |
| if (this->bufferReturn != nullptr) |
| { |
| ::CoTaskMemFree(this->bufferReturn); |
| } |
| |
| if (this->discarded) |
| { |
| // When this workitem has been discarded, this is the last reference |
| // to the script source, so free it now during destruction. |
| ::HeapFree(GetProcessHeap(), 0, (void*)this->script); |
| } |
| } |
| |
| 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, |
| 0, // dlnHost |
| 0, // ulColumnHost |
| 0, // lnMinHost |
| 0, // ichMinHost |
| static_cast<ULONG>(cb / sizeof(utf8char_t)), // ichLimHost |
| 0, // ulCharOffset |
| kmodGlobal, // mod |
| 0 // grfsi |
| }; |
| |
| ENTER_PINNED_SCOPE(Js::Utf8SourceInfo, sourceInfo); |
| sourceInfo = Js::Utf8SourceInfo::NewWithNoCopy(scriptContext, (LPUTF8)this->script, (int32)this->cb, static_cast<int32>(this->cb), &si, false); |
| |
| charcount_t cchLength = 0; |
| uint sourceIndex = 0; |
| Js::ParseableFunctionInfo * func = nullptr; |
| |
| Parser ps(scriptContext); |
| this->parseHR = scriptContext->CompileUTF8Core( |
| ps, |
| sourceInfo, |
| &si, |
| true, // fOriginalUtf8Code |
| this->script, |
| this->cb, |
| BGPARSE_FLAGS, |
| &this->cse, |
| cchLength, |
| this->parseSourceLength, |
| sourceIndex, |
| &func, |
| nullptr // pDataCache |
| ); |
| |
| |
| 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(), |
| &this->bufferReturn, |
| &this->bufferReturnBytes, |
| GENERATE_BYTE_CODE_PARSER_STATE | GENERATE_BYTE_CODE_COTASKMEMALLOC |
| ); |
| END_TEMP_ALLOCATOR(tempAllocator, scriptContext); |
| Assert(this->parseHR == S_OK); |
| } |
| else |
| { |
| Assert(this->cse.ei.bstrSource != nullptr); |
| Assert(func == nullptr); |
| } |
| |
| LEAVE_PINNED_SCOPE(); |
| } |
| |
| // Deserializes the background parse results into this thread |
| // Note: *must* run on a UI/Execution thread with an available ScriptContext |
| HRESULT BGParseWorkItem::DeserializeParseResults( |
| Js::ScriptContext* scriptContextUI, |
| LPCUTF8 pszSrc, |
| SRCINFO const * pSrcInfo, |
| Js::Utf8SourceInfo* utf8SourceInfo, |
| Js::FunctionBody** functionBodyReturn, |
| size_t& srcLength, |
| uint& sourceIndex |
| ) |
| { |
| HRESULT hr = this->parseHR; |
| if (hr == S_OK) |
| { |
| if (utf8SourceInfo == nullptr) |
| { |
| scriptContextUI->MakeUtf8SourceInfo( |
| this->script, |
| this->cb, |
| pSrcInfo, |
| &utf8SourceInfo, |
| LoadScriptFlag_Utf8Source, |
| nullptr // Var scriptSource |
| ); |
| } |
| |
| srcLength = this->parseSourceLength; |
| sourceIndex = scriptContextUI->SaveSourceNoCopy(utf8SourceInfo, (int)srcLength, false /*isCesu8*/); |
| Assert(sourceIndex != Js::Constants::InvalidSourceIndex); |
| |
| Field(Js::FunctionBody*) functionBody = nullptr; |
| hr = Js::ByteCodeSerializer::DeserializeFromBuffer( |
| scriptContextUI, |
| BGPARSE_FLAGS, |
| (const byte *)pszSrc, |
| pSrcInfo, |
| this->bufferReturn, |
| nullptr, // nativeModule |
| &functionBody, |
| sourceIndex |
| ); |
| |
| if (hr == S_OK) |
| { |
| // The buffer is now owned by the output of DeserializeFromBuffer |
| (*functionBodyReturn) = functionBody; |
| this->bufferReturn = nullptr; |
| this->bufferReturnBytes = 0; |
| } |
| } |
| |
| return hr; |
| } |
| |
| 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(const bool succeeded) |
| { |
| Assert(Manager()->Processor()->GetCriticalSection()->IsLocked()); |
| |
| if (IsDiscarded()) |
| { |
| Js::Tick now = Js::Tick::Now(); |
| Output::Print( |
| _u("[BgParse: Discard Before GetResults -- cookie: %04d on thread 0x%X at %.2f ms]\n"), |
| GetCookie(), |
| ::GetCurrentThreadId(), |
| now.ToMilliseconds() |
| ); |
| |
| // When a workitem has been discarded while processing, there are now other |
| // references to it, so free it now |
| Assert((this->Next() == nullptr && this->Previous() == nullptr) || !succeeded); |
| HeapDelete(this); |
| } |
| else 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(); |
| } |
| } |