зеркало из https://github.com/mozilla/gecko-dev.git
Bug 900784 part 1.6 - Add a test case for the JS start-up bytecode cache. r=mrbkap
This commit is contained in:
Родитель
c6509f9e01
Коммит
cf3fce3eb4
|
@ -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>
|
Загрузка…
Ссылка в новой задаче