Bug 900784 part 1.6 - Add a test case for the JS start-up bytecode cache. r=mrbkap

This commit is contained in:
Nicolas B. Pierron 2017-04-21 16:57:58 +00:00
Родитель c6509f9e01
Коммит cf3fce3eb4
8 изменённых файлов: 340 добавлений и 0 удалений

Просмотреть файл

@ -97,6 +97,52 @@ ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback,
const char* aName,
uint32_t aFlags = 0);
// This macro is used to wrap a tracing mechanism which is scheduling events
// which are then used by the JavaScript code of test cases to track the code
// path to verify the optimizations are working as expected.
#define TRACE_FOR_TEST(elem, str) \
PR_BEGIN_MACRO \
nsresult rv = NS_OK; \
rv = TestingDispatchEvent(elem, NS_LITERAL_STRING(str)); \
NS_ENSURE_SUCCESS(rv, rv); \
PR_END_MACRO
#define TRACE_FOR_TEST_BOOL(elem, str) \
PR_BEGIN_MACRO \
nsresult rv = NS_OK; \
rv = TestingDispatchEvent(elem, NS_LITERAL_STRING(str)); \
NS_ENSURE_SUCCESS(rv, false); \
PR_END_MACRO
#define TRACE_FOR_TEST_NONE(elem, str) \
PR_BEGIN_MACRO \
TestingDispatchEvent(elem, NS_LITERAL_STRING(str)); \
PR_END_MACRO
static nsresult
TestingDispatchEvent(nsIScriptElement* aScriptElement,
const nsAString& aEventType)
{
static bool sExposeTestInterfaceEnabled = false;
static bool sExposeTestInterfacePrefCached = false;
if (!sExposeTestInterfacePrefCached) {
sExposeTestInterfacePrefCached = true;
Preferences::AddBoolVarCache(&sExposeTestInterfaceEnabled,
"dom.expose_test_interfaces",
false);
}
if (!sExposeTestInterfaceEnabled) {
return NS_OK;
}
nsCOMPtr<nsINode> target(do_QueryInterface(aScriptElement));
if (!target) {
return NS_OK;
}
RefPtr<AsyncEventDispatcher> dispatcher =
new AsyncEventDispatcher(target, aEventType, true, false);
return dispatcher->PostDOMEvent();
}
//////////////////////////////////////////////////////////////
// nsScriptLoadRequest
//////////////////////////////////////////////////////////////
@ -1254,6 +1300,7 @@ nsScriptLoader::RestartLoad(nsScriptLoadRequest *aRequest)
{
MOZ_ASSERT(aRequest->IsBytecode());
aRequest->mScriptBytecode.clearAndFree();
TRACE_FOR_TEST(aRequest->mElement, "scriptloader_fallback");
// Start a new channel from which we explicitly request to stream the source
// instead of the bytecode.
@ -1807,6 +1854,7 @@ nsScriptLoader::ProcessScriptElement(nsIScriptElement *aElement)
request->mLineNo = aElement->GetScriptLineNumber();
request->mProgress = nsScriptLoadRequest::Progress::Loading_Source;
request->mDataType = nsScriptLoadRequest::DataType::Source;
TRACE_FOR_TEST_BOOL(request->mElement, "scriptloader_load_source");
if (request->IsModuleRequest()) {
nsModuleLoadRequest* modReq = request->AsModuleRequest();
@ -2283,6 +2331,22 @@ nsScriptLoader::FillCompileOptionsForRequest(const AutoJSAPI&jsapi,
aOptions->setElement(&elementVal.toObject());
}
// When testing, we want to force use of the bytecode cache.
static bool sForceBytecodeCacheEnabled = false;
static bool sForceBytecodeCachePrefCached = false;
if (!sForceBytecodeCachePrefCached) {
sForceBytecodeCachePrefCached = true;
Preferences::AddBoolVarCache(&sForceBytecodeCacheEnabled,
"dom.script_loader.force_bytecode_cache",
false);
}
// At the moment, the bytecode cache is only triggered if a script is large
// enough to be parsed out of the main thread. Thus, for testing purposes, we
// force parsing any script out of the main thread.
if (sForceBytecodeCacheEnabled) {
aOptions->forceAsync = true;
}
return NS_OK;
}
@ -2375,6 +2439,7 @@ nsScriptLoader::EvaluateScript(nsScriptLoadRequest* aRequest)
if (NS_SUCCEEDED(rv)) {
if (aRequest->IsBytecode()) {
TRACE_FOR_TEST(aRequest->mElement, "scriptloader_execute");
nsJSUtils::ExecutionContext exec(aes.cx(), global);
if (aRequest->mOffThreadToken) {
LOG(("ScriptLoadRequest (%p): Decode Bytecode & Join and Execute", aRequest));
@ -2397,10 +2462,12 @@ nsScriptLoader::EvaluateScript(nsScriptLoadRequest* aRequest)
nsJSUtils::ExecutionContext exec(aes.cx(), global);
JS::Rooted<JSScript*> script(aes.cx());
if (!aRequest->mCacheInfo) {
TRACE_FOR_TEST(aRequest->mElement, "scriptloader_execute");
rv = exec.JoinAndExec(&aRequest->mOffThreadToken, &script);
LOG(("ScriptLoadRequest (%p): Cannot cache anything (cacheInfo = nullptr)",
aRequest));
} else {
TRACE_FOR_TEST(aRequest->mElement, "scriptloader_encode_and_execute");
MOZ_ASSERT(aRequest->mBytecodeOffset ==
aRequest->mScriptBytecode.length());
rv = exec.JoinEncodeAndExec(&aRequest->mOffThreadToken,
@ -2414,6 +2481,7 @@ nsScriptLoader::EvaluateScript(nsScriptLoadRequest* aRequest)
} else {
LOG(("ScriptLoadRequest (%p): Cannot cache anything (rv = %X, script = %p, cacheInfo = %p)",
aRequest, unsigned(rv), script.get(), aRequest->mCacheInfo.get()));
TRACE_FOR_TEST_NONE(aRequest->mElement, "scriptloader_bytecode_failed");
aRequest->mCacheInfo = nullptr;
}
}
@ -2424,6 +2492,7 @@ nsScriptLoader::EvaluateScript(nsScriptLoadRequest* aRequest)
nsJSUtils::ExecutionContext exec(aes.cx(), global);
nsAutoString inlineData;
SourceBufferHolder srcBuf = GetScriptSource(aRequest, inlineData);
TRACE_FOR_TEST(aRequest->mElement, "scriptloader_execute");
rv = exec.CompileAndExec(options, srcBuf);
aRequest->mCacheInfo = nullptr;
}
@ -2527,6 +2596,9 @@ nsScriptLoader::EncodeRequestBytecode(JSContext* aCx, nsScriptLoadRequest* aRequ
{
nsresult rv = NS_OK;
MOZ_ASSERT(aRequest->mCacheInfo);
auto bytecodeFailed = mozilla::MakeScopeExit([&]() {
TRACE_FOR_TEST_NONE(aRequest->mElement, "scriptloader_bytecode_failed");
});
JS::RootedScript script(aCx, aRequest->mScript);
if (!JS::FinishIncrementalEncoding(aCx, script)) {
@ -2567,6 +2639,9 @@ nsScriptLoader::EncodeRequestBytecode(JSContext* aCx, nsScriptLoadRequest* aRequ
if (NS_FAILED(rv)) {
return;
}
bytecodeFailed.release();
TRACE_FOR_TEST_NONE(aRequest->mElement, "scriptloader_bytecode_saved");
}
void
@ -2592,6 +2667,7 @@ nsScriptLoader::GiveUpBytecodeEncoding()
while (!mBytecodeEncodingQueue.isEmpty()) {
RefPtr<nsScriptLoadRequest> request = mBytecodeEncodingQueue.StealFirst();
LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", request.get()));
TRACE_FOR_TEST_NONE(request->mElement, "scriptloader_bytecode_failed");
script.set(request->mScript);
Unused << JS::FinishIncrementalEncoding(aes.cx(), script);
request->mScriptBytecode.clearAndFree();
@ -2604,6 +2680,7 @@ nsScriptLoader::GiveUpBytecodeEncoding()
while (!mBytecodeEncodingQueue.isEmpty()) {
RefPtr<nsScriptLoadRequest> request = mBytecodeEncodingQueue.StealFirst();
LOG(("ScriptLoadRequest (%p): Cannot serialize bytecode", request.get()));
TRACE_FOR_TEST_NONE(request->mElement, "scriptloader_bytecode_failed");
// Note: Do not clear the mScriptBytecode buffer, because the incremental
// encoder owned by the ScriptSource object still has a reference to this
// buffer. This reference would be removed as soon as the ScriptSource
@ -3562,6 +3639,7 @@ nsScriptLoadHandler::EnsureKnownDataType(nsIIncrementalStreamLoader *aLoader)
MOZ_ASSERT(mRequest->IsLoading());
if (mRequest->IsLoadingSource()) {
mRequest->mDataType = nsScriptLoadRequest::DataType::Source;
TRACE_FOR_TEST(mRequest->mElement, "scriptloader_load_source");
return NS_OK;
}
@ -3576,11 +3654,14 @@ nsScriptLoadHandler::EnsureKnownDataType(nsIIncrementalStreamLoader *aLoader)
cic->GetAlternativeDataType(altDataType);
if (altDataType == kBytecodeMimeType) {
mRequest->mDataType = nsScriptLoadRequest::DataType::Bytecode;
TRACE_FOR_TEST(mRequest->mElement, "scriptloader_load_bytecode");
} else {
mRequest->mDataType = nsScriptLoadRequest::DataType::Source;
TRACE_FOR_TEST(mRequest->mElement, "scriptloader_load_source");
}
} else {
mRequest->mDataType = nsScriptLoadRequest::DataType::Source;
TRACE_FOR_TEST(mRequest->mElement, "scriptloader_load_source");
}
MOZ_ASSERT(!mRequest->IsUnknownDataType());
MOZ_ASSERT(mRequest->IsLoading());
@ -3674,6 +3755,10 @@ nsScriptLoadHandler::OnStreamComplete(nsIIncrementalStreamLoader* aLoader,
return rv;
}
#undef TRACE_FOR_TEST
#undef TRACE_FOR_TEST_BOOL
#undef TRACE_FOR_TEST_NONE
#undef LOG_ENABLED
#undef LOG_ERROR
#undef LOG_WARN

Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Add a tag script to save the bytecode</title>
</head>
<body>
<script id="watchme" src="file_js_cache.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,5 @@
function baz() {}
function bar() {}
function foo() { bar() }
foo();

Просмотреть файл

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Save the bytecode when all scripts are executed</title>
</head>
<body>
<script id="watchme" src="file_js_cache_save_after_load.js"></script>
</body>
</html>

Просмотреть файл

@ -0,0 +1,15 @@
function send_ping() {
window.dispatchEvent(new Event("ping"));
}
send_ping(); // ping (=1)
window.addEventListener("load", function () {
send_ping(); // ping (=2)
// Append a script which should call |foo|, before the encoding of this script
// bytecode.
var script = document.createElement("script");
script.type = "text/javascript";
script.innerText = "send_ping();"; // ping (=3)
document.head.appendChild(script);
});

Просмотреть файл

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Add a tag script to save the bytecode</title>
</head>
<body>
<script id="watchme" src="file_js_cache.js"
integrity="sha384-8YSwN2ywq1SVThihWhj7uTVZ4UeIDwo3GgdPYnug+C+OS0oa6kH2IXBclwMaDJFb">
</script>
</body>
</html>

Просмотреть файл

@ -732,6 +732,14 @@ skip-if = toolkit == 'android'
[test_root_iframe.html]
[test_screen_orientation.html]
[test_script_loader_crossorigin_data_url.html]
[test_script_loader_js_cache.html]
support-files =
file_js_cache.html
file_js_cache_with_sri.html
file_js_cache.js
file_js_cache_save_after_load.html
file_js_cache_save_after_load.js
skip-if = (os == 'linux' || toolkit == 'android') # mochitest are executed on a single core
[test_selection_with_anon_trees.html]
skip-if = (toolkit == 'android' || asan) # bug 1330526
[test_setInterval_uncatchable_exception.html]

Просмотреть файл

@ -0,0 +1,195 @@
<!DOCTYPE html>
<html>
<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=900784 -->
<!-- The JS bytecode cache is not supposed to be observable. To make it
observable, the nsScriptLoader is instrumented to trigger events on the
script tag. These events are followed to reconstruct the code path taken by
the script loader and associate a simple name which is checked in these
test cases.
-->
<head>
<meta charset="utf-8">
<title>Test for saving and loading bytecode in/from the necko cache</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script type="application/javascript">
// This is the state machine of the trace events produced by the
// nsScriptLoader. This state machine is used to give a name to each
// code path, such that we can assert each code path with a single word.
var scriptLoaderStateMachine = {
"scriptloader_load_source": {
"scriptloader_encode_and_execute": {
"scriptloader_bytecode_saved": "bytecode_saved",
"scriptloader_bytecode_failed": "bytecode_failed"
},
"scriptloader_execute": "source_exec"
},
"scriptloader_load_bytecode": {
"scriptloader_fallback": {
// Replicate the top-level state machine without
// "scriptloader_load_bytecode" transition.
"scriptloader_load_source": {
"scriptloader_encode_and_execute": {
"scriptloader_bytecode_saved": "fallback_bytecode_saved",
"scriptloader_bytecode_failed": "fallback_bytecode_failed"
},
"scriptloader_execute": "fallback_source_exec"
}
},
"scriptloader_execute": "bytecode_exec"
}
};
function flushNeckoCache() {
return new Promise (resolve => {
// We need to do a GC pass to ensure the cache entry has been freed.
SpecialPowers.gc();
var nsICacheTesting = SpecialPowers.Ci.nsICacheTesting;
var cacheTesting = SpecialPowers.Services.cache2;
cacheTesting = cacheTesting.QueryInterface(nsICacheTesting);
cacheTesting.flush(() => { resolve(); });
});
};
function WaitForScriptTagEvent(url) {
var iframe = document.createElement("iframe");
document.body.appendChild(iframe);
var stateMachine = scriptLoaderStateMachine;
var stateHistory = [];
var stateMachineResolve, stateMachineReject;
var statePromise = new Promise((resolve, reject) => {
stateMachineResolve = resolve;
stateMachineReject = reject;
});
var ping = 0;
// Walk the script loader state machine with the emitted events.
function log_event(evt) {
// If we have multiple script tags in the loaded source, make sure
// we only watch a single one.
if (evt.target.id != "watchme")
return;
dump("## ScriptLoader event: " + evt.type + "\n");
stateHistory.push(evt.type)
if (typeof stateMachine == "object")
stateMachine = stateMachine[evt.type];
if (typeof stateMachine == "string") {
// We arrived to a final state, report the name of it.
var result = stateMachine;
if (ping) {
result = `${result} & ping(=${ping})`;
}
stateMachineResolve(result);
} else if (stateMachine === undefined) {
// We followed an unknown transition, report the known history.
stateMachineReject(stateHistory);
}
}
var iwin = iframe.contentWindow;
iwin.addEventListener("scriptloader_load_source", log_event);
iwin.addEventListener("scriptloader_load_bytecode", log_event);
iwin.addEventListener("scriptloader_generate_bytecode", log_event);
iwin.addEventListener("scriptloader_execute", log_event);
iwin.addEventListener("scriptloader_encode_and_execute", log_event);
iwin.addEventListener("scriptloader_bytecode_saved", log_event);
iwin.addEventListener("scriptloader_bytecode_failed", log_event);
iwin.addEventListener("scriptloader_fallback", log_event);
iwin.addEventListener("ping", (evt) => {
ping += 1;
dump(`## Content event: ${evt.type} (=${ping})\n`);
});
iframe.src = url;
statePromise.then(() => {
document.body.removeChild(iframe);
});
return statePromise;
}
promise_test(async function() {
// Setting dom.expose_test_interfaces pref causes the
// nsScriptLoadRequest to fire event on script tags, with information
// about its internal state. The nsScriptLoader source send events to
// trace these and resolve a promise with the path taken by the
// script loader.
//
// Setting dom.script_loader.force_bytecode_cache causes the
// nsScriptLoadRequest to force all the conditions necessary to make a
// script be saved as bytecode in the alternate data storage provided
// by the channel (necko cache).
await SpecialPowers.pushPrefEnv({set: [
['dom.script_loader.bytecode_cache.enabled', true],
['dom.expose_test_interfaces', true],
['dom.script_loader.force_bytecode_cache', true]
]});
// Load the test page, and verify that the code path taken by the
// nsScriptLoadRequest corresponds to the code path which is loading a
// source and saving it as bytecode.
var stateMachineResult = WaitForScriptTagEvent("file_js_cache.html");
assert_equals(await stateMachineResult, "bytecode_saved",
"[1] ScriptLoadRequest status after the first visit");
// When the bytecode is saved, we have to flush the cache to read it.
await flushNeckoCache();
// Reload the same test page, and verify that the code path taken by
// the nsScriptLoadRequest corresponds to the code path which is
// loading bytecode and executing it.
stateMachineResult = WaitForScriptTagEvent("file_js_cache.html");
assert_equals(await stateMachineResult, "bytecode_exec",
"[2] ScriptLoadRequest status after the second visit");
// Load another page which loads the same script with an SRI, while
// the cached bytecode does not have any. This should fallback to
// loading the source before saving the bytecode once more.
stateMachineResult = WaitForScriptTagEvent("file_js_cache_with_sri.html");
assert_equals(await stateMachineResult, "fallback_bytecode_saved",
"[3] ScriptLoadRequest status after the SRI hash");
// When the bytecode is saved, we have to flush the cache to read it.
await flushNeckoCache();
// Loading a page, which has the same SRI should verify the SRI and
// continue by executing the bytecode.
var stateMachineResult1 = WaitForScriptTagEvent("file_js_cache_with_sri.html");
// Loading a page which does not have a SRI while we have one in the
// cache should not change anything. We should also be able to load
// the cache simultanesouly.
var stateMachineResult2 = WaitForScriptTagEvent("file_js_cache.html");
assert_equals(await stateMachineResult1, "bytecode_exec",
"[4] ScriptLoadRequest status after same SRI hash");
assert_equals(await stateMachineResult2, "bytecode_exec",
"[5] ScriptLoadRequest status after visit with no SRI");
}, "Check the JS bytecode cache");
promise_test(async function() {
// (see above)
await SpecialPowers.pushPrefEnv({set: [
['dom.script_loader.bytecode_cache.enabled', true],
['dom.expose_test_interfaces', true],
['dom.script_loader.force_bytecode_cache', true]
]});
// The test page add a new script which generate a "ping" event, which
// should be recorded before the bytecode is stored in the cache.
var stateMachineResult =
WaitForScriptTagEvent("file_js_cache_save_after_load.html");
assert_equals(await stateMachineResult, "bytecode_saved & ping(=3)",
"Wait on all scripts to be executed");
}, "Save bytecode after the initialization of the page");
done();
</script>
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=900784">Mozilla Bug 900784</a>
</body>
</html>