Skip to content
Snippets Groups Projects
Commit fa7b6b54 authored by aa@chromium.org's avatar aa@chromium.org
Browse files

Re-commit "Add new user script injection point:

document_idle."

Original code review: http://codereview.chromium.org/339064

BUG=26126
TBR=rafaelw@chromium.org

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@30796 0039d316-1c4b-4281-b951-d872f2087c98
parent d12f7a95
No related merge requests found
Showing
with 250 additions and 49 deletions
......@@ -166,6 +166,11 @@ void UserScriptMaster::ScriptReloader::LoadScriptsFromDirectory(
file = enumerator.Next()) {
result->push_back(UserScript());
UserScript& user_script = result->back();
// We default standalone user scripts to document-end for better
// Greasemonkey compatibility.
user_script.set_run_location(UserScript::DOCUMENT_END);
// Push single js file in this UserScript.
GURL url(std::string(chrome::kUserScriptScheme) + ":/" +
net::FilePathToFileURL(file).ExtractFileName());
......
......@@ -3302,6 +3302,8 @@
'renderer/renderer_web_database_observer.h',
'renderer/socket_stream_dispatcher.cc',
'renderer/socket_stream_dispatcher.h',
'renderer/user_script_idle_scheduler.cc',
'renderer/user_script_idle_scheduler.h',
'renderer/user_script_slave.cc',
'renderer/user_script_slave.h',
'renderer/visitedlink_slave.cc',
......
......@@ -346,7 +346,24 @@ learn about the
<tr>
<td>run_at</td>
<td>string</td>
<td>Optional. Controls when the files in <code>js</code> are injected. Can be <code>"document_start"</code> or <code>"document_end"</code>. Defaults to <code>"document_end"</code>. In the case of <code>"document_start"</code>, the files are injected after any files from <code>"css"</code>, but before any other DOM is constructed or any other script is run. In the case of <code>"document_end"</code>, the files are injected after the DOM is complete, but before subresources like images and frames have necessarily loaded.</td>
<td>Optional. Controls when the files in <code>js</code> are injected. Can be <code>"document_start"</code>, <code>"document_end"</code>, or <code>"document_idle"</code>. Defaults to <code>"document_idle"</code>.
<br><br>
In the case of <code>"document_start"</code>, the files are injected after any files from <code>"css"</code>, but before any other DOM is constructed or any other script is run.
<br><br>
In the case of <code>"document_end"</code>, the files are injected immediately after the DOM is complete, but before subresources like images and frames have loaded.
<br><br>
In the case of <code>"document_idle"</code>, the browser chooses a time to inject scripts between <code>"document_end"</code> and immediately after the <code><a href="http://www.whatwg.org/specs/web-apps/current-work/#handler-onload">window.onload</a></code> event fires. The exact moment of injection depends on how complex the document is and how long it is taking to load, and is optimized for page load speed.
<br><br>
<b>NOTE:</b> In <code>document_idle</code>, content scripts may not necessarily receive the window.onload event, because they may run after it has
already fired. In most cases, listening for the onload event is unnecessary for content scripts running at <code>document_idle</code> because they are guaranteed to run after the DOM is complete. If your script definitely needs to run after <code>window.onload</code> you can check if it has already fired by using the <code><a href="http://www.whatwg.org/specs/web-apps/current-work/#dom-document-readystate">document.readyState</a></code> property.</td>
</tr>
</tbody></table>
......
......@@ -104,7 +104,24 @@ learn about the
<tr>
<td>run_at</td>
<td>string</td>
<td>Optional. Controls when the files in <code>js</code> are injected. Can be <code>"document_start"</code> or <code>"document_end"</code>. Defaults to <code>"document_end"</code>. In the case of <code>"document_start"</code>, the files are injected after any files from <code>"css"</code>, but before any other DOM is constructed or any other script is run. In the case of <code>"document_end"</code>, the files are injected after the DOM is complete, but before subresources like images and frames have necessarily loaded.</td>
<td>Optional. Controls when the files in <code>js</code> are injected. Can be <code>"document_start"</code>, <code>"document_end"</code>, or <code>"document_idle"</code>. Defaults to <code>"document_idle"</code>.
<br><br>
In the case of <code>"document_start"</code>, the files are injected after any files from <code>"css"</code>, but before any other DOM is constructed or any other script is run.
<br><br>
In the case of <code>"document_end"</code>, the files are injected immediately after the DOM is complete, but before subresources like images and frames have loaded.
<br><br>
In the case of <code>"document_idle"</code>, the browser chooses a time to inject scripts between <code>"document_end"</code> and immediately after the <code><a href="http://www.whatwg.org/specs/web-apps/current-work/#handler-onload">window.onload</a></code> event fires. The exact moment of injection depends on how complex the document is and how long it is taking to load, and is optimized for page load speed.
<br><br>
<b>NOTE:</b> In <code>document_idle</code>, content scripts may not necessarily receive the window.onload event, because they may run after it has
already fired. In most cases, listening for the onload event is unnecessary for content scripts running at <code>document_idle</code> because they are guaranteed to run after the DOM is complete. If your script definitely needs to run after <code>window.onload</code> you can check if it has already fired by using the <code><a href="http://www.whatwg.org/specs/web-apps/current-work/#dom-document-readystate">document.readyState</a></code> property.</td>
</tr>
</table>
......
......@@ -208,6 +208,8 @@ bool Extension::LoadUserScriptHelper(const DictionaryValue* content_script,
result->set_run_location(UserScript::DOCUMENT_START);
} else if (run_location == values::kRunAtDocumentEnd) {
result->set_run_location(UserScript::DOCUMENT_END);
} else if (run_location == values::kRunAtDocumentIdle) {
result->set_run_location(UserScript::DOCUMENT_IDLE);
} else {
*error = ExtensionErrorUtils::FormatErrorMessage(errors::kInvalidRunAt,
IntToString(definition_index));
......
......@@ -52,6 +52,7 @@ const wchar_t* kOptionsPage = L"options_page";
namespace extension_manifest_values {
const char* kRunAtDocumentStart = "document_start";
const char* kRunAtDocumentEnd = "document_end";
const char* kRunAtDocumentIdle = "document_idle";
const char* kPageActionTypeTab = "tab";
const char* kPageActionTypePermanent = "permanent";
} // namespace extension_manifest_values
......
......@@ -54,6 +54,7 @@ namespace extension_manifest_keys {
namespace extension_manifest_values {
extern const char* kRunAtDocumentStart;
extern const char* kRunAtDocumentEnd;
extern const char* kRunAtDocumentIdle;
extern const char* kPageActionTypeTab;
extern const char* kPageActionTypePermanent;
} // namespace extension_manifest_values
......
......@@ -28,6 +28,10 @@ class UserScript {
// anything else happens.
DOCUMENT_END, // After the entire document is parsed. Same as
// DOMContentLoaded.
DOCUMENT_IDLE, // Sometime after DOMContentLoaded, as soon as the document
// is "idle". Currently this uses the simple heuristic of:
// min(DOM_CONTENT_LOADED + TIMEOUT, ONLOAD), but no
// particular injection point is guaranteed.
RUN_LOCATION_LAST // Leave this as the last item.
};
......@@ -88,9 +92,10 @@ class UserScript {
typedef std::vector<File> FileList;
// Constructor. Default the run location to document end, which is like
// Greasemonkey and probably more useful for typical scripts.
UserScript() : run_location_(DOCUMENT_END) {}
// Constructor. Default the run location to document idle, which is similar
// to Greasemonkey but should result in better page load times for fast-
// loading pages.
UserScript() : run_location_(DOCUMENT_IDLE) {}
// The place in the document to run the script.
RunLocation run_location() const { return run_location_; }
......
......@@ -122,5 +122,5 @@ TEST(UserScriptTest, Pickle) {
TEST(UserScriptTest, Defaults) {
UserScript script;
ASSERT_EQ(UserScript::DOCUMENT_END, script.run_location());
ASSERT_EQ(UserScript::DOCUMENT_IDLE, script.run_location());
}
......@@ -8,6 +8,7 @@
#include "base/scoped_ptr.h"
#include "base/time.h"
#include "chrome/common/page_transition_types.h"
#include "chrome/renderer/user_script_idle_scheduler.h"
#include "webkit/api/public/WebDataSource.h"
#include "webkit/glue/alt_error_page_resource_fetcher.h"
#include "webkit/glue/password_form.h"
......@@ -33,6 +34,13 @@ class NavigationState : public WebKit::WebDataSource::ExtraData {
return static_cast<NavigationState*>(ds->extraData());
}
UserScriptIdleScheduler* user_script_idle_scheduler() {
return user_script_idle_scheduler_.get();
}
void set_user_script_idle_scheduler(UserScriptIdleScheduler* scheduler) {
user_script_idle_scheduler_.reset(scheduler);
}
// Contains the page_id for this navigation or -1 if there is none yet.
int32 pending_page_id() const { return pending_page_id_; }
......@@ -173,7 +181,8 @@ class NavigationState : public WebKit::WebDataSource::ExtraData {
request_committed_(false),
is_content_initiated_(is_content_initiated),
pending_page_id_(pending_page_id),
postpone_loading_data_(false) {
postpone_loading_data_(false),
user_script_idle_scheduler_(NULL) {
}
PageTransition::Type transition_type_;
......@@ -195,6 +204,7 @@ class NavigationState : public WebKit::WebDataSource::ExtraData {
std::string security_info_;
bool postpone_loading_data_;
std::string postponed_data_;
scoped_ptr<UserScriptIdleScheduler> user_script_idle_scheduler_;
DISALLOW_COPY_AND_ASSIGN(NavigationState);
};
......
......@@ -1829,6 +1829,10 @@ void RenderView::willClose(WebFrame* frame) {
if (url.SchemeIs("http") || url.SchemeIs("https"))
DumpLoadHistograms();
}
WebDataSource* ds = frame->dataSource();
NavigationState* navigation_state = NavigationState::FromDataSource(ds);
navigation_state->user_script_idle_scheduler()->Cancel();
}
void RenderView::loadURLExternally(
......@@ -2031,11 +2035,13 @@ void RenderView::didCompleteClientRedirect(
void RenderView::didCreateDataSource(WebFrame* frame, WebDataSource* ds) {
// The rest of RenderView assumes that a WebDataSource will always have a
// non-null NavigationState.
if (pending_navigation_state_.get()) {
ds->setExtraData(pending_navigation_state_.release());
} else {
ds->setExtraData(NavigationState::CreateContentInitiated());
}
NavigationState* state = pending_navigation_state_.get() ?
pending_navigation_state_.release() :
NavigationState::CreateContentInitiated();
state->set_user_script_idle_scheduler(
new UserScriptIdleScheduler(this, frame));
ds->setExtraData(state);
}
void RenderView::didStartProvisionalLoad(WebFrame* frame) {
......@@ -2250,14 +2256,6 @@ void RenderView::didCreateDocumentElement(WebFrame* frame) {
ExtensionProcessBindings::SetViewType(webview(), view_type_);
}
while (!pending_code_execution_queue_.empty()) {
scoped_refptr<CodeExecutionInfo> info =
pending_code_execution_queue_.front();
OnExecuteCode(info->request_id, info->extension_id, info->is_js_code,
info->code_string);
pending_code_execution_queue_.pop();
}
// Notify the browser about non-blank documents loading in the top frame.
GURL url = frame->url();
if (url.is_valid() && url.spec() != chrome::kAboutBlankURL) {
......@@ -2292,6 +2290,26 @@ void RenderView::didFinishDocumentLoad(WebFrame* frame) {
RenderThread::current()->user_script_slave()->InjectScripts(
frame, UserScript::DOCUMENT_END);
}
navigation_state->user_script_idle_scheduler()->DidFinishDocumentLoad();
}
void RenderView::OnUserScriptIdleTriggered(WebFrame* frame) {
if (RenderThread::current()) { // Will be NULL during unit tests.
RenderThread::current()->user_script_slave()->InjectScripts(
frame, UserScript::DOCUMENT_IDLE);
}
WebFrame* main_frame = webview()->mainFrame();
if (frame == main_frame) {
while (!pending_code_execution_queue_.empty()) {
scoped_refptr<CodeExecutionInfo> info =
pending_code_execution_queue_.front();
ExecuteCodeImpl(main_frame, info->request_id, info->extension_id,
info->is_js_code, info->code_string);
pending_code_execution_queue_.pop();
}
}
}
void RenderView::didHandleOnloadEvents(WebFrame* frame) {
......@@ -2307,6 +2325,7 @@ void RenderView::didFinishLoad(WebFrame* frame) {
NavigationState* navigation_state = NavigationState::FromDataSource(ds);
DCHECK(navigation_state);
navigation_state->set_finish_load_time(Time::Now());
navigation_state->user_script_idle_scheduler()->DidFinishLoad();
}
void RenderView::didChangeLocationWithinPage(
......@@ -3662,28 +3681,40 @@ void RenderView::OnSetEditCommandsForNextKeyEvent(
void RenderView::OnExecuteCode(int request_id, const std::string& extension_id,
bool is_js_code,
const std::string& code_string) {
if (is_loading_) {
scoped_refptr<CodeExecutionInfo> info = new CodeExecutionInfo(
request_id, extension_id, is_js_code, code_string);
pending_code_execution_queue_.push(info);
return;
}
WebFrame* main_frame = webview() ? webview()->mainFrame() : NULL;
if (!main_frame) {
Send(new ViewMsg_ExecuteCodeFinished(routing_id_, request_id, false));
return;
}
WebDataSource* ds = main_frame->dataSource();
NavigationState* navigation_state = NavigationState::FromDataSource(ds);
if (!navigation_state->user_script_idle_scheduler()->has_run()) {
scoped_refptr<CodeExecutionInfo> info = new CodeExecutionInfo(
request_id, extension_id, is_js_code, code_string);
pending_code_execution_queue_.push(info);
return;
}
ExecuteCodeImpl(main_frame, request_id, extension_id, is_js_code,
code_string);
}
void RenderView::ExecuteCodeImpl(WebFrame* frame,
int request_id,
const std::string& extension_id,
bool is_js_code,
const std::string& code_string) {
if (is_js_code) {
std::vector<WebScriptSource> sources;
sources.push_back(
WebScriptSource(WebString::fromUTF8(code_string)));
UserScriptSlave::InsertInitExtensionCode(&sources, extension_id);
main_frame->executeScriptInIsolatedWorld(
frame->executeScriptInIsolatedWorld(
UserScriptSlave::GetIsolatedWorldId(extension_id),
&sources.front(), sources.size(), EXTENSION_GROUP_CONTENT_SCRIPTS);
} else {
main_frame->insertStyleText(WebString::fromUTF8(code_string), WebString());
frame->insertStyleText(WebString::fromUTF8(code_string), WebString());
}
Send(new ViewMsg_ExecuteCodeFinished(routing_id_, request_id, true));
......
......@@ -435,6 +435,10 @@ class RenderView : public RenderWidget,
// Sends a message and runs a nested message loop.
bool SendAndRunNestedMessageLoop(IPC::SyncMessage* message);
// Called when the "idle" user script state has been reached. See
// UserScript::DOCUMENT_IDLE.
void OnUserScriptIdleTriggered(WebKit::WebFrame* frame);
protected:
// RenderWidget overrides:
virtual void Close();
......@@ -620,6 +624,11 @@ class RenderView : public RenderWidget,
const std::string& extension_id,
bool is_js_code,
const std::string& code_string);
void ExecuteCodeImpl(WebKit::WebFrame* frame,
int request_id,
const std::string& extension_id,
bool is_js_code,
const std::string& code_string);
void OnUpdateBackForwardListCount(int back_list_count,
int forward_list_count);
void OnGetAccessibilityInfo(
......
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/renderer/user_script_idle_scheduler.h"
#include "base/message_loop.h"
#include "chrome/renderer/render_view.h"
namespace {
// The length of time to wait after the DOM is complete to try and run user
// scripts.
const int kUserScriptIdleTimeoutMs = 200;
}
UserScriptIdleScheduler::UserScriptIdleScheduler(RenderView* view,
WebKit::WebFrame* frame)
: ALLOW_THIS_IN_INITIALIZER_LIST(method_factory_(this)), view_(view),
frame_(frame), has_run_(false) {
}
void UserScriptIdleScheduler::DidFinishDocumentLoad() {
MessageLoop::current()->PostDelayedTask(FROM_HERE,
method_factory_.NewRunnableMethod(&UserScriptIdleScheduler::MaybeRun),
kUserScriptIdleTimeoutMs);
}
void UserScriptIdleScheduler::DidFinishLoad() {
// Ensure that running scripts does not keep any progress UI running.
MessageLoop::current()->PostTask(FROM_HERE,
method_factory_.NewRunnableMethod(&UserScriptIdleScheduler::MaybeRun));
}
void UserScriptIdleScheduler::Cancel() {
view_ = NULL;
frame_ = NULL;
}
void UserScriptIdleScheduler::MaybeRun() {
if (!view_)
return;
DCHECK(frame_);
view_->OnUserScriptIdleTriggered(frame_);
Cancel();
has_run_ = true;
}
// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef CHROME_RENDERER_USER_SCRIPT_IDLE_SCHEDULER_H_
#define CHROME_RENDERER_USER_SCRIPT_IDLE_SCHEDULER_H_
#include "base/task.h"
class RenderView;
namespace WebKit {
class WebFrame;
}
// Implements support for injecting scripts at "document idle". Currently,
// determining idleness is simple: it is whichever of the following happens
// first:
//
// a) When the initial DOM for a page is complete + kUserScriptIdleTimeout,
// b) or when the page has completely loaded including all subresources.
//
// The intent of this mechanism is to prevent user scripts from slowing down
// fast pages (run after load), while still allowing them to run relatively
// timelily for pages with lots of slow subresources.
class UserScriptIdleScheduler {
public:
UserScriptIdleScheduler(RenderView* view, WebKit::WebFrame* frame);
bool has_run() { return has_run_; }
// Called when the DOM has been completely constructed.
void DidFinishDocumentLoad();
// Called when the document has completed loading.
void DidFinishLoad();
// Called when the client has gone away and we should no longer run scripts.
void Cancel();
private:
// Run user scripts, except if they've already run for this frame, or the
// frame has been destroyed.
void MaybeRun();
ScopedRunnableMethodFactory<UserScriptIdleScheduler> method_factory_;
// The RenderView we will call back to when it is time to run scripts.
RenderView* view_;
// The Frame we will run scripts in.
WebKit::WebFrame* frame_;
// Whether we have already run scripts.
bool has_run_;
};
#endif // CHROME_RENDERER_USER_SCRIPT_IDLE_SCHEDULER_H_
......@@ -3,28 +3,24 @@ if (typeof(contentWindow) != 'undefined') {
win = contentWindow;
}
win.onload = function() {
// Do this in an onload handler because I'm not sure if chrome.extension
// is available before then.
chrome.extension.onConnect.addListener(function(port) {
console.log('connected');
port.onMessage.addListener(function(msg) {
console.log('got ' + msg);
if (msg.testPostMessage) {
port.postMessage({success: true});
} else if (msg.testPostMessageFromTab) {
testPostMessageFromTab(port);
} else if (msg.testDisconnect) {
port.disconnect();
} else if (msg.testDisconnectOnClose) {
win.location = "about:blank";
} else if (msg.testPortName) {
port.postMessage({portName:port.name});
}
// Ignore other messages since they are from us.
});
chrome.extension.onConnect.addListener(function(port) {
console.log('connected');
port.onMessage.addListener(function(msg) {
console.log('got ' + msg);
if (msg.testPostMessage) {
port.postMessage({success: true});
} else if (msg.testPostMessageFromTab) {
testPostMessageFromTab(port);
} else if (msg.testDisconnect) {
port.disconnect();
} else if (msg.testDisconnectOnClose) {
win.location = "about:blank";
} else if (msg.testPortName) {
port.postMessage({portName:port.name});
}
// Ignore other messages since they are from us.
});
};
});
// Tests that postMessage to the extension and its response works.
function testPostMessageFromTab(origPort) {
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment