blob: cf939cf2907d2deb7c26d72eb62b5d657737e684 [file] [log] [blame]
//-------------------------------------------------------------------------------------------------------
// 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();
}
}