diff options
22 files changed, 6547 insertions, 6 deletions
diff --git a/webkit/glue/devtools/js/Images/segmentChromium.png b/webkit/glue/devtools/js/Images/segmentChromium.png Binary files differnew file mode 100644 index 0000000..607559b --- /dev/null +++ b/webkit/glue/devtools/js/Images/segmentChromium.png diff --git a/webkit/glue/devtools/js/Images/segmentHoverChromium.png b/webkit/glue/devtools/js/Images/segmentHoverChromium.png Binary files differnew file mode 100644 index 0000000..0a743d9 --- /dev/null +++ b/webkit/glue/devtools/js/Images/segmentHoverChromium.png diff --git a/webkit/glue/devtools/js/Images/segmentHoverEndChromium.png b/webkit/glue/devtools/js/Images/segmentHoverEndChromium.png Binary files differnew file mode 100644 index 0000000..cf62072 --- /dev/null +++ b/webkit/glue/devtools/js/Images/segmentHoverEndChromium.png diff --git a/webkit/glue/devtools/js/Images/segmentSelectedChromium.png b/webkit/glue/devtools/js/Images/segmentSelectedChromium.png Binary files differnew file mode 100644 index 0000000..a1f7251 --- /dev/null +++ b/webkit/glue/devtools/js/Images/segmentSelectedChromium.png diff --git a/webkit/glue/devtools/js/Images/segmentSelectedEndChromium.png b/webkit/glue/devtools/js/Images/segmentSelectedEndChromium.png Binary files differnew file mode 100644 index 0000000..07641db --- /dev/null +++ b/webkit/glue/devtools/js/Images/segmentSelectedEndChromium.png diff --git a/webkit/glue/devtools/js/Images/statusbarBackgroundChromium.png b/webkit/glue/devtools/js/Images/statusbarBackgroundChromium.png Binary files differnew file mode 100644 index 0000000..9d326ac --- /dev/null +++ b/webkit/glue/devtools/js/Images/statusbarBackgroundChromium.png diff --git a/webkit/glue/devtools/js/Images/statusbarBottomBackgroundChromium.png b/webkit/glue/devtools/js/Images/statusbarBottomBackgroundChromium.png Binary files differnew file mode 100644 index 0000000..7c7db0a --- /dev/null +++ b/webkit/glue/devtools/js/Images/statusbarBottomBackgroundChromium.png diff --git a/webkit/glue/devtools/js/Images/statusbarButtonsChromium.png b/webkit/glue/devtools/js/Images/statusbarButtonsChromium.png Binary files differnew file mode 100644 index 0000000..0c6635d --- /dev/null +++ b/webkit/glue/devtools/js/Images/statusbarButtonsChromium.png diff --git a/webkit/glue/devtools/js/Images/statusbarMenuButtonChromium.png b/webkit/glue/devtools/js/Images/statusbarMenuButtonChromium.png Binary files differnew file mode 100644 index 0000000..bf26684 --- /dev/null +++ b/webkit/glue/devtools/js/Images/statusbarMenuButtonChromium.png diff --git a/webkit/glue/devtools/js/Images/statusbarMenuButtonSelectedChromium.png b/webkit/glue/devtools/js/Images/statusbarMenuButtonSelectedChromium.png Binary files differnew file mode 100644 index 0000000..3c0aeec --- /dev/null +++ b/webkit/glue/devtools/js/Images/statusbarMenuButtonSelectedChromium.png diff --git a/webkit/glue/devtools/js/debugger_agent.js b/webkit/glue/devtools/js/debugger_agent.js new file mode 100644 index 0000000..51a1d40 --- /dev/null +++ b/webkit/glue/devtools/js/debugger_agent.js @@ -0,0 +1,1527 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview Provides communication interface to remote v8 debugger. See + * protocol decription at http://code.google.com/p/v8/wiki/DebuggerProtocol + */ + +/** + * @constructor + */ +devtools.DebuggerAgent = function() +{ + RemoteDebuggerAgent.debuggerOutput = this.handleDebuggerOutput_.bind(this); + RemoteDebuggerAgent.setContextId = this.setContextId_.bind(this); + + /** + * Id of the inspected page global context. It is used for filtering scripts. + * @type {number} + */ + this.contextId_ = null; + + /** + * Mapping from script id to script info. + * @type {Object} + */ + this.parsedScripts_ = null; + + /** + * Mapping from the request id to the devtools.BreakpointInfo for the + * breakpoints whose v8 ids are not set yet. These breakpoints are waiting for + * "setbreakpoint" responses to learn their ids in the v8 debugger. + * @see #handleSetBreakpointResponse_ + * @type {Object} + */ + this.requestNumberToBreakpointInfo_ = null; + + /** + * Information on current stack frames. + * @type {Array.<devtools.CallFrame>} + */ + this.callFrames_ = []; + + /** + * Whether to stop in the debugger on the exceptions. + * @type {boolean} + */ + this.pauseOnExceptions_ = false; + + /** + * Mapping: request sequence number->callback. + * @type {Object} + */ + this.requestSeqToCallback_ = null; + + /** + * Whether the scripts panel has been shown and initialilzed. + * @type {boolean} + */ + this.scriptsPanelInitialized_ = false; + + /** + * Whether the scripts list should be requested next time when context id is + * set. + * @type {boolean} + */ + this.requestScriptsWhenContextIdSet_ = false; + + /** + * Whether the agent is waiting for initial scripts response. + * @type {boolean} + */ + this.waitingForInitialScriptsResponse_ = false; + + /** + * If backtrace response is received when initial scripts response + * is not yet processed the backtrace handling will be postponed until + * after the scripts response processing. The handler bound to its arguments + * and this agent will be stored in this field then. + * @type {?function()} + */ + this.pendingBacktraceResponseHandler_ = null; + + /** + * Container of all breakpoints set using resource URL. These breakpoints + * survive page reload. Breakpoints set by script id(for scripts that don't + * have URLs) are stored in ScriptInfo objects. + * @type {Object} + */ + this.urlToBreakpoints_ = {}; + + + /** + * Exception message that is shown to user while on exception break. + * @type {WebInspector.ConsoleMessage} + */ + this.currentExceptionMessage_ = null; +}; + + +/** + * A copy of the scope types from v8/src/mirror-delay.js + * @enum {number} + */ +devtools.DebuggerAgent.ScopeType = { + Global: 0, + Local: 1, + With: 2, + Closure: 3, + Catch: 4 +}; + + +/** + * Resets debugger agent to its initial state. + */ +devtools.DebuggerAgent.prototype.reset = function() +{ + this.contextId_ = null; + // No need to request scripts since they all will be pushed in AfterCompile + // events. + this.requestScriptsWhenContextIdSet_ = false; + this.waitingForInitialScriptsResponse_ = false; + + this.parsedScripts_ = {}; + this.requestNumberToBreakpointInfo_ = {}; + this.callFrames_ = []; + this.requestSeqToCallback_ = {}; +}; + + +/** + * Initializes scripts UI. This method is called every time Scripts panel + * is shown. It will send request for context id if it's not set yet. + */ +devtools.DebuggerAgent.prototype.initUI = function() +{ + // Initialize scripts cache when Scripts panel is shown first time. + if (this.scriptsPanelInitialized_) + return; + this.scriptsPanelInitialized_ = true; + if (this.contextId_) { + // We already have context id. This means that we are here from the + // very beginning of the page load cycle and hence will get all scripts + // via after-compile events. No need to request scripts for this session. + // + // There can be a number of scripts from after-compile events that are + // pending addition into the UI. + for (var scriptId in this.parsedScripts_) { + var script = this.parsedScripts_[scriptId]; + WebInspector.parsedScriptSource(scriptId, script.getUrl(), undefined /* script source */, script.getLineOffset()); + } + return; + } + this.waitingForInitialScriptsResponse_ = true; + // Script list should be requested only when current context id is known. + RemoteDebuggerAgent.getContextId(); + this.requestScriptsWhenContextIdSet_ = true; +}; + + +/** + * Asynchronously requests the debugger for the script source. + * @param {number} scriptId Id of the script whose source should be resolved. + * @param {function(source:?string):void} callback Function that will be called + * when the source resolution is completed. "source" parameter will be null + * if the resolution fails. + */ +devtools.DebuggerAgent.prototype.resolveScriptSource = function(scriptId, callback) +{ + var script = this.parsedScripts_[scriptId]; + if (!script || script.isUnresolved()) { + callback(null); + return; + } + + var cmd = new devtools.DebugCommand("scripts", { + "ids": [scriptId], + "includeSource": true + }); + devtools.DebuggerAgent.sendCommand_(cmd); + // Force v8 execution so that it gets to processing the requested command. + RemoteToolsAgent.executeVoidJavaScript(); + + this.requestSeqToCallback_[cmd.getSequenceNumber()] = function(msg) { + if (msg.isSuccess()) { + var scriptJson = msg.getBody()[0]; + if (scriptJson) + callback(scriptJson.source); + else + callback(null); + } else + callback(null); + }; +}; + + +/** + * Tells the v8 debugger to stop on as soon as possible. + */ +devtools.DebuggerAgent.prototype.pauseExecution = function() +{ + RemoteDebuggerCommandExecutor.DebuggerPauseScript(); +}; + + +/** + * @param {number} sourceId Id of the script fot the breakpoint. + * @param {number} line Number of the line for the breakpoint. + * @param {?string} condition The breakpoint condition. + */ +devtools.DebuggerAgent.prototype.addBreakpoint = function(sourceId, line, condition) +{ + var script = this.parsedScripts_[sourceId]; + if (!script) + return; + + line = devtools.DebuggerAgent.webkitToV8LineNumber_(line); + + var commandArguments; + if (script.getUrl()) { + var breakpoints = this.urlToBreakpoints_[script.getUrl()]; + if (breakpoints && breakpoints[line]) + return; + if (!breakpoints) { + breakpoints = {}; + this.urlToBreakpoints_[script.getUrl()] = breakpoints; + } + + var breakpointInfo = new devtools.BreakpointInfo(line); + breakpoints[line] = breakpointInfo; + + commandArguments = { + "groupId": this.contextId_, + "type": "script", + "target": script.getUrl(), + "line": line, + "condition": condition + }; + } else { + var breakpointInfo = script.getBreakpointInfo(line); + if (breakpointInfo) + return; + + breakpointInfo = new devtools.BreakpointInfo(line); + script.addBreakpointInfo(breakpointInfo); + + commandArguments = { + "groupId": this.contextId_, + "type": "scriptId", + "target": sourceId, + "line": line, + "condition": condition + }; + } + + var cmd = new devtools.DebugCommand("setbreakpoint", commandArguments); + + this.requestNumberToBreakpointInfo_[cmd.getSequenceNumber()] = breakpointInfo; + + devtools.DebuggerAgent.sendCommand_(cmd); + // Force v8 execution so that it gets to processing the requested command. + // It is necessary for being able to change a breakpoint just after it + // has been created (since we need an existing breakpoint id for that). + RemoteToolsAgent.executeVoidJavaScript(); +}; + + +/** + * @param {number} sourceId Id of the script for the breakpoint. + * @param {number} line Number of the line for the breakpoint. + */ +devtools.DebuggerAgent.prototype.removeBreakpoint = function(sourceId, line) +{ + var script = this.parsedScripts_[sourceId]; + if (!script) + return; + + line = devtools.DebuggerAgent.webkitToV8LineNumber_(line); + + var breakpointInfo; + if (script.getUrl()) { + var breakpoints = this.urlToBreakpoints_[script.getUrl()]; + breakpointInfo = breakpoints[line]; + delete breakpoints[line]; + } else { + breakpointInfo = script.getBreakpointInfo(line); + if (breakpointInfo) + script.removeBreakpointInfo(breakpointInfo); + } + + if (!breakpointInfo) + return; + + breakpointInfo.markAsRemoved(); + + var id = breakpointInfo.getV8Id(); + + // If we don't know id of this breakpoint in the v8 debugger we cannot send + // "clearbreakpoint" request. In that case it will be removed in + // "setbreakpoint" response handler when we learn the id. + if (id !== -1) { + this.requestClearBreakpoint_(id); + } +}; + + +/** + * @param {number} sourceId Id of the script for the breakpoint. + * @param {number} line Number of the line for the breakpoint. + * @param {?string} condition New breakpoint condition. + */ +devtools.DebuggerAgent.prototype.updateBreakpoint = function(sourceId, line, condition) +{ + var script = this.parsedScripts_[sourceId]; + if (!script) + return; + + line = devtools.DebuggerAgent.webkitToV8LineNumber_(line); + + var breakpointInfo; + if (script.getUrl()) { + var breakpoints = this.urlToBreakpoints_[script.getUrl()]; + breakpointInfo = breakpoints[line]; + } else + breakpointInfo = script.getBreakpointInfo(line); + + var id = breakpointInfo.getV8Id(); + + // If we don't know id of this breakpoint in the v8 debugger we cannot send + // the "changebreakpoint" request. + if (id !== -1) { + // TODO(apavlov): make use of the real values for "enabled" and + // "ignoreCount" when appropriate. + this.requestChangeBreakpoint_(id, true, condition, null); + } +}; + + +/** + * Tells the v8 debugger to step into the next statement. + */ +devtools.DebuggerAgent.prototype.stepIntoStatement = function() +{ + this.stepCommand_("in"); +}; + + +/** + * Tells the v8 debugger to step out of current function. + */ +devtools.DebuggerAgent.prototype.stepOutOfFunction = function() +{ + this.stepCommand_("out"); +}; + + +/** + * Tells the v8 debugger to step over the next statement. + */ +devtools.DebuggerAgent.prototype.stepOverStatement = function() +{ + this.stepCommand_("next"); +}; + + +/** + * Tells the v8 debugger to continue execution after it has been stopped on a + * breakpoint or an exception. + */ +devtools.DebuggerAgent.prototype.resumeExecution = function() +{ + this.clearExceptionMessage_(); + var cmd = new devtools.DebugCommand("continue"); + devtools.DebuggerAgent.sendCommand_(cmd); +}; + + +/** + * Creates exception message and schedules it for addition to the resource upon + * backtrace availability. + * @param {string} url Resource url. + * @param {number} line Resource line number. + * @param {string} message Exception text. + */ +devtools.DebuggerAgent.prototype.createExceptionMessage_ = function(url, line, message) +{ + this.currentExceptionMessage_ = new WebInspector.ConsoleMessage( + WebInspector.ConsoleMessage.MessageSource.JS, + WebInspector.ConsoleMessage.MessageType.Log, + WebInspector.ConsoleMessage.MessageLevel.Error, + line, + url, + 0 /* group level */, + 1 /* repeat count */, + "[Exception] " + message); +}; + + +/** + * Shows pending exception message that is created with createExceptionMessage_ + * earlier. + */ +devtools.DebuggerAgent.prototype.showPendingExceptionMessage_ = function() +{ + if (!this.currentExceptionMessage_) + return; + var msg = this.currentExceptionMessage_; + var resource = WebInspector.resourceURLMap[msg.url]; + if (resource) { + msg.resource = resource; + WebInspector.panels.resources.addMessageToResource(resource, msg); + } else + this.currentExceptionMessage_ = null; +}; + + +/** + * Clears exception message from the resource. + */ +devtools.DebuggerAgent.prototype.clearExceptionMessage_ = function() +{ + if (this.currentExceptionMessage_) { + var messageElement = this.currentExceptionMessage_._resourceMessageLineElement; + var bubble = messageElement.parentElement; + bubble.removeChild(messageElement); + if (!bubble.firstChild) { + // Last message in bubble removed. + bubble.parentElement.removeChild(bubble); + } + this.currentExceptionMessage_ = null; + } +}; + + +/** + * @return {boolean} True iff the debugger will pause execution on the + * exceptions. + */ +devtools.DebuggerAgent.prototype.pauseOnExceptions = function() +{ + return this.pauseOnExceptions_; +}; + + +/** + * Tells whether to pause in the debugger on the exceptions or not. + * @param {boolean} value True iff execution should be stopped in the debugger + * on the exceptions. + */ +devtools.DebuggerAgent.prototype.setPauseOnExceptions = function(value) +{ + this.pauseOnExceptions_ = value; +}; + + +/** + * Sends "evaluate" request to the debugger. + * @param {Object} arguments Request arguments map. + * @param {function(devtools.DebuggerMessage)} callback Callback to be called + * when response is received. + */ +devtools.DebuggerAgent.prototype.requestEvaluate = function(arguments, callback) +{ + var cmd = new devtools.DebugCommand("evaluate", arguments); + devtools.DebuggerAgent.sendCommand_(cmd); + this.requestSeqToCallback_[cmd.getSequenceNumber()] = callback; +}; + + +/** + * Sends "lookup" request for each unresolved property of the object. When + * response is received the properties will be changed with their resolved + * values. + * @param {Object} object Object whose properties should be resolved. + * @param {function(devtools.DebuggerMessage)} Callback to be called when all + * children are resolved. + * @param {boolean} noIntrinsic Whether intrinsic properties should be included. + */ +devtools.DebuggerAgent.prototype.resolveChildren = function(object, callback, noIntrinsic) +{ + if ("handle" in object) { + var result = []; + devtools.DebuggerAgent.formatObjectProperties_(object, result, noIntrinsic); + callback(result); + } else { + this.requestLookup_([object.ref], function(msg) { + var result = []; + if (msg.isSuccess()) { + var handleToObject = msg.getBody(); + var resolved = handleToObject[object.ref]; + devtools.DebuggerAgent.formatObjectProperties_(resolved, result, noIntrinsic); + callback(result); + } else + callback([]); + }); + } +}; + + +/** + * Sends "scope" request for the scope object to resolve its variables. + * @param {Object} scope Scope to be resolved. + * @param {function(Array.<WebInspector.ObjectPropertyProxy>)} callback + * Callback to be called when all scope variables are resolved. + */ +devtools.DebuggerAgent.prototype.resolveScope = function(scope, callback) +{ + var cmd = new devtools.DebugCommand("scope", { + "frameNumber": scope.frameNumber, + "number": scope.index, + "compactFormat": true + }); + devtools.DebuggerAgent.sendCommand_(cmd); + this.requestSeqToCallback_[cmd.getSequenceNumber()] = function(msg) { + var result = []; + if (msg.isSuccess()) { + var scopeObjectJson = msg.getBody().object; + devtools.DebuggerAgent.formatObjectProperties_(scopeObjectJson, result, true /* no intrinsic */); + } + callback(result); + }; +}; + + +/** + * Sends "scopes" request for the frame object to resolve all variables + * available in the frame. + * @param {number} callFrameId Id of call frame whose variables need to + * be resolved. + * @param {function(Object)} callback Callback to be called when all frame + * variables are resolved. + */ +devtools.DebuggerAgent.prototype.resolveFrameVariables_ = function(callFrameId, callback) +{ + var result = {}; + + var frame = this.callFrames_[callFrameId]; + if (!frame) { + callback(result); + return; + } + + var waitingResponses = 0; + function scopeResponseHandler(msg) { + waitingResponses--; + + if (msg.isSuccess()) { + var properties = msg.getBody().object.properties; + for (var j = 0; j < properties.length; j++) + result[properties[j].name] = true; + } + + // When all scopes are resolved invoke the callback. + if (waitingResponses === 0) + callback(result); + }; + + for (var i = 0; i < frame.scopeChain.length; i++) { + var scope = frame.scopeChain[i].objectId; + if (scope.type === devtools.DebuggerAgent.ScopeType.Global) { + // Do not resolve global scope since it takes for too long. + // TODO(yurys): allow to send only property names in the response. + continue; + } + var cmd = new devtools.DebugCommand("scope", { + "frameNumber": scope.frameNumber, + "number": scope.index, + "compactFormat": true + }); + devtools.DebuggerAgent.sendCommand_(cmd); + this.requestSeqToCallback_[cmd.getSequenceNumber()] = scopeResponseHandler; + waitingResponses++; + } +}; + +/** + * Evaluates the expressionString to an object in the call frame and reports + * all its properties. + * @param{string} expressionString Expression whose properties should be + * collected. + * @param{number} callFrameId The frame id. + * @param{function(Object result,bool isException)} reportCompletions Callback + * function. + */ +devtools.DebuggerAgent.prototype.resolveCompletionsOnFrame = function(expressionString, callFrameId, reportCompletions) +{ + if (expressionString) { + expressionString = "var obj = " + expressionString + + "; var names = {}; for (var n in obj) { names[n] = true; };" + + "names;"; + this.evaluateInCallFrame( + callFrameId, + expressionString, + function(result) { + var names = {}; + if (!result.isException) { + var props = result.value.objectId.properties; + // Put all object properties into the map. + for (var i = 0; i < props.length; i++) + names[props[i].name] = true; + } + reportCompletions(names, result.isException); + }); + } else { + this.resolveFrameVariables_(callFrameId, + function(result) { + reportCompletions(result, false /* isException */); + }); + } +}; + + +/** + * @param{number} scriptId + * @return {string} Type of the context of the script with specified id. + */ +devtools.DebuggerAgent.prototype.getScriptContextType = function(scriptId) +{ + return this.parsedScripts_[scriptId].getContextType(); +}; + + +/** + * Removes specified breakpoint from the v8 debugger. + * @param {number} breakpointId Id of the breakpoint in the v8 debugger. + */ +devtools.DebuggerAgent.prototype.requestClearBreakpoint_ = function(breakpointId) +{ + var cmd = new devtools.DebugCommand("clearbreakpoint", { + "breakpoint": breakpointId + }); + devtools.DebuggerAgent.sendCommand_(cmd); +}; + + +/** + * Changes breakpoint parameters in the v8 debugger. + * @param {number} breakpointId Id of the breakpoint in the v8 debugger. + * @param {boolean} enabled Whether to enable the breakpoint. + * @param {?string} condition New breakpoint condition. + * @param {number} ignoreCount New ignore count for the breakpoint. + */ +devtools.DebuggerAgent.prototype.requestChangeBreakpoint_ = function(breakpointId, enabled, condition, ignoreCount) +{ + var cmd = new devtools.DebugCommand("changebreakpoint", { + "breakpoint": breakpointId, + "enabled": enabled, + "condition": condition, + "ignoreCount": ignoreCount + }); + devtools.DebuggerAgent.sendCommand_(cmd); +}; + + +/** + * Sends "backtrace" request to v8. + */ +devtools.DebuggerAgent.prototype.requestBacktrace_ = function() +{ + var cmd = new devtools.DebugCommand("backtrace", { + "compactFormat":true + }); + devtools.DebuggerAgent.sendCommand_(cmd); +}; + + +/** + * Sends command to v8 debugger. + * @param {devtools.DebugCommand} cmd Command to execute. + */ +devtools.DebuggerAgent.sendCommand_ = function(cmd) +{ + RemoteDebuggerCommandExecutor.DebuggerCommand(cmd.toJSONProtocol()); +}; + + +/** + * Tells the v8 debugger to make the next execution step. + * @param {string} action "in", "out" or "next" action. + */ +devtools.DebuggerAgent.prototype.stepCommand_ = function(action) +{ + this.clearExceptionMessage_(); + var cmd = new devtools.DebugCommand("continue", { + "stepaction": action, + "stepcount": 1 + }); + devtools.DebuggerAgent.sendCommand_(cmd); +}; + + +/** + * Sends "lookup" request to v8. + * @param {number} handle Handle to the object to lookup. + */ +devtools.DebuggerAgent.prototype.requestLookup_ = function(handles, callback) +{ + var cmd = new devtools.DebugCommand("lookup", { + "compactFormat":true, + "handles": handles + }); + devtools.DebuggerAgent.sendCommand_(cmd); + this.requestSeqToCallback_[cmd.getSequenceNumber()] = callback; +}; + + +/** + * Sets debugger context id for scripts filtering. + * @param {number} contextId Id of the inspected page global context. + */ +devtools.DebuggerAgent.prototype.setContextId_ = function(contextId) +{ + this.contextId_ = contextId; + + // If it's the first time context id is set request scripts list. + if (this.requestScriptsWhenContextIdSet_) { + this.requestScriptsWhenContextIdSet_ = false; + var cmd = new devtools.DebugCommand("scripts", { + "includeSource": false + }); + devtools.DebuggerAgent.sendCommand_(cmd); + // Force v8 execution so that it gets to processing the requested command. + RemoteToolsAgent.executeVoidJavaScript(); + + var debuggerAgent = this; + this.requestSeqToCallback_[cmd.getSequenceNumber()] = function(msg) { + // Handle the response iff the context id hasn't changed since the request + // was issued. Otherwise if the context id did change all up-to-date + // scripts will be pushed in after compile events and there is no need to + // handle the response. + if (contextId === debuggerAgent.contextId_) + debuggerAgent.handleScriptsResponse_(msg); + + // We received initial scripts response so flush the flag and + // see if there is an unhandled backtrace response. + debuggerAgent.waitingForInitialScriptsResponse_ = false; + if (debuggerAgent.pendingBacktraceResponseHandler_) { + debuggerAgent.pendingBacktraceResponseHandler_(); + debuggerAgent.pendingBacktraceResponseHandler_ = null; + } + }; + } +}; + + +/** + * Handles output sent by v8 debugger. The output is either asynchronous event + * or response to a previously sent request. See protocol definitioun for more + * details on the output format. + * @param {string} output + */ +devtools.DebuggerAgent.prototype.handleDebuggerOutput_ = function(output) +{ + var msg; + try { + msg = new devtools.DebuggerMessage(output); + } catch(e) { + debugPrint("Failed to handle debugger response:\n" + e); + throw e; + } + + if (msg.getType() === "event") { + if (msg.getEvent() === "break") + this.handleBreakEvent_(msg); + else if (msg.getEvent() === "exception") + this.handleExceptionEvent_(msg); + else if (msg.getEvent() === "afterCompile") + this.handleAfterCompileEvent_(msg); + } else if (msg.getType() === "response") { + if (msg.getCommand() === "scripts") + this.invokeCallbackForResponse_(msg); + else if (msg.getCommand() === "setbreakpoint") + this.handleSetBreakpointResponse_(msg); + else if (msg.getCommand() === "clearbreakpoint") + this.handleClearBreakpointResponse_(msg); + else if (msg.getCommand() === "backtrace") + this.handleBacktraceResponse_(msg); + else if (msg.getCommand() === "lookup") + this.invokeCallbackForResponse_(msg); + else if (msg.getCommand() === "evaluate") + this.invokeCallbackForResponse_(msg); + else if (msg.getCommand() === "scope") + this.invokeCallbackForResponse_(msg); + } +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleBreakEvent_ = function(msg) +{ + // Force scrips panel to be shown first. + WebInspector.currentPanel = WebInspector.panels.scripts; + + var body = msg.getBody(); + + var line = devtools.DebuggerAgent.v8ToWwebkitLineNumber_(body.sourceLine); + this.requestBacktrace_(); +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleExceptionEvent_ = function(msg) +{ + // Force scrips panel to be shown first. + WebInspector.currentPanel = WebInspector.panels.scripts; + + var body = msg.getBody(); + // No script field in the body means that v8 failed to parse the script. We + // resume execution on parser errors automatically. + if (this.pauseOnExceptions_ && body.script) { + var line = devtools.DebuggerAgent.v8ToWwebkitLineNumber_(body.sourceLine); + this.createExceptionMessage_(body.script.name, line, body.exception.text); + this.requestBacktrace_(); + } else + this.resumeExecution(); +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleScriptsResponse_ = function(msg) +{ + var scripts = msg.getBody(); + for (var i = 0; i < scripts.length; i++) { + var script = scripts[i]; + + // Skip scripts from other tabs. + if (!this.isScriptFromInspectedContext_(script, msg)) + continue; + + // We may already have received the info in an afterCompile event. + if (script.id in this.parsedScripts_) + continue; + this.addScriptInfo_(script, msg); + } +}; + + +/** + * @param {Object} script Json object representing script. + * @param {devtools.DebuggerMessage} msg Debugger response. + */ +devtools.DebuggerAgent.prototype.isScriptFromInspectedContext_ = function(script, msg) +{ + if (!script.context) { + // Always ignore scripts from the utility context. + return false; + } + var context = msg.lookup(script.context.ref); + var scriptContextId = context.data; + if (typeof scriptContextId === "undefined") + return false; // Always ignore scripts from the utility context. + if (this.contextId_ === null) + return true; + // Find the id from context data. The context data has the format "type,id". + var comma = context.data.indexOf(","); + if (comma < 0) + return false; + return (context.data.substring(comma + 1) == this.contextId_); +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleSetBreakpointResponse_ = function(msg) +{ + var requestSeq = msg.getRequestSeq(); + var breakpointInfo = this.requestNumberToBreakpointInfo_[requestSeq]; + if (!breakpointInfo) { + // TODO(yurys): handle this case + return; + } + delete this.requestNumberToBreakpointInfo_[requestSeq]; + if (!msg.isSuccess()) { + // TODO(yurys): handle this case + return; + } + var idInV8 = msg.getBody().breakpoint; + breakpointInfo.setV8Id(idInV8); + + if (breakpointInfo.isRemoved()) + this.requestClearBreakpoint_(idInV8); +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleAfterCompileEvent_ = function(msg) +{ + if (!this.contextId_) { + // Ignore scripts delta if main request has not been issued yet. + return; + } + var script = msg.getBody().script; + + // Ignore scripts from other tabs. + if (!this.isScriptFromInspectedContext_(script, msg)) + return; + this.addScriptInfo_(script, msg); +}; + + +/** + * Adds the script info to the local cache. This method assumes that the script + * is not in the cache yet. + * @param {Object} script Script json object from the debugger message. + * @param {devtools.DebuggerMessage} msg Debugger message containing the script + * data. + */ +devtools.DebuggerAgent.prototype.addScriptInfo_ = function(script, msg) +{ + var context = msg.lookup(script.context.ref); + var contextType; + // Find the type from context data. The context data has the format + // "type,id". + var comma = context.data.indexOf(","); + if (comma < 0) + return + contextType = context.data.substring(0, comma); + this.parsedScripts_[script.id] = new devtools.ScriptInfo(script.id, script.name, script.lineOffset, contextType); + if (this.scriptsPanelInitialized_) { + // Only report script as parsed after scripts panel has been shown. + WebInspector.parsedScriptSource(script.id, script.name, script.source, script.lineOffset); + } +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleClearBreakpointResponse_ = function(msg) +{ + // Do nothing. +}; + + +/** + * Handles response to "backtrace" command. + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.handleBacktraceResponse_ = function(msg) +{ + if (this.waitingForInitialScriptsResponse_) + this.pendingBacktraceResponseHandler_ = this.doHandleBacktraceResponse_.bind(this, msg); + else + this.doHandleBacktraceResponse_(msg); +}; + + +/** + * @param {devtools.DebuggerMessage} msg + */ +devtools.DebuggerAgent.prototype.doHandleBacktraceResponse_ = function(msg) +{ + var frames = msg.getBody().frames; + this.callFrames_ = []; + for (var i = 0; i < frames.length; ++i) + this.callFrames_.push(this.formatCallFrame_(frames[i])); + WebInspector.pausedScript(this.callFrames_); + this.showPendingExceptionMessage_(); + InspectorFrontendHost.activateWindow(); +}; + + +/** + * Evaluates code on given callframe. + */ +devtools.DebuggerAgent.prototype.evaluateInCallFrame = function(callFrameId, code, callback) +{ + var callFrame = this.callFrames_[callFrameId]; + callFrame.evaluate_(code, callback); +}; + + +/** + * Handles response to a command by invoking its callback (if any). + * @param {devtools.DebuggerMessage} msg + * @return {boolean} Whether a callback for the given message was found and + * excuted. + */ +devtools.DebuggerAgent.prototype.invokeCallbackForResponse_ = function(msg) +{ + var callback = this.requestSeqToCallback_[msg.getRequestSeq()]; + if (!callback) { + // It may happend if reset was called. + return false; + } + delete this.requestSeqToCallback_[msg.getRequestSeq()]; + callback(msg); + return true; +}; + + +/** + * @param {Object} stackFrame Frame json object from "backtrace" response. + * @return {!devtools.CallFrame} Object containing information related to the + * call frame in the format expected by ScriptsPanel and its panes. + */ +devtools.DebuggerAgent.prototype.formatCallFrame_ = function(stackFrame) +{ + var func = stackFrame.func; + var sourceId = func.scriptId; + + // Add service script if it does not exist. + var existingScript = this.parsedScripts_[sourceId]; + if (!existingScript) { + this.parsedScripts_[sourceId] = new devtools.ScriptInfo(sourceId, null /* name */, 0 /* line */, "unknown" /* type */, true /* unresolved */); + WebInspector.parsedScriptSource(sourceId, null, null, 0); + } + + var funcName = func.name || func.inferredName || "(anonymous function)"; + var line = devtools.DebuggerAgent.v8ToWwebkitLineNumber_(stackFrame.line); + + // Add basic scope chain info with scope variables. + var scopeChain = []; + var ScopeType = devtools.DebuggerAgent.ScopeType; + for (var i = 0; i < stackFrame.scopes.length; i++) { + var scope = stackFrame.scopes[i]; + scope.frameNumber = stackFrame.index; + var scopeObjectProxy = new WebInspector.ObjectProxy(0, scope, [], 0, "", true); + scopeObjectProxy.isScope = true; + switch(scope.type) { + case ScopeType.Global: + scopeObjectProxy.isDocument = true; + break; + case ScopeType.Local: + scopeObjectProxy.isLocal = true; + scopeObjectProxy.thisObject = devtools.DebuggerAgent.formatObjectProxy_(stackFrame.receiver); + break; + case ScopeType.With: + // Catch scope is treated as a regular with scope by WebKit so we + // also treat it this way. + case ScopeType.Catch: + scopeObjectProxy.isWithBlock = true; + break; + case ScopeType.Closure: + scopeObjectProxy.isClosure = true; + break; + } + scopeChain.push(scopeObjectProxy); + } + return new devtools.CallFrame(stackFrame.index, "function", funcName, sourceId, line, scopeChain); +}; + + +/** + * Collects properties for an object from the debugger response. + * @param {Object} object An object from the debugger protocol response. + * @param {Array.<WebInspector.ObjectPropertyProxy>} result An array to put the + * properties into. + * @param {boolean} noIntrinsic Whether intrinsic properties should be + * included. + */ +devtools.DebuggerAgent.formatObjectProperties_ = function(object, result, noIntrinsic) +{ + devtools.DebuggerAgent.propertiesToProxies_(object.properties, result); + if (noIntrinsic) + return; + + result.push(new WebInspector.ObjectPropertyProxy("__proto__", devtools.DebuggerAgent.formatObjectProxy_(object.protoObject))); + result.push(new WebInspector.ObjectPropertyProxy("constructor", devtools.DebuggerAgent.formatObjectProxy_(object.constructorFunction))); + // Don't add 'prototype' property since it is one of the regualar properties. +}; + + +/** + * For each property in "properties" creates its proxy representative. + * @param {Array.<Object>} properties Receiver properties or locals array from + * "backtrace" response. + * @param {Array.<WebInspector.ObjectPropertyProxy>} Results holder. + */ +devtools.DebuggerAgent.propertiesToProxies_ = function(properties, result) +{ + var map = {}; + for (var i = 0; i < properties.length; ++i) { + var property = properties[i]; + var name = String(property.name); + if (name in map) + continue; + map[name] = true; + var value = devtools.DebuggerAgent.formatObjectProxy_(property.value); + var propertyProxy = new WebInspector.ObjectPropertyProxy(name, value); + result.push(propertyProxy); + } +}; + + +/** + * @param {Object} v An object reference from the debugger response. + * @return {*} The value representation expected by ScriptsPanel. + */ +devtools.DebuggerAgent.formatObjectProxy_ = function(v) +{ + var description; + var hasChildren = false; + if (v.type === "object") { + description = v.className; + hasChildren = true; + } else if (v.type === "function") { + if (v.source) + description = v.source; + else + description = "function " + v.name + "()"; + hasChildren = true; + } else if (v.type === "undefined") + description = "undefined"; + else if (v.type === "null") + description = "null"; + else if (typeof v.value !== "undefined") { + // Check for undefined and null types before checking the value, otherwise + // null/undefined may have blank value. + description = v.value; + } else + description = "<unresolved ref: " + v.ref + ", type: " + v.type + ">"; + + var proxy = new WebInspector.ObjectProxy(0, v, [], 0, description, hasChildren); + proxy.type = v.type; + proxy.isV8Ref = true; + return proxy; +}; + + +/** + * Converts line number from Web Inspector UI(1-based) to v8(0-based). + * @param {number} line Resource line number in Web Inspector UI. + * @return {number} The line number in v8. + */ +devtools.DebuggerAgent.webkitToV8LineNumber_ = function(line) +{ + return line - 1; +}; + + +/** + * Converts line number from v8(0-based) to Web Inspector UI(1-based). + * @param {number} line Resource line number in v8. + * @return {number} The line number in Web Inspector. + */ +devtools.DebuggerAgent.v8ToWwebkitLineNumber_ = function(line) +{ + return line + 1; +}; + + +/** + * @param {number} scriptId Id of the script. + * @param {?string} url Script resource URL if any. + * @param {number} lineOffset First line 0-based offset in the containing + * document. + * @param {string} contextType Type of the script's context: + * "page" - regular script from html page + * "injected" - extension content script + * @param {bool} opt_isUnresolved If true, script will not be resolved. + * @constructor + */ +devtools.ScriptInfo = function(scriptId, url, lineOffset, contextType, opt_isUnresolved) +{ + this.scriptId_ = scriptId; + this.lineOffset_ = lineOffset; + this.contextType_ = contextType; + this.url_ = url; + this.isUnresolved_ = opt_isUnresolved; + + this.lineToBreakpointInfo_ = {}; +}; + + +/** + * @return {number} + */ +devtools.ScriptInfo.prototype.getLineOffset = function() +{ + return this.lineOffset_; +}; + + +/** + * @return {string} + */ +devtools.ScriptInfo.prototype.getContextType = function() +{ + return this.contextType_; +}; + + +/** + * @return {?string} + */ +devtools.ScriptInfo.prototype.getUrl = function() +{ + return this.url_; +}; + + +/** + * @return {?bool} + */ +devtools.ScriptInfo.prototype.isUnresolved = function() +{ + return this.isUnresolved_; +}; + + +/** + * @param {number} line 0-based line number in the script. + * @return {?devtools.BreakpointInfo} Information on a breakpoint at the + * specified line in the script or undefined if there is no breakpoint at + * that line. + */ +devtools.ScriptInfo.prototype.getBreakpointInfo = function(line) +{ + return this.lineToBreakpointInfo_[line]; +}; + + +/** + * Adds breakpoint info to the script. + * @param {devtools.BreakpointInfo} breakpoint + */ +devtools.ScriptInfo.prototype.addBreakpointInfo = function(breakpoint) +{ + this.lineToBreakpointInfo_[breakpoint.getLine()] = breakpoint; +}; + + +/** + * @param {devtools.BreakpointInfo} breakpoint Breakpoint info to be removed. + */ +devtools.ScriptInfo.prototype.removeBreakpointInfo = function(breakpoint) +{ + var line = breakpoint.getLine(); + delete this.lineToBreakpointInfo_[line]; +}; + + + +/** + * @param {number} line Breakpoint 0-based line number in the containing script. + * @constructor + */ +devtools.BreakpointInfo = function(line) +{ + this.line_ = line; + this.v8id_ = -1; + this.removed_ = false; +}; + + +/** + * @return {number} + */ +devtools.BreakpointInfo.prototype.getLine = function(n) +{ + return this.line_; +}; + + +/** + * @return {number} Unique identifier of this breakpoint in the v8 debugger. + */ +devtools.BreakpointInfo.prototype.getV8Id = function(n) +{ + return this.v8id_; +}; + + +/** + * Sets id of this breakpoint in the v8 debugger. + * @param {number} id + */ +devtools.BreakpointInfo.prototype.setV8Id = function(id) +{ + this.v8id_ = id; +}; + + +/** + * Marks this breakpoint as removed from the front-end. + */ +devtools.BreakpointInfo.prototype.markAsRemoved = function() +{ + this.removed_ = true; +}; + + +/** + * @return {boolean} Whether this breakpoint has been removed from the + * front-end. + */ +devtools.BreakpointInfo.prototype.isRemoved = function() +{ + return this.removed_; +}; + + +/** + * Call stack frame data. + * @param {string} id CallFrame id. + * @param {string} type CallFrame type. + * @param {string} functionName CallFrame type. + * @param {string} sourceID Source id. + * @param {number} line Source line. + * @param {Array.<Object>} scopeChain Array of scoped objects. + * @construnctor + */ +devtools.CallFrame = function(id, type, functionName, sourceID, line, scopeChain) +{ + this.id = id; + this.type = type; + this.functionName = functionName; + this.sourceID = sourceID; + this.line = line; + this.scopeChain = scopeChain; +}; + + +/** + * This method issues asynchronous evaluate request, reports result to the + * callback. + * @param {string} expression An expression to be evaluated in the context of + * this call frame. + * @param {function(Object):undefined} callback Callback to report result to. + */ +devtools.CallFrame.prototype.evaluate_ = function(expression, callback) +{ + devtools.tools.getDebuggerAgent().requestEvaluate({ + "expression": expression, + "frame": this.id, + "global": false, + "disable_break": false, + "compactFormat": true + }, + function(response) { + var result = {}; + if (response.isSuccess()) + result.value = devtools.DebuggerAgent.formatObjectProxy_(response.getBody()); + else { + result.value = response.getMessage(); + result.isException = true; + } + callback(result); + }); +}; + + +/** + * JSON based commands sent to v8 debugger. + * @param {string} command Name of the command to execute. + * @param {Object} opt_arguments Command-specific arguments map. + * @constructor + */ +devtools.DebugCommand = function(command, opt_arguments) +{ + this.command_ = command; + this.type_ = "request"; + this.seq_ = ++devtools.DebugCommand.nextSeq_; + if (opt_arguments) + this.arguments_ = opt_arguments; +}; + + +/** + * Next unique number to be used as debugger request sequence number. + * @type {number} + */ +devtools.DebugCommand.nextSeq_ = 1; + + +/** + * @return {number} + */ +devtools.DebugCommand.prototype.getSequenceNumber = function() +{ + return this.seq_; +}; + + +/** + * @return {string} + */ +devtools.DebugCommand.prototype.toJSONProtocol = function() +{ + var json = { + "seq": this.seq_, + "type": this.type_, + "command": this.command_ + } + if (this.arguments_) + json.arguments = this.arguments_; + return JSON.stringify(json); +}; + + +/** + * JSON messages sent from v8 debugger. See protocol definition for more + * details: http://code.google.com/p/v8/wiki/DebuggerProtocol + * @param {string} msg Raw protocol packet as JSON string. + * @constructor + */ +devtools.DebuggerMessage = function(msg) +{ + this.packet_ = JSON.parse(msg); + this.refs_ = []; + if (this.packet_.refs) { + for (var i = 0; i < this.packet_.refs.length; i++) + this.refs_[this.packet_.refs[i].handle] = this.packet_.refs[i]; + } +}; + + +/** + * @return {string} The packet type. + */ +devtools.DebuggerMessage.prototype.getType = function() +{ + return this.packet_.type; +}; + + +/** + * @return {?string} The packet event if the message is an event. + */ +devtools.DebuggerMessage.prototype.getEvent = function() +{ + return this.packet_.event; +}; + + +/** + * @return {?string} The packet command if the message is a response to a + * command. + */ +devtools.DebuggerMessage.prototype.getCommand = function() +{ + return this.packet_.command; +}; + + +/** + * @return {number} The packet request sequence. + */ +devtools.DebuggerMessage.prototype.getRequestSeq = function() +{ + return this.packet_.request_seq; +}; + + +/** + * @return {number} Whether the v8 is running after processing the request. + */ +devtools.DebuggerMessage.prototype.isRunning = function() +{ + return this.packet_.running ? true : false; +}; + + +/** + * @return {boolean} Whether the request succeeded. + */ +devtools.DebuggerMessage.prototype.isSuccess = function() +{ + return this.packet_.success ? true : false; +}; + + +/** + * @return {string} + */ +devtools.DebuggerMessage.prototype.getMessage = function() +{ + return this.packet_.message; +}; + + +/** + * @return {Object} Parsed message body json. + */ +devtools.DebuggerMessage.prototype.getBody = function() +{ + return this.packet_.body; +}; + + +/** + * @param {number} handle Object handle. + * @return {?Object} Returns the object with the handle if it was sent in this + * message(some objects referenced by handles may be missing in the message). + */ +devtools.DebuggerMessage.prototype.lookup = function(handle) +{ + return this.refs_[handle]; +}; diff --git a/webkit/glue/devtools/js/devtools.css b/webkit/glue/devtools/js/devtools.css new file mode 100644 index 0000000..1fa935f --- /dev/null +++ b/webkit/glue/devtools/js/devtools.css @@ -0,0 +1,223 @@ +#scripts-files option.injected { + color: rgb(70, 134, 240); +} + +.data-grid table { + line-height: 120%; +} + +body.attached #toolbar { + height: 34px; + border-top: 1px solid rgb(100, 100, 100); + cursor: default; /* overriden */ + padding-left: 0; +} + + +/* Chrome theme overrides */ +body.platform-windows #toolbar { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(242, 247, 253)), to(rgb(223, 234, 248))); +} + +body.platform-windows.inactive #toolbar { + background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(248, 248, 248)), to(rgb(237, 237, 237))); +} + + +/* Heap Profiler Styles */ + +.heap-snapshot-status-bar-item .glyph { + -webkit-mask-image: url(Images/focusButtonGlyph.png); +} + +.heap-snapshot-sidebar-tree-item .icon { + content: url(Images/profileIcon.png); +} + +.heap-snapshot-sidebar-tree-item.small .icon { + content: url(Images/profileSmallIcon.png); +} + +.heap-snapshot-view { + display: none; + overflow: hidden; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; +} + +.heap-snapshot-view.visible { + display: block; +} + +.heap-snapshot-view .data-grid { + border: none; + max-height: 100%; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 93px; +} + +.heap-snapshot-view .data-grid th.count-column { + text-align: center; +} + +.heap-snapshot-view .data-grid td.count-column { + text-align: right; +} + +.heap-snapshot-view .data-grid th.size-column { + text-align: center; +} + +.heap-snapshot-view .data-grid td.size-column { + text-align: right; +} + +.heap-snapshot-view .data-grid th.countDelta-column { + text-align: center; +} + +.heap-snapshot-view .data-grid td.countDelta-column { + text-align: right; +} + +.heap-snapshot-view .data-grid th.sizeDelta-column { + text-align: center; +} + +.heap-snapshot-view .data-grid td.sizeDelta-column { + text-align: right; +} + +#heap-snapshot-summary-container { + position: absolute; + padding-top: 20px; + bottom: 0; + left: 0; + right: 0; + height: 93px; + margin-left: -1px; + border-left: 1px solid rgb(102, 102, 102); + background-color: rgb(101, 111, 130); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0))); + background-repeat: repeat-x; + background-position: top; + text-align: center; + text-shadow: black 0 1px 1px; + white-space: nowrap; + color: white; + -webkit-background-size: 1px 6px; + -webkit-background-origin: padding; + -webkit-background-clip: padding; +} + +.heap-snapshot-summary { + display: inline-block; + width: 50%; + min-width: 300px; + position: relative; +} + +.heap-snapshot-summary canvas.summary-graph { + width: 225px; +} + +.heap-snapshot-summary-label { + font-size: 12px; + font-weight: bold; + position: absolute; + top: 1px; + width: 50%; + left: 25%; +} + +body.platform-windows .section > .header { + border: 1px solid rgb(92, 116, 157); + background-image: -webkit-gradient(linear, left top, left bottom, from(rgb(105, 133, 180)), to(rgb(92, 116, 157))); +} + +body.platform-windows .console-group-messages .section > .header { + padding: 0 8px 0 0; + background-image: none; + border: none; + min-height: 0; +} + +body.platform-windows #resources-filter { + background: -webkit-gradient(linear, left top, left bottom, from(rgb(233, 233, 233)), to(rgb(233, 233, 233))); +} + +body.platform-windows .crumbs .crumb { + -webkit-border-image: url(Images/segmentChromium.png) 0 12 0 2; +} + +body.platform-windows .crumbs .crumb.end { + -webkit-border-image: url(Images/segmentEndChromium.png) 0 2 0 2; +} + +body.platform-windows .crumbs .crumb.selected { + -webkit-border-image: url(Images/segmentSelectedChromium.png) 0 12 0 2; + color: white; + text-shadow: rgba(255, 255, 255, 0.5) 0 0px 0; +} + +body.platform-windows .crumbs .crumb.selected:hover { + -webkit-border-image: url(Images/segmentSelectedChromium.png) 0 12 0 2; +} + +body.platform-windows .crumbs .crumb.selected.end, .crumbs .crumb.selected.end:hover { + -webkit-border-image: url(Images/segmentSelectedEndChromium.png) 0 2 0 2; +} + +body.platform-windows .crumbs .crumb:hover { + -webkit-border-image: url(Images/segmentHoverChromium.png) 0 12 0 2; +} + +body.platform-windows .crumbs .crumb.dimmed:hover { + -webkit-border-image: url(Images/segmentHoverChromium.png) 0 12 0 2; +} + +body.platform-windows .crumbs .crumb.end:hover { + -webkit-border-image: url(Images/segmentHoverEndChromium.png) 0 2 0 2; +} + +body.platform-windows body.drawer-visible #main-status-bar { + background-image: url(Images/statusbarResizerVertical.png), url(Images/statusbarBackgroundChromium.png); +} + +body.platform-windows .status-bar { + background-image: url(Images/statusbarBackgroundChromium.png); +} + +body.platform-windows button.status-bar-item { + background-image: url(Images/statusbarButtonsChromium.png); +} + +body.platform-windows select.status-bar-item:active { + -webkit-border-image: url(Images/statusbarMenuButtonSelectedChromium.png) 0 17 0 2; +} + +body.platform-windows #drawer { + background-image: url(Images/statusbarBottomBackgroundChromium.png); +} + +body.platform-windows select.status-bar-item { + -webkit-border-image: url(Images/statusbarMenuButtonChromium.png) 0 17 0 2; +} + +.scope-bar li.selected { + -webkit-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.0); +} + +.scope-bar li:active { + -webkit-box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.0); +} + +.timeline-category-tree-item input { + vertical-align: middle; +} diff --git a/webkit/glue/devtools/js/devtools.js b/webkit/glue/devtools/js/devtools.js new file mode 100644 index 0000000..e759f12 --- /dev/null +++ b/webkit/glue/devtools/js/devtools.js @@ -0,0 +1,503 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview Tools is a main class that wires all components of the + * DevTools frontend together. It is also responsible for overriding existing + * WebInspector functionality while it is getting upstreamed into WebCore. + */ + +/** + * Dispatches raw message from the host. + * @param {string} remoteName + * @prama {string} methodName + * @param {string} param1, param2, param3 Arguments to dispatch. + */ +devtools$$dispatch = function(remoteName, methodName, param1, param2, param3) +{ + remoteName = "Remote" + remoteName.substring(0, remoteName.length - 8); + var agent = window[remoteName]; + if (!agent) { + debugPrint("No remote agent '" + remoteName + "' found."); + return; + } + var method = agent[methodName]; + if (!method) { + debugPrint("No method '" + remoteName + "." + methodName + "' found."); + return; + } + method.call(this, param1, param2, param3); +}; + + +devtools.ToolsAgent = function() +{ + RemoteToolsAgent.didDispatchOn = WebInspector.Callback.processCallback; + RemoteToolsAgent.frameNavigate = this.frameNavigate_.bind(this); + RemoteToolsAgent.dispatchOnClient = this.dispatchOnClient_.bind(this); + this.debuggerAgent_ = new devtools.DebuggerAgent(); + this.profilerAgent_ = new devtools.ProfilerAgent(); +}; + + +/** + * Resets tools agent to its initial state. + */ +devtools.ToolsAgent.prototype.reset = function() +{ + this.debuggerAgent_.reset(); +}; + + +/** + * @param {string} script Script exression to be evaluated in the context of the + * inspected page. + * @param {function(Object|string, boolean):undefined} opt_callback Function to + * call with the result. + */ +devtools.ToolsAgent.prototype.evaluateJavaScript = function(script, opt_callback) +{ + InspectorBackend.evaluate(script, opt_callback || function() {}); +}; + + +/** + * @return {devtools.DebuggerAgent} Debugger agent instance. + */ +devtools.ToolsAgent.prototype.getDebuggerAgent = function() +{ + return this.debuggerAgent_; +}; + + +/** + * @return {devtools.ProfilerAgent} Profiler agent instance. + */ +devtools.ToolsAgent.prototype.getProfilerAgent = function() +{ + return this.profilerAgent_; +}; + + +/** + * @param {string} url Url frame navigated to. + * @see tools_agent.h + * @private + */ +devtools.ToolsAgent.prototype.frameNavigate_ = function(url) +{ + this.reset(); + // Do not reset Profiles panel. + var profiles = null; + if ("profiles" in WebInspector.panels) { + profiles = WebInspector.panels["profiles"]; + delete WebInspector.panels["profiles"]; + } + WebInspector.reset(); + if (profiles !== null) + WebInspector.panels["profiles"] = profiles; +}; + + +/** + * @param {string} message Serialized call to be dispatched on WebInspector. + * @private + */ +devtools.ToolsAgent.prototype.dispatchOnClient_ = function(message) +{ + var args = JSON.parse(message); + var methodName = args[0]; + var parameters = args.slice(1); + WebInspector[methodName].apply(WebInspector, parameters); +}; + + +/** + * Evaluates js expression. + * @param {string} expr + */ +devtools.ToolsAgent.prototype.evaluate = function(expr) +{ + RemoteToolsAgent.evaluate(expr); +}; + + +/** + * Enables / disables resources panel in the ui. + * @param {boolean} enabled New panel status. + */ +WebInspector.setResourcesPanelEnabled = function(enabled) +{ + InspectorBackend._resourceTrackingEnabled = enabled; + WebInspector.panels.resources.reset(); +}; + + +/** + * Prints string to the inspector console or shows alert if the console doesn't + * exist. + * @param {string} text + */ +function debugPrint(text) { + var console = WebInspector.console; + if (console) { + console.addMessage(new WebInspector.ConsoleMessage( + WebInspector.ConsoleMessage.MessageSource.JS, + WebInspector.ConsoleMessage.MessageType.Log, + WebInspector.ConsoleMessage.MessageLevel.Log, + 1, "chrome://devtools/<internal>", undefined, -1, text)); + } else + alert(text); +} + + +/** + * Global instance of the tools agent. + * @type {devtools.ToolsAgent} + */ +devtools.tools = null; + + +var context = {}; // Used by WebCore's inspector routines. + +/////////////////////////////////////////////////////////////////////////////// +// Here and below are overrides to existing WebInspector methods only. +// TODO(pfeldman): Patch WebCore and upstream changes. +var oldLoaded = WebInspector.loaded; +WebInspector.loaded = function() +{ + devtools.tools = new devtools.ToolsAgent(); + devtools.tools.reset(); + + Preferences.ignoreWhitespace = false; + Preferences.samplingCPUProfiler = true; + Preferences.heapProfilerPresent = true; + oldLoaded.call(this); + + InspectorFrontendHost.loaded(); +}; + + +(function() +{ + + /** + * Handles an F3 keydown event to focus the Inspector search box. + * @param {KeyboardEvent} event Event to optionally handle + * @return {boolean} whether the event has been handled + */ + function handleF3Keydown(event) { + if (event.keyIdentifier === "F3" && !event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey) { + var searchField = document.getElementById("search"); + searchField.focus(); + searchField.select(); + event.preventDefault(); + return true; + } + return false; + } + + + var oldKeyDown = WebInspector.documentKeyDown; + /** + * This override allows to intercept keydown events we want to handle in a + * custom way. Some nested documents (iframes) delegate keydown handling to + * WebInspector.documentKeyDown (e.g. SourceFrame). + * @param {KeyboardEvent} event + * @override + */ + WebInspector.documentKeyDown = function(event) { + var isHandled = handleF3Keydown(event); + if (!isHandled) { + // Mute refresh action. + if (event.keyIdentifier === "F5") + event.preventDefault(); + else if (event.keyIdentifier === "U+0052" /* "R" */ && (event.ctrlKey || event.metaKey)) + event.preventDefault(); + else + oldKeyDown.call(this, event); + } + }; +})(); + + +/** + * This override is necessary for adding script source asynchronously. + * @override + */ +WebInspector.ScriptView.prototype.setupSourceFrameIfNeeded = function() +{ + if (!this._frameNeedsSetup) + return; + + this.attach(); + + if (this.script.source) + this.didResolveScriptSource_(); + else { + var self = this; + devtools.tools.getDebuggerAgent().resolveScriptSource( + this.script.sourceID, + function(source) { + self.script.source = source || WebInspector.UIString("<source is not available>"); + self.didResolveScriptSource_(); + }); + } +}; + + +/** + * Performs source frame setup when script source is aready resolved. + */ +WebInspector.ScriptView.prototype.didResolveScriptSource_ = function() +{ + this.sourceFrame.setContent("text/javascript", this.script.source); + this._sourceFrameSetup = true; + delete this._frameNeedsSetup; +}; + + +/** + * @param {string} type Type of the the property value("object" or "function"). + * @param {string} className Class name of the property value. + * @constructor + */ +WebInspector.UnresolvedPropertyValue = function(type, className) +{ + this.type = type; + this.className = className; +}; + + +(function() +{ + var oldShow = WebInspector.ScriptsPanel.prototype.show; + WebInspector.ScriptsPanel.prototype.show = function() + { + devtools.tools.getDebuggerAgent().initUI(); + this.enableToggleButton.visible = false; + oldShow.call(this); + }; +})(); + + +(function InterceptProfilesPanelEvents() +{ + var oldShow = WebInspector.ProfilesPanel.prototype.show; + WebInspector.ProfilesPanel.prototype.show = function() + { + devtools.tools.getProfilerAgent().initializeProfiling(); + this.enableToggleButton.visible = false; + oldShow.call(this); + // Show is called on every show event of a panel, so + // we only need to intercept it once. + WebInspector.ProfilesPanel.prototype.show = oldShow; + }; +})(); + + +/* + * @override + * TODO(mnaganov): Restore l10n when it will be agreed that it is needed. + */ +WebInspector.UIString = function(string) +{ + return String.vsprintf(string, Array.prototype.slice.call(arguments, 1)); +}; + + +// There is no clear way of setting frame title yet. So sniffing main resource +// load. +(function OverrideUpdateResource() { + var originalUpdateResource = WebInspector.updateResource; + WebInspector.updateResource = function(identifier, payload) + { + originalUpdateResource.call(this, identifier, payload); + var resource = this.resources[identifier]; + if (resource && resource.mainResource && resource.finished) + document.title = WebInspector.UIString("Developer Tools - %s", resource.url); + }; +})(); + + +// Highlight extension content scripts in the scripts list. +(function () { + var original = WebInspector.ScriptsPanel.prototype._addScriptToFilesMenu; + WebInspector.ScriptsPanel.prototype._addScriptToFilesMenu = function(script) + { + var result = original.apply(this, arguments); + var debuggerAgent = devtools.tools.getDebuggerAgent(); + var type = debuggerAgent.getScriptContextType(script.sourceID); + var option = script.filesSelectOption; + if (type === "injected" && option) + option.addStyleClass("injected"); + return result; + }; +})(); + + +/** Pending WebKit upstream by apavlov). Fixes iframe vs drag problem. */ +(function() +{ + var originalDragStart = WebInspector.elementDragStart; + WebInspector.elementDragStart = function(element) + { + if (element) { + var glassPane = document.createElement("div"); + glassPane.style.cssText = "position:absolute;width:100%;height:100%;opacity:0;z-index:1"; + glassPane.id = "glass-pane-for-drag"; + element.parentElement.appendChild(glassPane); + } + + originalDragStart.apply(this, arguments); + }; + + var originalDragEnd = WebInspector.elementDragEnd; + WebInspector.elementDragEnd = function() + { + originalDragEnd.apply(this, arguments); + + var glassPane = document.getElementById("glass-pane-for-drag"); + if (glassPane) + glassPane.parentElement.removeChild(glassPane); + }; +})(); + + +(function () { +var orig = InjectedScriptAccess.prototype.getProperties; +InjectedScriptAccess.prototype.getProperties = function(objectProxy, ignoreHasOwnProperty, abbreviate, callback) +{ + if (objectProxy.isScope) + devtools.tools.getDebuggerAgent().resolveScope(objectProxy.objectId, callback); + else if (objectProxy.isV8Ref) + devtools.tools.getDebuggerAgent().resolveChildren(objectProxy.objectId, callback, false); + else + orig.apply(this, arguments); +}; +})(); + + +(function() +{ +InjectedScriptAccess.prototype.evaluateInCallFrame = function(callFrameId, code, objectGroup, callback) +{ + //TODO(pfeldman): remove once 49084 is rolled. + if (!callback) + callback = objectGroup; + devtools.tools.getDebuggerAgent().evaluateInCallFrame(callFrameId, code, callback); +}; +})(); + + +WebInspector.resourceTrackingWasEnabled = function() +{ + InspectorBackend._resourceTrackingEnabled = true; + this.panels.resources.resourceTrackingWasEnabled(); +}; + +WebInspector.resourceTrackingWasDisabled = function() +{ + InspectorBackend._resourceTrackingEnabled = false; + this.panels.resources.resourceTrackingWasDisabled(); +}; + +(function() +{ +var orig = WebInspector.ConsoleMessage.prototype.setMessageBody; +WebInspector.ConsoleMessage.prototype.setMessageBody = function(args) +{ + for (var i = 0; i < args.length; ++i) { + if (typeof args[i] === "string") + args[i] = WebInspector.ObjectProxy.wrapPrimitiveValue(args[i]); + } + orig.call(this, args); +}; +})(); + + +(function() +{ +var orig = InjectedScriptAccess.prototype.getCompletions; +InjectedScriptAccess.prototype.getCompletions = function(expressionString, includeInspectorCommandLineAPI, callFrameId, reportCompletions) +{ + if (typeof callFrameId === "number") + devtools.tools.getDebuggerAgent().resolveCompletionsOnFrame(expressionString, callFrameId, reportCompletions); + else + return orig.apply(this, arguments); +}; +})(); + + +(function() +{ +WebInspector.ElementsPanel.prototype._nodeSearchButtonClicked = function( event) +{ + InspectorBackend.toggleNodeSearch(); + this.nodeSearchButton.toggled = !this.nodeSearchButton.toggled; +}; +})(); + + +// We need to have a place for postponed tasks +// which should be executed when all the messages between agent and frontend +// are processed. + +WebInspector.runAfterPendingDispatchesQueue = []; + +WebInspector.TestController.prototype.runAfterPendingDispatches = function(callback) +{ + WebInspector.runAfterPendingDispatchesQueue.push(callback); +}; + +WebInspector.queuesAreEmpty = function() +{ + var copy = this.runAfterPendingDispatchesQueue.slice(); + this.runAfterPendingDispatchesQueue = []; + for (var i = 0; i < copy.length; ++i) + copy[i].call(this); +}; + +(function() +{ +var originalAddToFrame = InspectorFrontendHost.addResourceSourceToFrame; +InspectorFrontendHost.addResourceSourceToFrame = function(identifier, element) +{ + var resource = WebInspector.resources[identifier]; + if (!resource) + return; + originalAddToFrame.call(this, identifier, resource.mimeType, element); +}; +})(); + +WebInspector.pausedScript = function(callFrames) +{ + this.panels.scripts.debuggerPaused(callFrames); +}; diff --git a/webkit/glue/devtools/js/devtools_host_stub.js b/webkit/glue/devtools/js/devtools_host_stub.js new file mode 100644 index 0000000..49a9555 --- /dev/null +++ b/webkit/glue/devtools/js/devtools_host_stub.js @@ -0,0 +1,308 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview These stubs emulate backend functionality and allows + * DevTools frontend to function as a standalone web app. + */ + +if (!window["RemoteDebuggerAgent"]) { + +/** + * @constructor + */ +RemoteDebuggerAgentStub = function() +{ +}; + + +RemoteDebuggerAgentStub.prototype.getContextId = function() +{ + RemoteDebuggerAgent.setContextId(3); +}; + + +/** + * @constructor + */ +RemoteProfilerAgentStub = function() +{ +}; + + +RemoteProfilerAgentStub.prototype.getActiveProfilerModules = function() +{ + ProfilerStubHelper.GetInstance().getActiveProfilerModules(); +}; + + +RemoteProfilerAgentStub.prototype.getLogLines = function(pos) +{ + ProfilerStubHelper.GetInstance().getLogLines(pos); +}; + + +/** + * @constructor + */ +RemoteToolsAgentStub = function() +{ +}; + + +RemoteToolsAgentStub.prototype.dispatchOnInjectedScript = function() +{ +}; + + +RemoteToolsAgentStub.prototype.dispatchOnInspectorController = function() +{ +}; + + +RemoteToolsAgentStub.prototype.executeVoidJavaScript = function() +{ +}; + + +/** + * @constructor + */ +ProfilerStubHelper = function() +{ + this.activeProfilerModules_ = devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_NONE; + this.heapProfSample_ = 0; + this.log_ = ''; +}; + + +ProfilerStubHelper.GetInstance = function() +{ + if (!ProfilerStubHelper.instance_) + ProfilerStubHelper.instance_ = new ProfilerStubHelper(); + return ProfilerStubHelper.instance_; +}; + + +ProfilerStubHelper.prototype.StopProfiling = function(modules) +{ + this.activeProfilerModules_ &= ~modules; +}; + + +ProfilerStubHelper.prototype.StartProfiling = function(modules) +{ + var profModules = devtools.ProfilerAgent.ProfilerModules; + if (modules & profModules.PROFILER_MODULE_HEAP_SNAPSHOT) { + if (modules & profModules.PROFILER_MODULE_HEAP_STATS) { + this.log_ += + 'heap-sample-begin,"Heap","allocated",' + + (new Date()).getTime() + '\n' + + 'heap-sample-stats,"Heap","allocated",10000,1000\n'; + this.log_ += + 'heap-sample-item,STRING_TYPE,100,1000\n' + + 'heap-sample-item,CODE_TYPE,10,200\n' + + 'heap-sample-item,MAP_TYPE,20,350\n'; + this.log_ += ProfilerStubHelper.HeapSamples[this.heapProfSample_++]; + this.heapProfSample_ %= ProfilerStubHelper.HeapSamples.length; + this.log_ += 'heap-sample-end,"Heap","allocated"\n'; + } + } else { + if (modules & profModules.PROFILER_MODULE_CPU) + this.log_ += ProfilerStubHelper.ProfilerLogBuffer; + this.activeProfilerModules_ |= modules; + } +}; + + +ProfilerStubHelper.prototype.getActiveProfilerModules = function() +{ + var self = this; + setTimeout(function() { + RemoteProfilerAgent.didGetActiveProfilerModules(self.activeProfilerModules_); + }, 100); +}; + + +ProfilerStubHelper.prototype.getLogLines = function(pos) +{ + var profModules = devtools.ProfilerAgent.ProfilerModules; + var logLines = this.log_.substr(pos); + setTimeout(function() { + RemoteProfilerAgent.didGetLogLines(pos + logLines.length, logLines); + }, 100); +}; + + +ProfilerStubHelper.ProfilerLogBuffer = + 'profiler,begin,1\n' + + 'profiler,resume\n' + + 'code-creation,LazyCompile,0x1000,256,"test1 http://aaa.js:1"\n' + + 'code-creation,LazyCompile,0x2000,256,"test2 http://bbb.js:2"\n' + + 'code-creation,LazyCompile,0x3000,256,"test3 http://ccc.js:3"\n' + + 'tick,0x1010,0x0,3\n' + + 'tick,0x2020,0x0,3,0x1010\n' + + 'tick,0x2020,0x0,3,0x1010\n' + + 'tick,0x3010,0x0,3,0x2020, 0x1010\n' + + 'tick,0x2020,0x0,3,0x1010\n' + + 'tick,0x2030,0x0,3,0x2020, 0x1010\n' + + 'tick,0x2020,0x0,3,0x1010\n' + + 'tick,0x1010,0x0,3\n' + + 'profiler,pause\n'; + + +ProfilerStubHelper.HeapSamples = [ + 'heap-js-cons-item,foo,1,100\n' + + 'heap-js-cons-item,bar,20,2000\n' + + 'heap-js-cons-item,Object,5,100\n' + + 'heap-js-ret-item,foo,bar;3\n' + + 'heap-js-ret-item,bar,foo;5\n' + + 'heap-js-ret-item,Object:0x1234,(roots);1\n', + + 'heap-js-cons-item,foo,2000,200000\n' + + 'heap-js-cons-item,bar,10,1000\n' + + 'heap-js-cons-item,Object,6,120\n' + + 'heap-js-ret-item,foo,bar;7,Object:0x1234;10\n' + + 'heap-js-ret-item,bar,foo;10,Object:0x1234;10\n' + + 'heap-js-ret-item,Object:0x1234,(roots);1\n', + + 'heap-js-cons-item,foo,15,1500\n' + + 'heap-js-cons-item,bar,15,1500\n' + + 'heap-js-cons-item,Object,5,100\n' + + 'heap-js-cons-item,Array,3,1000\n' + + 'heap-js-ret-item,foo,bar;3,Array:0x5678;1\n' + + 'heap-js-ret-item,bar,foo;5,Object:0x1234;8,Object:0x5678;2\n' + + 'heap-js-ret-item,Object:0x1234,(roots);1,Object:0x5678;2\n' + + 'heap-js-ret-item,Object:0x5678,(global property);3,Object:0x1234;5\n' + + 'heap-js-ret-item,Array:0x5678,(global property);3,Array:0x5678;2\n', + + 'heap-js-cons-item,bar,20,2000\n' + + 'heap-js-cons-item,Object,6,120\n' + + 'heap-js-ret-item,bar,foo;5,Object:0x1234;1,Object:0x1235;3\n' + + 'heap-js-ret-item,Object:0x1234,(global property);3\n' + + 'heap-js-ret-item,Object:0x1235,(global property);5\n', + + 'heap-js-cons-item,foo,15,1500\n' + + 'heap-js-cons-item,bar,15,1500\n' + + 'heap-js-cons-item,Array,10,1000\n' + + 'heap-js-ret-item,foo,bar;1,Array:0x5678;1\n' + + 'heap-js-ret-item,bar,foo;5\n' + + 'heap-js-ret-item,Array:0x5678,(roots);3\n', + + 'heap-js-cons-item,bar,20,2000\n' + + 'heap-js-cons-item,baz,15,1500\n' + + 'heap-js-ret-item,bar,baz;3\n' + + 'heap-js-ret-item,baz,bar;3\n' +]; + + +/** + * @constructor + */ +RemoteDebuggerCommandExecutorStub = function() +{ +}; + + +RemoteDebuggerCommandExecutorStub.prototype.DebuggerCommand = function(cmd) +{ + if ('{"seq":2,"type":"request","command":"scripts","arguments":{"includeSource":false}}' === cmd) { + var response1 = + '{"seq":5,"request_seq":2,"type":"response","command":"scripts","' + + 'success":true,"body":[{"handle":61,"type":"script","name":"' + + 'http://www/~test/t.js","id":59,"lineOffset":0,"columnOffset":0,' + + '"lineCount":1,"sourceStart":"function fib(n) {","sourceLength":300,' + + '"scriptType":2,"compilationType":0,"context":{"ref":60}}],"refs":[{' + + '"handle":60,"type":"context","data":"page,3"}],"running":false}'; + this.sendResponse_(response1); + } else if ('{"seq":3,"type":"request","command":"scripts","arguments":{"ids":[59],"includeSource":true}}' === cmd) { + this.sendResponse_( + '{"seq":8,"request_seq":3,"type":"response","command":"scripts",' + + '"success":true,"body":[{"handle":1,"type":"script","name":' + + '"http://www/~test/t.js","id":59,"lineOffset":0,"columnOffset":0,' + + '"lineCount":1,"source":"function fib(n) {return n+1;}",' + + '"sourceLength":244,"scriptType":2,"compilationType":0,"context":{' + + '"ref":0}}],"refs":[{"handle":0,"type":"context","data":"page,3}],"' + + '"running":false}'); + } else if (cmd.indexOf('"command":"profile"') !== -1) { + var cmdObj = JSON.parse(cmd); + if (cmdObj.arguments.command === "resume") + ProfilerStubHelper.GetInstance().StartProfiling(parseInt(cmdObj.arguments.modules)); + else if (cmdObj.arguments.command === "pause") + ProfilerStubHelper.GetInstance().StopProfiling(parseInt(cmdObj.arguments.modules)); + else + debugPrint("Unexpected profile command: " + cmdObj.arguments.command); + } else + debugPrint("Unexpected command: " + cmd); +}; + + +RemoteDebuggerCommandExecutorStub.prototype.DebuggerPauseScript = function() +{ +}; + + +RemoteDebuggerCommandExecutorStub.prototype.sendResponse_ = function(response) +{ + setTimeout(function() { + RemoteDebuggerAgent.debuggerOutput(response); + }, 0); +}; + + +DevToolsHostStub = function() +{ + this.isStub = true; +}; +DevToolsHostStub.prototype.__proto__ = WebInspector.InspectorFrontendHostStub.prototype; + + +DevToolsHostStub.prototype.reset = function() +{ +}; + + +DevToolsHostStub.prototype.setting = function() +{ +}; + + +DevToolsHostStub.prototype.setSetting = function() +{ +}; + + +window["RemoteDebuggerAgent"] = new RemoteDebuggerAgentStub(); +window["RemoteDebuggerCommandExecutor"] = new RemoteDebuggerCommandExecutorStub(); +window["RemoteProfilerAgent"] = new RemoteProfilerAgentStub(); +window["RemoteToolsAgent"] = new RemoteToolsAgentStub(); +InspectorFrontendHost = new DevToolsHostStub(); + +} diff --git a/webkit/glue/devtools/js/heap_profiler_panel.js b/webkit/glue/devtools/js/heap_profiler_panel.js new file mode 100644 index 0000000..abbf580 --- /dev/null +++ b/webkit/glue/devtools/js/heap_profiler_panel.js @@ -0,0 +1,966 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview Heap profiler panel implementation. + */ + +WebInspector.ProfilesPanel.prototype.addSnapshot = function(snapshot) { + snapshot.title = WebInspector.UIString("Snapshot %d", snapshot.number); + snapshot.typeId = WebInspector.HeapSnapshotProfileType.TypeId; + + var snapshots = WebInspector.HeapSnapshotProfileType.snapshots; + snapshots.push(snapshot); + + snapshot.listIndex = snapshots.length - 1; + + if (WebInspector.CPUProfile) + this.addProfileHeader(WebInspector.HeapSnapshotProfileType.TypeId, snapshot); + else + this.addProfileHeader(snapshot); + + this.dispatchEventToListeners("snapshot added"); +} + + +WebInspector.HeapSnapshotView = function(parent, profile) +{ + WebInspector.View.call(this); + + this.element.addStyleClass("heap-snapshot-view"); + + this.parent = parent; + this.parent.addEventListener("snapshot added", this._updateBaseOptions, this); + + this.showCountAsPercent = false; + this.showSizeAsPercent = false; + this.showCountDeltaAsPercent = false; + this.showSizeDeltaAsPercent = false; + + this.categories = { + code: new WebInspector.ResourceCategory("code", WebInspector.UIString("Code"), "rgb(255,121,0)"), + data: new WebInspector.ResourceCategory("data", WebInspector.UIString("Objects"), "rgb(47,102,236)") + }; + + var summaryContainer = document.createElement("div"); + summaryContainer.id = "heap-snapshot-summary-container"; + + this.countsSummaryBar = new WebInspector.SummaryBar(this.categories); + this.countsSummaryBar.element.className = "heap-snapshot-summary"; + this.countsSummaryBar.calculator = new WebInspector.HeapSummaryCountCalculator(); + var countsLabel = document.createElement("div"); + countsLabel.className = "heap-snapshot-summary-label"; + countsLabel.textContent = WebInspector.UIString("Count"); + this.countsSummaryBar.element.appendChild(countsLabel); + summaryContainer.appendChild(this.countsSummaryBar.element); + + this.sizesSummaryBar = new WebInspector.SummaryBar(this.categories); + this.sizesSummaryBar.element.className = "heap-snapshot-summary"; + this.sizesSummaryBar.calculator = new WebInspector.HeapSummarySizeCalculator(); + var sizesLabel = document.createElement("label"); + sizesLabel.className = "heap-snapshot-summary-label"; + sizesLabel.textContent = WebInspector.UIString("Size"); + this.sizesSummaryBar.element.appendChild(sizesLabel); + summaryContainer.appendChild(this.sizesSummaryBar.element); + + this.element.appendChild(summaryContainer); + + var columns = { "cons": { title: WebInspector.UIString("Constructor"), disclosure: true, sortable: true }, + "count": { title: WebInspector.UIString("Count"), width: "54px", sortable: true }, + "size": { title: WebInspector.UIString("Size"), width: "72px", sort: "descending", sortable: true }, + "countDelta": { title: WebInspector.UIString("\xb1 Count"), width: "72px", sortable: true }, + "sizeDelta": { title: WebInspector.UIString("\xb1 Size"), width: "72px", sortable: true } }; + + this.dataGrid = new WebInspector.DataGrid(columns); + this.dataGrid.addEventListener("sorting changed", this._sortData, this); + this.dataGrid.element.addEventListener("mousedown", this._mouseDownInDataGrid.bind(this), true); + this.element.appendChild(this.dataGrid.element); + + this.profile = profile; + + this.baseSelectElement = document.createElement("select"); + this.baseSelectElement.className = "status-bar-item"; + this.baseSelectElement.addEventListener("change", this._changeBase.bind(this), false); + this._updateBaseOptions(); + if (this.profile.listIndex > 0) + this.baseSelectElement.selectedIndex = this.profile.listIndex - 1; + else + this.baseSelectElement.selectedIndex = this.profile.listIndex; + this._resetDataGridList(); + + this.percentButton = new WebInspector.StatusBarButton("", "percent-time-status-bar-item status-bar-item"); + this.percentButton.addEventListener("click", this._percentClicked.bind(this), false); + + this.refresh(); + + this._updatePercentButton(); +}; + +WebInspector.HeapSnapshotView.prototype = { + + get statusBarItems() + { + return [this.baseSelectElement, this.percentButton.element]; + }, + + get profile() + { + return this._profile; + }, + + set profile(profile) + { + this._profile = profile; + }, + + show: function(parentElement) + { + WebInspector.View.prototype.show.call(this, parentElement); + this.dataGrid.updateWidths(); + }, + + hide: function() + { + WebInspector.View.prototype.hide.call(this); + this._currentSearchResultIndex = -1; + }, + + resize: function() + { + if (this.dataGrid) + this.dataGrid.updateWidths(); + }, + + refresh: function() + { + this.dataGrid.removeChildren(); + + var children = this.snapshotDataGridList.children; + var count = children.length; + for (var index = 0; index < count; ++index) + this.dataGrid.appendChild(children[index]); + + this._updateSummaryGraph(); + }, + + refreshShowAsPercents: function() + { + this._updatePercentButton(); + this.refreshVisibleData(); + }, + + _deleteSearchMatchedFlags: function(node) + { + delete node._searchMatchedConsColumn; + delete node._searchMatchedCountColumn; + delete node._searchMatchedSizeColumn; + delete node._searchMatchedCountDeltaColumn; + delete node._searchMatchedSizeDeltaColumn; + }, + + searchCanceled: function() + { + if (this._searchResults) { + for (var i = 0; i < this._searchResults.length; ++i) { + var profileNode = this._searchResults[i].profileNode; + this._deleteSearchMatchedFlags(profileNode); + profileNode.refresh(); + } + } + + delete this._searchFinishedCallback; + this._currentSearchResultIndex = -1; + this._searchResults = []; + }, + + performSearch: function(query, finishedCallback) + { + // Call searchCanceled since it will reset everything we need before doing a new search. + this.searchCanceled(); + + query = query.trimWhitespace(); + + if (!query.length) + return; + + this._searchFinishedCallback = finishedCallback; + + var helper = WebInspector.HeapSnapshotView.SearchHelper; + + var operationAndNumber = helper.parseOperationAndNumber(query); + var operation = operationAndNumber[0]; + var queryNumber = operationAndNumber[1]; + + var percentUnits = helper.percents.test(query); + var megaBytesUnits = helper.megaBytes.test(query); + var kiloBytesUnits = helper.kiloBytes.test(query); + var bytesUnits = helper.bytes.test(query); + + var queryNumberBytes = (megaBytesUnits ? (queryNumber * 1024 * 1024) : (kiloBytesUnits ? (queryNumber * 1024) : queryNumber)); + + function matchesQuery(heapSnapshotDataGridNode) + { + WebInspector.HeapSnapshotView.prototype._deleteSearchMatchedFlags(heapSnapshotDataGridNode); + + if (percentUnits) { + heapSnapshotDataGridNode._searchMatchedCountColumn = operation(heapSnapshotDataGridNode.countPercent, queryNumber); + heapSnapshotDataGridNode._searchMatchedSizeColumn = operation(heapSnapshotDataGridNode.sizePercent, queryNumber); + heapSnapshotDataGridNode._searchMatchedCountDeltaColumn = operation(heapSnapshotDataGridNode.countDeltaPercent, queryNumber); + heapSnapshotDataGridNode._searchMatchedSizeDeltaColumn = operation(heapSnapshotDataGridNode.sizeDeltaPercent, queryNumber); + } else if (megaBytesUnits || kiloBytesUnits || bytesUnits) { + heapSnapshotDataGridNode._searchMatchedSizeColumn = operation(heapSnapshotDataGridNode.size, queryNumberBytes); + heapSnapshotDataGridNode._searchMatchedSizeDeltaColumn = operation(heapSnapshotDataGridNode.sizeDelta, queryNumberBytes); + } else { + heapSnapshotDataGridNode._searchMatchedCountColumn = operation(heapSnapshotDataGridNode.count, queryNumber); + heapSnapshotDataGridNode._searchMatchedCountDeltaColumn = operation(heapSnapshotDataGridNode.countDelta, queryNumber); + } + + if (heapSnapshotDataGridNode.constructorName.hasSubstring(query, true)) + heapSnapshotDataGridNode._searchMatchedConsColumn = true; + + if (heapSnapshotDataGridNode._searchMatchedConsColumn || + heapSnapshotDataGridNode._searchMatchedCountColumn || + heapSnapshotDataGridNode._searchMatchedSizeColumn || + heapSnapshotDataGridNode._searchMatchedCountDeltaColumn || + heapSnapshotDataGridNode._searchMatchedSizeDeltaColumn) { + heapSnapshotDataGridNode.refresh(); + return true; + } + + return false; + } + + var current = this.snapshotDataGridList.children[0]; + var depth = 0; + var info = {}; + + // The second and subsequent levels of heap snapshot nodes represent retainers, + // so recursive expansion will be infinite, since a graph is being traversed. + // So default to a recursion cap of 2 levels. + var maxDepth = 2; + + while (current) { + if (matchesQuery(current)) + this._searchResults.push({ profileNode: current }); + current = current.traverseNextNode(false, null, (depth >= maxDepth), info); + depth += info.depthChange; + } + + finishedCallback(this, this._searchResults.length); + }, + + jumpToFirstSearchResult: WebInspector.CPUProfileView.prototype.jumpToFirstSearchResult, + jumpToLastSearchResult: WebInspector.CPUProfileView.prototype.jumpToLastSearchResult, + jumpToNextSearchResult: WebInspector.CPUProfileView.prototype.jumpToNextSearchResult, + jumpToPreviousSearchResult: WebInspector.CPUProfileView.prototype.jumpToPreviousSearchResult, + showingFirstSearchResult: WebInspector.CPUProfileView.prototype.showingFirstSearchResult, + showingLastSearchResult: WebInspector.CPUProfileView.prototype.showingLastSearchResult, + _jumpToSearchResult: WebInspector.CPUProfileView.prototype._jumpToSearchResult, + + refreshVisibleData: function() + { + var child = this.dataGrid.children[0]; + while (child) { + child.refresh(); + child = child.traverseNextNode(false, null, true); + } + this._updateSummaryGraph(); + }, + + _changeBase: function() { + if (this.baseSnapshot === WebInspector.HeapSnapshotProfileType.snapshots[this.baseSelectElement.selectedIndex]) + return; + + this._resetDataGridList(); + this.refresh(); + + if (!this.currentQuery || !this._searchFinishedCallback || !this._searchResults) + return; + + // The current search needs to be performed again. First negate out previous match + // count by calling the search finished callback with a negative number of matches. + // Then perform the search again with the same query and callback. + this._searchFinishedCallback(this, -this._searchResults.length); + this.performSearch(this.currentQuery, this._searchFinishedCallback); + }, + + _createSnapshotDataGridList: function() + { + if (this._snapshotDataGridList) + delete this._snapshotDataGridList; + + this._snapshotDataGridList = new WebInspector.HeapSnapshotDataGridList(this, this.baseSnapshot.entries, this.profile.entries); + return this._snapshotDataGridList; + }, + + _mouseDownInDataGrid: function(event) + { + if (event.detail < 2) + return; + + var cell = event.target.enclosingNodeOrSelfWithNodeName("td"); + if (!cell || (!cell.hasStyleClass("count-column") && !cell.hasStyleClass("size-column") && !cell.hasStyleClass("countDelta-column") && !cell.hasStyleClass("sizeDelta-column"))) + return; + + if (cell.hasStyleClass("count-column")) + this.showCountAsPercent = !this.showCountAsPercent; + else if (cell.hasStyleClass("size-column")) + this.showSizeAsPercent = !this.showSizeAsPercent; + else if (cell.hasStyleClass("countDelta-column")) + this.showCountDeltaAsPercent = !this.showCountDeltaAsPercent; + else if (cell.hasStyleClass("sizeDelta-column")) + this.showSizeDeltaAsPercent = !this.showSizeDeltaAsPercent; + + this.refreshShowAsPercents(); + + event.preventDefault(); + event.stopPropagation(); + }, + + get _isShowingAsPercent() + { + return this.showCountAsPercent && this.showSizeAsPercent && this.showCountDeltaAsPercent && this.showSizeDeltaAsPercent; + }, + + _percentClicked: function(event) + { + var currentState = this._isShowingAsPercent; + this.showCountAsPercent = !currentState; + this.showSizeAsPercent = !currentState; + this.showCountDeltaAsPercent = !currentState; + this.showSizeDeltaAsPercent = !currentState; + this.refreshShowAsPercents(); + }, + + _resetDataGridList: function() + { + this.baseSnapshot = WebInspector.HeapSnapshotProfileType.snapshots[this.baseSelectElement.selectedIndex]; + var lastComparator = WebInspector.HeapSnapshotDataGridList.propertyComparator("size", false); + if (this.snapshotDataGridList) + lastComparator = this.snapshotDataGridList.lastComparator; + this.snapshotDataGridList = this._createSnapshotDataGridList(); + this.snapshotDataGridList.sort(lastComparator, true); + }, + + _sortData: function() + { + var sortAscending = this.dataGrid.sortOrder === "ascending"; + var sortColumnIdentifier = this.dataGrid.sortColumnIdentifier; + var sortProperty = { + "cons": ["constructorName", null], + "count": ["count", null], + "size": ["size", "count"], + "countDelta": this.showCountDeltaAsPercent ? ["countDeltaPercent", null] : ["countDelta", null], + "sizeDelta": this.showSizeDeltaAsPercent ? ["sizeDeltaPercent", "countDeltaPercent"] : ["sizeDelta", "sizeDeltaPercent"] + }[sortColumnIdentifier]; + + this.snapshotDataGridList.sort(WebInspector.HeapSnapshotDataGridList.propertyComparator(sortProperty[0], sortProperty[1], sortAscending)); + + this.refresh(); + }, + + _updateBaseOptions: function() + { + var list = WebInspector.HeapSnapshotProfileType.snapshots; + // We're assuming that snapshots can only be added. + if (this.baseSelectElement.length === list.length) + return; + + for (var i = this.baseSelectElement.length, n = list.length; i < n; ++i) { + var baseOption = document.createElement("option"); + baseOption.label = WebInspector.UIString("Compared to %s", list[i].title); + this.baseSelectElement.appendChild(baseOption); + } + }, + + _updatePercentButton: function() + { + if (this._isShowingAsPercent) { + this.percentButton.title = WebInspector.UIString("Show absolute counts and sizes."); + this.percentButton.toggled = true; + } else { + this.percentButton.title = WebInspector.UIString("Show counts and sizes as percentages."); + this.percentButton.toggled = false; + } + }, + + _updateSummaryGraph: function() + { + this.countsSummaryBar.calculator.showAsPercent = this._isShowingAsPercent; + this.countsSummaryBar.update(this.profile.lowlevels); + + this.sizesSummaryBar.calculator.showAsPercent = this._isShowingAsPercent; + this.sizesSummaryBar.update(this.profile.lowlevels); + } +}; + +WebInspector.HeapSnapshotView.prototype.__proto__ = WebInspector.View.prototype; + +WebInspector.HeapSnapshotView.SearchHelper = { + // In comparators, we assume that a value from a node is passed as the first parameter. + operations: { LESS: function (a, b) { return a !== null && a < b; }, + LESS_OR_EQUAL: function (a, b) { return a !== null && a <= b; }, + EQUAL: function (a, b) { return a !== null && a === b; }, + GREATER_OR_EQUAL: function (a, b) { return a !== null && a >= b; }, + GREATER: function (a, b) { return a !== null && a > b; } }, + + operationParsers: { LESS: /^<(\d+)/, + LESS_OR_EQUAL: /^<=(\d+)/, + GREATER_OR_EQUAL: /^>=(\d+)/, + GREATER: /^>(\d+)/ }, + + parseOperationAndNumber: function(query) + { + var operations = WebInspector.HeapSnapshotView.SearchHelper.operations; + var parsers = WebInspector.HeapSnapshotView.SearchHelper.operationParsers; + for (var operation in parsers) { + var match = query.match(parsers[operation]); + if (match !== null) + return [operations[operation], parseFloat(match[1])]; + } + return [operations.EQUAL, parseFloat(query)]; + }, + + percents: /%$/, + + megaBytes: /MB$/i, + + kiloBytes: /KB$/i, + + bytes: /B$/i +} + +WebInspector.HeapSummaryCalculator = function(lowLevelField) +{ + this.total = 1; + this.lowLevelField = lowLevelField; +} + +WebInspector.HeapSummaryCalculator.prototype = { + computeSummaryValues: function(lowLevels) + { + var highLevels = {data: 0, code: 0}; + this.total = 0; + for (var item in lowLevels) { + var highItem = this._highFromLow(item); + if (highItem) { + var value = lowLevels[item][this.lowLevelField]; + highLevels[highItem] += value; + this.total += value; + } + } + var result = {categoryValues: highLevels}; + if (!this.showAsPercent) + result.total = this.total; + return result; + }, + + formatValue: function(value) + { + if (this.showAsPercent) + return WebInspector.UIString("%.2f%%", value / this.total * 100.0); + else + return this._valueToString(value); + }, + + get showAsPercent() + { + return this._showAsPercent; + }, + + set showAsPercent(x) + { + this._showAsPercent = x; + } +} + +WebInspector.HeapSummaryCountCalculator = function() +{ + WebInspector.HeapSummaryCalculator.call(this, "count"); +} + +WebInspector.HeapSummaryCountCalculator.prototype = { + _highFromLow: function(type) { + if (type === "CODE_TYPE" || type === "SHARED_FUNCTION_INFO_TYPE" || type === "SCRIPT_TYPE") return "code"; + if (type === "STRING_TYPE" || type === "HEAP_NUMBER_TYPE" || type.match(/^JS_/)) return "data"; + return null; + }, + + _valueToString: function(value) { + return value.toString(); + } +} + +WebInspector.HeapSummaryCountCalculator.prototype.__proto__ = WebInspector.HeapSummaryCalculator.prototype; + +WebInspector.HeapSummarySizeCalculator = function() +{ + WebInspector.HeapSummaryCalculator.call(this, "size"); +} + +WebInspector.HeapSummarySizeCalculator.prototype = { + _highFromLow: function(type) { + if (type === "CODE_TYPE" || type === "SHARED_FUNCTION_INFO_TYPE" || type === "SCRIPT_TYPE") return "code"; + if (type === "STRING_TYPE" || type === "HEAP_NUMBER_TYPE" || type.match(/^JS_/) || type.match(/_ARRAY_TYPE$/)) return "data"; + return null; + }, + + _valueToString: Number.bytesToString +} + +WebInspector.HeapSummarySizeCalculator.prototype.__proto__ = WebInspector.HeapSummaryCalculator.prototype; + +WebInspector.HeapSnapshotSidebarTreeElement = function(snapshot) +{ + this.profile = snapshot; + + WebInspector.SidebarTreeElement.call(this, "heap-snapshot-sidebar-tree-item", "", "", snapshot, false); + + this.refreshTitles(); +}; + +WebInspector.HeapSnapshotSidebarTreeElement.prototype = { + get mainTitle() + { + if (this._mainTitle) + return this._mainTitle; + return this.profile.title; + }, + + set mainTitle(x) + { + this._mainTitle = x; + this.refreshTitles(); + } +}; + +WebInspector.HeapSnapshotSidebarTreeElement.prototype.__proto__ = WebInspector.ProfileSidebarTreeElement.prototype; + +WebInspector.HeapSnapshotDataGridNodeWithRetainers = function(owningTree) +{ + this.tree = owningTree; + + WebInspector.DataGridNode.call(this, null, this._hasRetainers); + + this.addEventListener("populate", this._populate, this); +}; + +WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype = { + isEmptySet: function(set) + { + for (var x in set) + return false; + return true; + }, + + get _hasRetainers() + { + return !this.isEmptySet(this.retainers); + }, + + get _parent() + { + // For top-level nodes, return owning tree as a parent, not data grid. + return this.parent !== this.dataGrid ? this.parent : this.tree; + }, + + _populate: function(event) + { + var self = this; + this.produceDiff(this.baseRetainers, this.retainers, function(baseItem, snapshotItem) { + self.appendChild(new WebInspector.HeapSnapshotDataGridRetainerNode(self.snapshotView, baseItem, snapshotItem, self.tree)); + }); + + if (this._parent) { + var currentComparator = this._parent.lastComparator; + if (currentComparator) + this.sort(currentComparator, true); + } + + this.removeEventListener("populate", this._populate, this); + }, + + produceDiff: function(baseEntries, currentEntries, callback) + { + for (var item in currentEntries) + callback(baseEntries[item], currentEntries[item]); + + for (item in baseEntries) { + if (!(item in currentEntries)) + callback(baseEntries[item], null); + } + }, + + sort: function(comparator, force) { + if (!force && this.lastComparator === comparator) + return; + + this.children.sort(comparator); + var childCount = this.children.length; + for (var childIndex = 0; childIndex < childCount; ++childIndex) + this.children[childIndex]._recalculateSiblings(childIndex); + for (var i = 0; i < this.children.length; ++i) { + var child = this.children[i]; + if (!force && (!child.expanded || child.lastComparator === comparator)) + continue; + child.sort(comparator, force); + } + this.lastComparator = comparator; + }, + + signForDelta: function(delta) { + if (delta === 0) + return ""; + if (delta > 0) + return "+"; + else + // Math minus sign, same width as plus. + return "\u2212"; + }, + + showDeltaAsPercent: function(value) { + if (value === Number.POSITIVE_INFINITY) + return WebInspector.UIString("new"); + else if (value === Number.NEGATIVE_INFINITY) + return WebInspector.UIString("deleted"); + if (value > 1000.0) + return WebInspector.UIString("%s >1000%%", this.signForDelta(value)); + return WebInspector.UIString("%s%.2f%%", this.signForDelta(value), Math.abs(value)); + }, + + getTotalCount: function() { + if (!this._count) { + this._count = 0; + for (var i = 0, n = this.children.length; i < n; ++i) + this._count += this.children[i].count; + } + return this._count; + }, + + getTotalSize: function() { + if (!this._size) { + this._size = 0; + for (var i = 0, n = this.children.length; i < n; ++i) + this._size += this.children[i].size; + } + return this._size; + }, + + get countPercent() + { + return this.count / this._parent.getTotalCount() * 100.0; + }, + + get sizePercent() + { + return this.size / this._parent.getTotalSize() * 100.0; + }, + + get countDeltaPercent() + { + if (this.baseCount > 0) { + if (this.count > 0) + return this.countDelta / this.baseCount * 100.0; + else + return Number.NEGATIVE_INFINITY; + } else + return Number.POSITIVE_INFINITY; + }, + + get sizeDeltaPercent() + { + if (this.baseSize > 0) { + if (this.size > 0) + return this.sizeDelta / this.baseSize * 100.0; + else + return Number.NEGATIVE_INFINITY; + } else + return Number.POSITIVE_INFINITY; + }, + + get data() + { + var data = {}; + + data["cons"] = this.constructorName; + + if (this.snapshotView.showCountAsPercent) + data["count"] = WebInspector.UIString("%.2f%%", this.countPercent); + else + data["count"] = this.count; + + if (this.size !== null) { + if (this.snapshotView.showSizeAsPercent) + data["size"] = WebInspector.UIString("%.2f%%", this.sizePercent); + else + data["size"] = Number.bytesToString(this.size); + } else + data["size"] = ""; + + if (this.snapshotView.showCountDeltaAsPercent) + data["countDelta"] = this.showDeltaAsPercent(this.countDeltaPercent); + else + data["countDelta"] = WebInspector.UIString("%s%d", this.signForDelta(this.countDelta), Math.abs(this.countDelta)); + + if (this.sizeDelta !== null) { + if (this.snapshotView.showSizeDeltaAsPercent) + data["sizeDelta"] = this.showDeltaAsPercent(this.sizeDeltaPercent); + else + data["sizeDelta"] = WebInspector.UIString("%s%s", this.signForDelta(this.sizeDelta), Number.bytesToString(Math.abs(this.sizeDelta))); + } else + data["sizeDelta"] = ""; + + return data; + }, + + createCell: function(columnIdentifier) + { + var cell = WebInspector.DataGridNode.prototype.createCell.call(this, columnIdentifier); + + if ((columnIdentifier === "cons" && this._searchMatchedConsColumn) || + (columnIdentifier === "count" && this._searchMatchedCountColumn) || + (columnIdentifier === "size" && this._searchMatchedSizeColumn) || + (columnIdentifier === "countDelta" && this._searchMatchedCountDeltaColumn) || + (columnIdentifier === "sizeDelta" && this._searchMatchedSizeDeltaColumn)) + cell.addStyleClass("highlight"); + + return cell; + } +}; + +WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype.__proto__ = WebInspector.DataGridNode.prototype; + +WebInspector.HeapSnapshotDataGridNode = function(snapshotView, baseEntry, snapshotEntry, owningTree) +{ + this.snapshotView = snapshotView; + + if (!snapshotEntry) + snapshotEntry = { cons: baseEntry.cons, count: 0, size: 0, retainers: {} }; + this.constructorName = snapshotEntry.cons; + this.count = snapshotEntry.count; + this.size = snapshotEntry.size; + this.retainers = snapshotEntry.retainers; + + if (!baseEntry) + baseEntry = { count: 0, size: 0, retainers: {} }; + this.baseCount = baseEntry.count; + this.countDelta = this.count - this.baseCount; + this.baseSize = baseEntry.size; + this.sizeDelta = this.size - this.baseSize; + this.baseRetainers = baseEntry.retainers; + + WebInspector.HeapSnapshotDataGridNodeWithRetainers.call(this, owningTree); +}; + +WebInspector.HeapSnapshotDataGridNode.prototype.__proto__ = WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype; + +WebInspector.HeapSnapshotDataGridList = function(snapshotView, baseEntries, snapshotEntries) +{ + this.tree = this; + this.snapshotView = snapshotView; + this.children = []; + this.lastComparator = null; + this.populateChildren(baseEntries, snapshotEntries); +}; + +WebInspector.HeapSnapshotDataGridList.prototype = { + appendChild: function(child) + { + this.insertChild(child, this.children.length); + }, + + insertChild: function(child, index) + { + this.children.splice(index, 0, child); + }, + + removeChildren: function() + { + this.children = []; + }, + + populateChildren: function(baseEntries, snapshotEntries) + { + var self = this; + this.produceDiff(baseEntries, snapshotEntries, function(baseItem, snapshotItem) { + self.appendChild(new WebInspector.HeapSnapshotDataGridNode(self.snapshotView, baseItem, snapshotItem, self)); + }); + }, + + produceDiff: WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype.produceDiff, + sort: WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype.sort, + getTotalCount: WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype.getTotalCount, + getTotalSize: WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype.getTotalSize +}; + +WebInspector.HeapSnapshotDataGridList.propertyComparators = [{}, {}]; + +WebInspector.HeapSnapshotDataGridList.propertyComparator = function(property, property2, isAscending) +{ + var propertyHash = property + "#" + property2; + var comparator = this.propertyComparators[(isAscending ? 1 : 0)][propertyHash]; + if (!comparator) { + comparator = function(lhs, rhs) { + var l = lhs[property], r = rhs[property]; + if ((l === null || r === null) && property2 !== null) + l = lhs[property2], r = rhs[property2]; + var result = l < r ? -1 : (l > r ? 1 : 0); + return isAscending ? result : -result; + }; + this.propertyComparators[(isAscending ? 1 : 0)][propertyHash] = comparator; + } + return comparator; +}; + +WebInspector.HeapSnapshotDataGridRetainerNode = function(snapshotView, baseEntry, snapshotEntry, owningTree) +{ + this.snapshotView = snapshotView; + + if (!snapshotEntry) + snapshotEntry = { cons: baseEntry.cons, count: 0, clusters: {} }; + this.constructorName = snapshotEntry.cons; + this.count = snapshotEntry.count; + this.retainers = this._calculateRetainers(this.snapshotView.profile, snapshotEntry.clusters); + + if (!baseEntry) + baseEntry = { count: 0, clusters: {} }; + this.baseCount = baseEntry.count; + this.countDelta = this.count - this.baseCount; + this.baseRetainers = this._calculateRetainers(this.snapshotView.baseSnapshot, baseEntry.clusters); + + this.size = null; + this.sizeDelta = null; + + WebInspector.HeapSnapshotDataGridNodeWithRetainers.call(this, owningTree); +} + +WebInspector.HeapSnapshotDataGridRetainerNode.prototype = { + get sizePercent() + { + return null; + }, + + get sizeDeltaPercent() + { + return null; + }, + + _calculateRetainers: function(snapshot, clusters) { + var retainers = {}; + if (this.isEmptySet(clusters)) { + if (this.constructorName in snapshot.entries) + return snapshot.entries[this.constructorName].retainers; + } else { + // In case when an entry is retained by clusters, we need to gather up the list + // of retainers by merging retainers of every cluster. + // E.g. having such a tree: + // A + // Object:1 10 + // X 3 + // Y 4 + // Object:2 5 + // X 6 + // + // will result in a following retainers list: X 9, Y 4. + for (var clusterName in clusters) { + if (clusterName in snapshot.clusters) { + var clusterRetainers = snapshot.clusters[clusterName].retainers; + for (var clusterRetainer in clusterRetainers) { + var clusterRetainerEntry = clusterRetainers[clusterRetainer]; + if (!(clusterRetainer in retainers)) + retainers[clusterRetainer] = { cons: clusterRetainerEntry.cons, count: 0, clusters: {} }; + retainers[clusterRetainer].count += clusterRetainerEntry.count; + for (var clusterRetainerCluster in clusterRetainerEntry.clusters) + retainers[clusterRetainer].clusters[clusterRetainerCluster] = true; + } + } + } + } + return retainers; + } +}; + +WebInspector.HeapSnapshotDataGridRetainerNode.prototype.__proto__ = WebInspector.HeapSnapshotDataGridNodeWithRetainers.prototype; + + +WebInspector.HeapSnapshotProfileType = function() +{ + WebInspector.ProfileType.call(this, WebInspector.HeapSnapshotProfileType.TypeId, WebInspector.UIString("HEAP SNAPSHOTS")); +} + +WebInspector.HeapSnapshotProfileType.TypeId = "HEAP"; + +WebInspector.HeapSnapshotProfileType.snapshots = []; + +WebInspector.HeapSnapshotProfileType.prototype = { + get buttonTooltip() + { + return WebInspector.UIString("Take heap snapshot."); + }, + + get buttonStyle() + { + return "heap-snapshot-status-bar-item status-bar-item"; + }, + + buttonClicked: function() + { + InspectorBackend.takeHeapSnapshot(); + }, + + get welcomeMessage() + { + return WebInspector.UIString("Get a heap snapshot by pressing<br>the %s button on the status bar."); + }, + + createSidebarTreeElementForProfile: function(profile) + { + var element = new WebInspector.HeapSnapshotSidebarTreeElement(profile); + element.small = false; + return element; + }, + + createView: function(profile) + { + return new WebInspector.HeapSnapshotView(WebInspector.panels.profiles, profile); + } +} + +WebInspector.HeapSnapshotProfileType.prototype.__proto__ = WebInspector.ProfileType.prototype; + + +(function() { + var originalCreatePanels = WebInspector._createPanels; + WebInspector._createPanels = function() { + originalCreatePanels.apply(this, arguments); + if (WebInspector.panels.profiles) + WebInspector.panels.profiles.registerProfileType(new WebInspector.HeapSnapshotProfileType()); + } +})(); diff --git a/webkit/glue/devtools/js/inject_dispatch.js b/webkit/glue/devtools/js/inject_dispatch.js new file mode 100644 index 0000000..1c2cba2 --- /dev/null +++ b/webkit/glue/devtools/js/inject_dispatch.js @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview Injects "injected" object into the inspectable page. + */ + + +var InspectorControllerDispatcher = {}; + +/** + * Main dispatch method, all calls from the host to InspectorController go + * through this one. + * @param {string} functionName Function to call + * @param {string} json_args JSON-serialized call parameters. + * @return {string} JSON-serialized result of the dispatched call. + */ +InspectorControllerDispatcher.dispatch = function(functionName, json_args) +{ + var params = JSON.parse(json_args); + InspectorBackend[functionName].apply(InspectorBackend, params); +}; + +/** + * Special controller object for APU related messages. Outgoing messages + * are sent to this object if the ApuAgentDispatcher is enabled. + **/ +var ApuAgentDispatcher = { enabled : false }; + +/** + * Dispatches messages to APU. This filters and transforms + * outgoing messages that are used by APU. + * @param {string} method name of the dispatch method. + **/ +ApuAgentDispatcher.dispatchToApu = function(method, args) +{ + if (method !== "addRecordToTimeline" && method !== "updateResource" && method !== "addResource") + return; + // TODO(knorton): Transform args so they can be used + // by APU. + DevToolsAgentHost.dispatchToApu(JSON.stringify(args)); +}; + +/** + * This is called by the InspectorFrontend for serialization. + * We serialize the call and send it to the client over the IPC + * using dispatchOut bound method. + */ +function dispatch(method, var_args) { + // Handle all messages with non-primitieve arguments here. + var args = Array.prototype.slice.call(arguments); + + if (method === "inspectedWindowCleared" || method === "reset" || method === "setAttachedWindow") { + // Filter out messages we don't need here. + // We do it on the sender side since they may have non-serializable + // parameters. + return; + } + + // Sniff some inspector controller state changes in order to support + // cross-navigation instrumentation. Keep names in sync with + // webdevtoolsagent_impl. + if (method === "timelineProfilerWasStarted") + DevToolsAgentHost.runtimeFeatureStateChanged("timeline-profiler", true); + else if (method === "timelineProfilerWasStopped") + DevToolsAgentHost.runtimeFeatureStateChanged("timeline-profiler", false); + else if (method === "resourceTrackingWasEnabled") + DevToolsAgentHost.runtimeFeatureStateChanged("resource-tracking", true); + else if (method === "resourceTrackingWasDisabled") + DevToolsAgentHost.runtimeFeatureStateChanged("resource-tracking", false); + + if (ApuAgentDispatcher.enabled) { + ApuAgentDispatcher.dispatchToApu(method, args); + return; + } + + var call = JSON.stringify(args); + DevToolsAgentHost.dispatch(call); +}; + +/** + * A no-op function that is called by debugger agent just to trigger v8 + * execution. + */ +function devtools$$void() { +} diff --git a/webkit/glue/devtools/js/inspector_controller_impl.js b/webkit/glue/devtools/js/inspector_controller_impl.js new file mode 100644 index 0000000..c92a94c --- /dev/null +++ b/webkit/glue/devtools/js/inspector_controller_impl.js @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview DevTools' implementation of the InspectorController API. + */ + +if (!this.devtools) + devtools = {}; + +devtools.InspectorBackendImpl = function() +{ + WebInspector.InspectorBackendStub.call(this); + this.installInspectorControllerDelegate_("clearMessages"); + this.installInspectorControllerDelegate_("copyNode"); + this.installInspectorControllerDelegate_("deleteCookie"); + this.installInspectorControllerDelegate_("didEvaluateForTestInFrontend"); + this.installInspectorControllerDelegate_("disableResourceTracking"); + this.installInspectorControllerDelegate_("disableTimeline"); + this.installInspectorControllerDelegate_("enableResourceTracking"); + this.installInspectorControllerDelegate_("enableTimeline"); + this.installInspectorControllerDelegate_("getChildNodes"); + this.installInspectorControllerDelegate_("getCookies"); + this.installInspectorControllerDelegate_("getDatabaseTableNames"); + this.installInspectorControllerDelegate_("getDOMStorageEntries"); + this.installInspectorControllerDelegate_("getEventListenersForNode"); + this.installInspectorControllerDelegate_("getResourceContent"); + this.installInspectorControllerDelegate_("highlightDOMNode"); + this.installInspectorControllerDelegate_("hideDOMNodeHighlight"); + this.installInspectorControllerDelegate_("releaseWrapperObjectGroup"); + this.installInspectorControllerDelegate_("removeAttribute"); + this.installInspectorControllerDelegate_("removeDOMStorageItem"); + this.installInspectorControllerDelegate_("removeNode"); + this.installInspectorControllerDelegate_("saveFrontendSettings"); + this.installInspectorControllerDelegate_("setAttribute"); + this.installInspectorControllerDelegate_("setDOMStorageItem"); + this.installInspectorControllerDelegate_("setInjectedScriptSource"); + this.installInspectorControllerDelegate_("setTextNodeValue"); + this.installInspectorControllerDelegate_("startTimelineProfiler"); + this.installInspectorControllerDelegate_("stopTimelineProfiler"); + this.installInspectorControllerDelegate_("storeLastActivePanel"); +}; +devtools.InspectorBackendImpl.prototype.__proto__ = WebInspector.InspectorBackendStub.prototype; + + +/** + * {@inheritDoc}. + */ +devtools.InspectorBackendImpl.prototype.toggleNodeSearch = function() +{ + WebInspector.InspectorBackendStub.prototype.toggleNodeSearch.call(this); + this.callInspectorController_.call(this, "toggleNodeSearch"); + if (!this.searchingForNode()) { + // This is called from ElementsPanel treeOutline's focusNodeChanged(). + DevToolsHost.activateWindow(); + } +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.debuggerEnabled = function() +{ + return true; +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.profilerEnabled = function() +{ + return true; +}; + + +devtools.InspectorBackendImpl.prototype.addBreakpoint = function(sourceID, line, condition) +{ + devtools.tools.getDebuggerAgent().addBreakpoint(sourceID, line, condition); +}; + + +devtools.InspectorBackendImpl.prototype.removeBreakpoint = function(sourceID, line) +{ + devtools.tools.getDebuggerAgent().removeBreakpoint(sourceID, line); +}; + +devtools.InspectorBackendImpl.prototype.updateBreakpoint = function(sourceID, line, condition) +{ + devtools.tools.getDebuggerAgent().updateBreakpoint(sourceID, line, condition); +}; + +devtools.InspectorBackendImpl.prototype.pauseInDebugger = function() +{ + devtools.tools.getDebuggerAgent().pauseExecution(); +}; + + +devtools.InspectorBackendImpl.prototype.resumeDebugger = function() +{ + devtools.tools.getDebuggerAgent().resumeExecution(); +}; + + +devtools.InspectorBackendImpl.prototype.stepIntoStatementInDebugger = function() +{ + devtools.tools.getDebuggerAgent().stepIntoStatement(); +}; + + +devtools.InspectorBackendImpl.prototype.stepOutOfFunctionInDebugger = function() +{ + devtools.tools.getDebuggerAgent().stepOutOfFunction(); +}; + + +devtools.InspectorBackendImpl.prototype.stepOverStatementInDebugger = function() +{ + devtools.tools.getDebuggerAgent().stepOverStatement(); +}; + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.setPauseOnExceptionsState = function(state) +{ + this._setPauseOnExceptionsState = state; + // TODO(yurys): support all three states. See http://crbug.com/32877 + var enabled = (state !== WebInspector.ScriptsPanel.PauseOnExceptionsState.DontPauseOnExceptions); + return devtools.tools.getDebuggerAgent().setPauseOnExceptions(enabled); +}; + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.pauseOnExceptionsState = function() +{ + return (this._setPauseOnExceptionsState || WebInspector.ScriptsPanel.PauseOnExceptionsState.DontPauseOnExceptions); +}; + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.pauseOnExceptions = function() +{ + return devtools.tools.getDebuggerAgent().pauseOnExceptions(); +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.setPauseOnExceptions = function(value) +{ + return devtools.tools.getDebuggerAgent().setPauseOnExceptions(value); +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.startProfiling = function() +{ + devtools.tools.getProfilerAgent().startProfiling(devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_CPU); +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.stopProfiling = function() +{ + devtools.tools.getProfilerAgent().stopProfiling( devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_CPU); +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.getProfileHeaders = function(callId) +{ + WebInspector.didGetProfileHeaders(callId, []); +}; + + +/** + * Emulate WebKit InspectorController behavior. It stores profiles on renderer side, + * and is able to retrieve them by uid using "getProfile". + */ +devtools.InspectorBackendImpl.prototype.addFullProfile = function(profile) +{ + WebInspector.__fullProfiles = WebInspector.__fullProfiles || {}; + WebInspector.__fullProfiles[profile.uid] = profile; +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.getProfile = function(callId, uid) +{ + if (WebInspector.__fullProfiles && (uid in WebInspector.__fullProfiles)) + WebInspector.didGetProfile(callId, WebInspector.__fullProfiles[uid]); +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.takeHeapSnapshot = function() +{ + devtools.tools.getProfilerAgent().startProfiling(devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_HEAP_SNAPSHOT + | devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_HEAP_STATS + | devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_JS_CONSTRUCTORS); +}; + + +/** + * @override + */ +devtools.InspectorBackendImpl.prototype.dispatchOnInjectedScript = function(callId, injectedScriptId, methodName, argsString, async) +{ + // Encode injectedScriptId into callId + if (typeof injectedScriptId !== "number") + injectedScriptId = 0; + RemoteToolsAgent.dispatchOnInjectedScript(callId, injectedScriptId, methodName, argsString, async); +}; + + +/** + * Installs delegating handler into the inspector controller. + * @param {string} methodName Method to install delegating handler for. + */ +devtools.InspectorBackendImpl.prototype.installInspectorControllerDelegate_ = function(methodName) +{ + this[methodName] = this.callInspectorController_.bind(this, methodName); +}; + + +/** + * Bound function with the installInjectedScriptDelegate_ actual + * implementation. + */ +devtools.InspectorBackendImpl.prototype.callInspectorController_ = function(methodName, var_arg) +{ + var args = Array.prototype.slice.call(arguments, 1); + RemoteToolsAgent.dispatchOnInspectorController(WebInspector.Callback.wrap(function(){}), methodName, JSON.stringify(args)); +}; + + +InspectorBackend = new devtools.InspectorBackendImpl(); diff --git a/webkit/glue/devtools/js/profiler_agent.js b/webkit/glue/devtools/js/profiler_agent.js new file mode 100644 index 0000000..b7411ab --- /dev/null +++ b/webkit/glue/devtools/js/profiler_agent.js @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview Provides communication interface to remote v8 profiler. + */ + +/** + * @constructor + */ +devtools.ProfilerAgent = function() +{ + RemoteProfilerAgent.didGetActiveProfilerModules = this.didGetActiveProfilerModules_.bind(this); + RemoteProfilerAgent.didGetLogLines = this.didGetLogLines_.bind(this); + + /** + * Active profiler modules flags. + * @type {number} + */ + this.activeProfilerModules_ = devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_NONE; + + /** + * Interval for polling profiler state. + * @type {number} + */ + this.getActiveProfilerModulesInterval_ = null; + + /** + * Profiler log position. + * @type {number} + */ + this.logPosition_ = 0; + + /** + * Whether log contents retrieval must be forced next time. + * @type {boolean} + */ + this.forceGetLogLines_ = false; + + /** + * Profiler processor instance. + * @type {devtools.profiler.Processor} + */ + this.profilerProcessor_ = new devtools.profiler.Processor(); +}; + + +/** + * A copy of enum from include/v8.h + * @enum {number} + */ +devtools.ProfilerAgent.ProfilerModules = { + PROFILER_MODULE_NONE: 0, + PROFILER_MODULE_CPU: 1, + PROFILER_MODULE_HEAP_STATS: 1 << 1, + PROFILER_MODULE_JS_CONSTRUCTORS: 1 << 2, + PROFILER_MODULE_HEAP_SNAPSHOT: 1 << 16 +}; + + +/** + * Sets up callbacks that deal with profiles processing. + */ +devtools.ProfilerAgent.prototype.setupProfilerProcessorCallbacks = function() +{ + // A temporary icon indicating that the profile is being processed. + var processingIcon = new WebInspector.SidebarTreeElement( + "profile-sidebar-tree-item", + WebInspector.UIString("Processing..."), + '', null, false); + var profilesSidebar = WebInspector.panels.profiles.getProfileType(WebInspector.CPUProfileType.TypeId).treeElement; + + this.profilerProcessor_.setCallbacks( + function onProfileProcessingStarted() { + // Set visually empty string. Subtitle hiding is done via styles + // manipulation which doesn't play well with dynamic append / removal. + processingIcon.subtitle = " "; + profilesSidebar.appendChild(processingIcon); + }, + function onProfileProcessingStatus(ticksCount) { + processingIcon.subtitle = WebInspector.UIString("%d ticks processed", ticksCount); + }, + function onProfileProcessingFinished(profile) { + profilesSidebar.removeChild(processingIcon); + profile.typeId = WebInspector.CPUProfileType.TypeId; + InspectorBackend.addFullProfile(profile); + WebInspector.addProfileHeader(profile); + // If no profile is currently shown, show the new one. + var profilesPanel = WebInspector.panels.profiles; + if (!profilesPanel.visibleView) { + profilesPanel.showProfile(profile); + } + } + ); +}; + + +/** + * Initializes profiling state. + */ +devtools.ProfilerAgent.prototype.initializeProfiling = function() +{ + this.setupProfilerProcessorCallbacks(); + this.forceGetLogLines_ = true; + this.getActiveProfilerModulesInterval_ = setInterval(function() { RemoteProfilerAgent.getActiveProfilerModules(); }, 1000); +}; + + +/** + * Starts profiling. + * @param {number} modules List of modules to enable. + */ +devtools.ProfilerAgent.prototype.startProfiling = function(modules) +{ + var cmd = new devtools.DebugCommand("profile", { + "modules": modules, + "command": "resume"}); + devtools.DebuggerAgent.sendCommand_(cmd); + RemoteToolsAgent.executeVoidJavaScript(); + if (modules & devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_HEAP_SNAPSHOT) { + var pos = this.logPosition_; + // Active modules will not change, instead, a snapshot will be logged. + setTimeout(function() { RemoteProfilerAgent.getLogLines(pos); }, 500); + } +}; + + +/** + * Stops profiling. + */ +devtools.ProfilerAgent.prototype.stopProfiling = function(modules) +{ + var cmd = new devtools.DebugCommand("profile", { + "modules": modules, + "command": "pause"}); + devtools.DebuggerAgent.sendCommand_(cmd); + RemoteToolsAgent.executeVoidJavaScript(); +}; + + +/** + * Handles current profiler status. + * @param {number} modules List of active (started) modules. + */ +devtools.ProfilerAgent.prototype.didGetActiveProfilerModules_ = function(modules) +{ + var profModules = devtools.ProfilerAgent.ProfilerModules; + var profModuleNone = profModules.PROFILER_MODULE_NONE; + if (this.forceGetLogLines_ || (modules !== profModuleNone && this.activeProfilerModules_ === profModuleNone)) { + this.forceGetLogLines_ = false; + // Start to query log data. + RemoteProfilerAgent.getLogLines(this.logPosition_); + } + this.activeProfilerModules_ = modules; + // Update buttons. + WebInspector.setRecordingProfile(modules & profModules.PROFILER_MODULE_CPU); +}; + + +/** + * Handles a portion of a profiler log retrieved by getLogLines call. + * @param {number} pos Current position in log. + * @param {string} log A portion of profiler log. + */ +devtools.ProfilerAgent.prototype.didGetLogLines_ = function(pos, log) +{ + this.logPosition_ = pos; + if (log.length > 0) + this.profilerProcessor_.processLogChunk(log); + else if (this.activeProfilerModules_ === devtools.ProfilerAgent.ProfilerModules.PROFILER_MODULE_NONE) { + // No new data and profiling is stopped---suspend log reading. + return; + } + setTimeout(function() { RemoteProfilerAgent.getLogLines(pos); }, 500); +}; diff --git a/webkit/glue/devtools/js/profiler_processor.js b/webkit/glue/devtools/js/profiler_processor.js new file mode 100644 index 0000000..b9b0beb --- /dev/null +++ b/webkit/glue/devtools/js/profiler_processor.js @@ -0,0 +1,542 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * @fileoverview Profiler processor is used to process log file produced + * by V8 and produce an internal profile representation which is used + * for building profile views in "Profiles" tab. + */ + + +/** + * Creates a Profile View builder object compatible with WebKit Profiler UI. + * + * @param {number} samplingRate Number of ms between profiler ticks. + * @constructor + */ +devtools.profiler.WebKitViewBuilder = function(samplingRate) +{ + devtools.profiler.ViewBuilder.call(this, samplingRate); +}; +devtools.profiler.WebKitViewBuilder.prototype.__proto__ = devtools.profiler.ViewBuilder.prototype; + + +/** + * @override + */ +devtools.profiler.WebKitViewBuilder.prototype.createViewNode = function(funcName, totalTime, selfTime, head) +{ + return new devtools.profiler.WebKitViewNode(funcName, totalTime, selfTime, head); +}; + + +/** + * Constructs a Profile View node object for displaying in WebKit Profiler UI. + * + * @param {string} internalFuncName A fully qualified function name. + * @param {number} totalTime Amount of time that application spent in the + * corresponding function and its descendants (not that depending on + * profile they can be either callees or callers.) + * @param {number} selfTime Amount of time that application spent in the + * corresponding function only. + * @param {devtools.profiler.ProfileView.Node} head Profile view head. + * @constructor + */ +devtools.profiler.WebKitViewNode = function(internalFuncName, totalTime, selfTime, head) +{ + devtools.profiler.ProfileView.Node.call(this, internalFuncName, totalTime, selfTime, head); + this.initFuncInfo_(); + this.callUID = internalFuncName; +}; +devtools.profiler.WebKitViewNode.prototype.__proto__ = devtools.profiler.ProfileView.Node.prototype; + + +/** + * RegEx for stripping V8's prefixes of compiled functions. + */ +devtools.profiler.WebKitViewNode.FUNC_NAME_STRIP_RE = /^(?:LazyCompile|Function|Callback): (.*)$/; + + +/** + * RegEx for extracting script source URL and line number. + */ +devtools.profiler.WebKitViewNode.FUNC_NAME_PARSE_RE = /^((?:get | set )?[^ ]+) (.*):(\d+)( \{\d+\})?$/; + + +/** + * Inits "functionName", "url", and "lineNumber" fields using "internalFuncName" + * field. + * @private + */ +devtools.profiler.WebKitViewNode.prototype.initFuncInfo_ = function() +{ + var nodeAlias = devtools.profiler.WebKitViewNode; + this.functionName = this.internalFuncName; + + var strippedName = nodeAlias.FUNC_NAME_STRIP_RE.exec(this.functionName); + if (strippedName) + this.functionName = strippedName[1]; + + var parsedName = nodeAlias.FUNC_NAME_PARSE_RE.exec(this.functionName); + if (parsedName) { + this.functionName = parsedName[1]; + if (parsedName[4]) + this.functionName += parsedName[4]; + this.url = parsedName[2]; + this.lineNumber = parsedName[3]; + } else { + this.url = ''; + this.lineNumber = 0; + } +}; + + +/** + * Ancestor of a profile object that leaves out only JS-related functions. + * @constructor + */ +devtools.profiler.JsProfile = function() +{ + devtools.profiler.Profile.call(this); +}; +devtools.profiler.JsProfile.prototype.__proto__ = devtools.profiler.Profile.prototype; + + +/** + * RegExp that leaves only non-native JS functions. + * @type {RegExp} + */ +devtools.profiler.JsProfile.JS_NON_NATIVE_RE = new RegExp( + "^" + + "(?:Callback:)|" + + "(?:Script: (?!native))|" + + "(?:(?:LazyCompile|Function): [^ ]*(?: (?!native )[^ ]+:\\d+)?$)"); + + +/** + * @override + */ +devtools.profiler.JsProfile.prototype.skipThisFunction = function(name) +{ + return !devtools.profiler.JsProfile.JS_NON_NATIVE_RE.test(name); +}; + + +/** + * Profiler processor. Consumes profiler log and builds profile views. + * + * @param {function(devtools.profiler.ProfileView)} newProfileCallback Callback + * that receives a new processed profile. + * @constructor + */ +devtools.profiler.Processor = function() +{ + var dispatches = { + "code-creation": { + parsers: [null, this.createAddressParser("code"), parseInt, null], + processor: this.processCodeCreation_, backrefs: true, + needsProfile: true }, + "code-move": { parsers: [this.createAddressParser("code"), + this.createAddressParser("code-move-to")], + processor: this.processCodeMove_, backrefs: true, + needsProfile: true }, + "code-delete": { parsers: [this.createAddressParser("code")], + processor: this.processCodeDelete_, backrefs: true, + needsProfile: true }, + "function-creation": { parsers: [this.createAddressParser("code"), + this.createAddressParser("function-obj")], + processor: this.processFunctionCreation_, backrefs: true }, + "function-move": { parsers: [this.createAddressParser("code"), + this.createAddressParser("code-move-to")], + processor: this.processFunctionMove_, backrefs: true }, + "function-delete": { parsers: [this.createAddressParser("code")], + processor: this.processFunctionDelete_, backrefs: true }, + "tick": { parsers: [this.createAddressParser("code"), + this.createAddressParser("stack"), parseInt, "var-args"], + processor: this.processTick_, backrefs: true, needProfile: true }, + "profiler": { parsers: [null, "var-args"], + processor: this.processProfiler_, needsProfile: false }, + "heap-sample-begin": { parsers: [null, null, parseInt], + processor: this.processHeapSampleBegin_ }, + "heap-sample-stats": { parsers: [null, null, parseInt, parseInt], + processor: this.processHeapSampleStats_ }, + "heap-sample-item": { parsers: [null, parseInt, parseInt], + processor: this.processHeapSampleItem_ }, + "heap-js-cons-item": { parsers: [null, parseInt, parseInt], + processor: this.processHeapJsConsItem_ }, + "heap-js-ret-item": { parsers: [null, "var-args"], + processor: this.processHeapJsRetItem_ }, + "heap-sample-end": { parsers: [null, null], + processor: this.processHeapSampleEnd_ }, + // Not used in DevTools Profiler. + "shared-library": null, + // Obsolete row types. + "code-allocate": null, + "begin-code-region": null, + "end-code-region": null}; + + if (devtools.profiler.Profile.VERSION === 2) { + dispatches["tick"] = { parsers: [this.createAddressParser("code"), + this.createAddressParser("stack"), + this.createAddressParser("func"), parseInt, "var-args"], + processor: this.processTickV2_, backrefs: true }; + } + + devtools.profiler.LogReader.call(this, dispatches); + + /** + * Callback that is called when a new profile is encountered in the log. + * @type {function()} + */ + this.startedProfileProcessing_ = null; + + /** + * Callback that is called periodically to display processing status. + * @type {function()} + */ + this.profileProcessingStatus_ = null; + + /** + * Callback that is called when a profile has been processed and is ready + * to be shown. + * @type {function(devtools.profiler.ProfileView)} + */ + this.finishedProfileProcessing_ = null; + + /** + * The current profile. + * @type {devtools.profiler.JsProfile} + */ + this.currentProfile_ = null; + + /** + * Builder of profile views. Created during "profiler,begin" event processing. + * @type {devtools.profiler.WebKitViewBuilder} + */ + this.viewBuilder_ = null; + + /** + * Next profile id. + * @type {number} + */ + this.profileId_ = 1; + + /** + * Counter for processed ticks. + * @type {number} + */ + this.ticksCount_ = 0; + + /** + * Interval id for updating processing status. + * @type {number} + */ + this.processingInterval_ = null; + + /** + * The current heap snapshot. + * @type {string} + */ + this.currentHeapSnapshot_ = null; + + /** + * Next heap snapshot id. + * @type {number} + */ + this.heapSnapshotId_ = 1; +}; +devtools.profiler.Processor.prototype.__proto__ = devtools.profiler.LogReader.prototype; + + +/** + * @override + */ +devtools.profiler.Processor.prototype.printError = function(str) +{ + debugPrint(str); +}; + + +/** + * @override + */ +devtools.profiler.Processor.prototype.skipDispatch = function(dispatch) +{ + return dispatch.needsProfile && this.currentProfile_ === null; +}; + + +/** + * Sets profile processing callbacks. + * + * @param {function()} started Started processing callback. + * @param {function(devtools.profiler.ProfileView)} finished Finished + * processing callback. + */ +devtools.profiler.Processor.prototype.setCallbacks = function(started, processing, finished) +{ + this.startedProfileProcessing_ = started; + this.profileProcessingStatus_ = processing; + this.finishedProfileProcessing_ = finished; +}; + + +/** + * An address for the fake "(program)" entry. WebKit's visualisation + * has assumptions on how the top of the call tree should look like, + * and we need to add a fake entry as the topmost function. This + * address is chosen because it's the end address of the first memory + * page, which is never used for code or data, but only as a guard + * page for catching AV errors. + * + * @type {number} + */ +devtools.profiler.Processor.PROGRAM_ENTRY = 0xffff; +/** + * @type {string} + */ +devtools.profiler.Processor.PROGRAM_ENTRY_STR = "0xffff"; + + +/** + * Sets new profile callback. + * @param {function(devtools.profiler.ProfileView)} callback Callback function. + */ +devtools.profiler.Processor.prototype.setNewProfileCallback = function(callback) +{ + this.newProfileCallback_ = callback; +}; + + +devtools.profiler.Processor.prototype.processProfiler_ = function(state, params) +{ + switch (state) { + case "resume": + if (this.currentProfile_ === null) { + this.currentProfile_ = new devtools.profiler.JsProfile(); + // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY + this.currentProfile_.addCode("Function", "(program)", devtools.profiler.Processor.PROGRAM_ENTRY, 1); + if (this.startedProfileProcessing_) + this.startedProfileProcessing_(); + this.ticksCount_ = 0; + var self = this; + if (this.profileProcessingStatus_) { + this.processingInterval_ = window.setInterval( + function() { self.profileProcessingStatus_(self.ticksCount_); }, + 1000); + } + } + break; + case "pause": + if (this.currentProfile_ !== null) { + window.clearInterval(this.processingInterval_); + this.processingInterval_ = null; + if (this.finishedProfileProcessing_) + this.finishedProfileProcessing_(this.createProfileForView()); + this.currentProfile_ = null; + } + break; + case "begin": + var samplingRate = NaN; + if (params.length > 0) + samplingRate = parseInt(params[0]); + if (isNaN(samplingRate)) + samplingRate = 1; + this.viewBuilder_ = new devtools.profiler.WebKitViewBuilder(samplingRate); + break; + // These events are valid but aren't used. + case "compression": + case "end": break; + default: + throw new Error("unknown profiler state: " + state); + } +}; + + +devtools.profiler.Processor.prototype.processCodeCreation_ = function(type, start, size, name) +{ + this.currentProfile_.addCode(this.expandAlias(type), name, start, size); +}; + + +devtools.profiler.Processor.prototype.processCodeMove_ = function(from, to) +{ + this.currentProfile_.moveCode(from, to); +}; + + +devtools.profiler.Processor.prototype.processCodeDelete_ = function(start) +{ + this.currentProfile_.deleteCode(start); +}; + + +devtools.profiler.Processor.prototype.processFunctionCreation_ = function(functionAddr, codeAddr) +{ + this.currentProfile_.addCodeAlias(functionAddr, codeAddr); +}; + + +devtools.profiler.Processor.prototype.processFunctionMove_ = function(from, to) +{ + this.currentProfile_.safeMoveDynamicCode(from, to); +}; + + +devtools.profiler.Processor.prototype.processFunctionDelete_ = function(start) +{ + this.currentProfile_.safeDeleteDynamicCode(start); +}; + + +// TODO(mnaganov): Remove after next V8 roll. +devtools.profiler.Processor.prototype.processTick_ = function(pc, sp, vmState, stack) +{ + // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY + stack.push(devtools.profiler.Processor.PROGRAM_ENTRY_STR); + this.currentProfile_.recordTick(this.processStack(pc, stack)); + this.ticksCount_++; +}; + + +devtools.profiler.Processor.prototype.processTickV2_ = function(pc, sp, func, vmState, stack) +{ + // see the comment for devtools.profiler.Processor.PROGRAM_ENTRY + stack.push(devtools.profiler.Processor.PROGRAM_ENTRY_STR); + + + if (func) { + var funcEntry = this.currentProfile_.findEntry(func); + if (!funcEntry || !funcEntry.isJSFunction || !funcEntry.isJSFunction()) + func = 0; + else { + var currEntry = this.currentProfile_.findEntry(pc); + if (!currEntry || !currEntry.isJSFunction || currEntry.isJSFunction()) { + func = 0; + } + } + } + + this.currentProfile_.recordTick(this.processStack(pc, func, stack)); + this.ticksCount_++; +}; + + +devtools.profiler.Processor.prototype.processHeapSampleBegin_ = function(space, state, ticks) +{ + if (space !== "Heap") return; + this.currentHeapSnapshot_ = { + number: this.heapSnapshotId_++, + entries: {}, + clusters: {}, + lowlevels: {}, + ticks: ticks + }; +}; + + +devtools.profiler.Processor.prototype.processHeapSampleStats_ = function(space, state, capacity, used) +{ + if (space !== "Heap") return; +}; + + +devtools.profiler.Processor.prototype.processHeapSampleItem_ = function(item, number, size) +{ + if (!this.currentHeapSnapshot_) return; + this.currentHeapSnapshot_.lowlevels[item] = { + type: item, count: number, size: size + }; +}; + + +devtools.profiler.Processor.prototype.processHeapJsConsItem_ = function(item, number, size) +{ + if (!this.currentHeapSnapshot_) return; + this.currentHeapSnapshot_.entries[item] = { + cons: item, count: number, size: size, retainers: {} + }; +}; + + +devtools.profiler.Processor.prototype.processHeapJsRetItem_ = function(item, retainersArray) +{ + if (!this.currentHeapSnapshot_) return; + var rawRetainers = {}; + for (var i = 0, n = retainersArray.length; i < n; ++i) { + var entry = retainersArray[i].split(";"); + rawRetainers[entry[0]] = parseInt(entry[1], 10); + } + + function mergeRetainers(entry) { + for (var rawRetainer in rawRetainers) { + var consName = rawRetainer.indexOf(":") !== -1 ? rawRetainer.split(":")[0] : rawRetainer; + if (!(consName in entry.retainers)) + entry.retainers[consName] = { cons: consName, count: 0, clusters: {} }; + var retainer = entry.retainers[consName]; + retainer.count += rawRetainers[rawRetainer]; + if (consName !== rawRetainer) + retainer.clusters[rawRetainer] = true; + } + } + + if (item.indexOf(":") !== -1) { + // Array, Function, or Object instances cluster case. + if (!(item in this.currentHeapSnapshot_.clusters)) { + this.currentHeapSnapshot_.clusters[item] = { + cons: item, retainers: {} + }; + } + mergeRetainers(this.currentHeapSnapshot_.clusters[item]); + item = item.split(":")[0]; + } + mergeRetainers(this.currentHeapSnapshot_.entries[item]); +}; + + +devtools.profiler.Processor.prototype.processHeapSampleEnd_ = function(space, state) +{ + if (space !== "Heap") return; + var snapshot = this.currentHeapSnapshot_; + this.currentHeapSnapshot_ = null; + WebInspector.panels.profiles.addSnapshot(snapshot); +}; + + +/** + * Creates a profile for further displaying in ProfileView. + */ +devtools.profiler.Processor.prototype.createProfileForView = function() +{ + var profile = this.viewBuilder_.buildView(this.currentProfile_.getTopDownProfile()); + profile.uid = this.profileId_++; + profile.title = UserInitiatedProfileName + "." + profile.uid; + return profile; +}; diff --git a/webkit/glue/devtools/js/tests.js b/webkit/glue/devtools/js/tests.js new file mode 100644 index 0000000..ba832c9 --- /dev/null +++ b/webkit/glue/devtools/js/tests.js @@ -0,0 +1,1861 @@ +/* + * Copyright (C) 2010 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + + +/** + * @fileoverview This file contains small testing framework along with the + * test suite for the frontend. These tests are a part of the continues build + * and are executed by the devtools_sanity_unittest.cc as a part of the + * Interactive UI Test suite. + */ + +if (window.domAutomationController) { + +var ___interactiveUiTestsMode = true; + +/** + * Test suite for interactive UI tests. + * @constructor + */ +TestSuite = function() +{ + this.controlTaken_ = false; + this.timerId_ = -1; +}; + + +/** + * Reports test failure. + * @param {string} message Failure description. + */ +TestSuite.prototype.fail = function(message) +{ + if (this.controlTaken_) + this.reportFailure_(message); + else + throw message; +}; + + +/** + * Equals assertion tests that expected === actual. + * @param {Object} expected Expected object. + * @param {Object} actual Actual object. + * @param {string} opt_message User message to print if the test fails. + */ +TestSuite.prototype.assertEquals = function(expected, actual, opt_message) +{ + if (expected !== actual) { + var message = "Expected: '" + expected + "', but was '" + actual + "'"; + if (opt_message) + message = opt_message + "(" + message + ")"; + this.fail(message); + } +}; + + +/** + * True assertion tests that value == true. + * @param {Object} value Actual object. + * @param {string} opt_message User message to print if the test fails. + */ +TestSuite.prototype.assertTrue = function(value, opt_message) +{ + this.assertEquals(true, !!value, opt_message); +}; + + +/** + * Contains assertion tests that string contains substring. + * @param {string} string Outer. + * @param {string} substring Inner. + */ +TestSuite.prototype.assertContains = function(string, substring) +{ + if (string.indexOf(substring) === -1) + this.fail("Expected to: '" + string + "' to contain '" + substring + "'"); +}; + + +/** + * Takes control over execution. + */ +TestSuite.prototype.takeControl = function() +{ + this.controlTaken_ = true; + // Set up guard timer. + var self = this; + this.timerId_ = setTimeout(function() { + self.reportFailure_("Timeout exceeded: 20 sec"); + }, 20000); +}; + + +/** + * Releases control over execution. + */ +TestSuite.prototype.releaseControl = function() +{ + if (this.timerId_ !== -1) { + clearTimeout(this.timerId_); + this.timerId_ = -1; + } + this.reportOk_(); +}; + + +/** + * Async tests use this one to report that they are completed. + */ +TestSuite.prototype.reportOk_ = function() +{ + window.domAutomationController.send("[OK]"); +}; + + +/** + * Async tests use this one to report failures. + */ +TestSuite.prototype.reportFailure_ = function(error) +{ + if (this.timerId_ !== -1) { + clearTimeout(this.timerId_); + this.timerId_ = -1; + } + window.domAutomationController.send("[FAILED] " + error); +}; + + +/** + * Runs all global functions starting with "test" as unit tests. + */ +TestSuite.prototype.runTest = function(testName) +{ + try { + this[testName](); + if (!this.controlTaken_) + this.reportOk_(); + } catch (e) { + this.reportFailure_(e); + } +}; + + +/** + * @param {string} panelName Name of the panel to show. + */ +TestSuite.prototype.showPanel = function(panelName) +{ + // Open Scripts panel. + var toolbar = document.getElementById("toolbar"); + var button = toolbar.getElementsByClassName(panelName)[0]; + button.click(); + this.assertEquals(WebInspector.panels[panelName], WebInspector.currentPanel); +}; + + +/** + * Overrides the method with specified name until it's called first time. + * @param {Object} receiver An object whose method to override. + * @param {string} methodName Name of the method to override. + * @param {Function} override A function that should be called right after the + * overriden method returns. + * @param {boolean} opt_sticky Whether restore original method after first run + * or not. + */ +TestSuite.prototype.addSniffer = function(receiver, methodName, override, opt_sticky) +{ + var orig = receiver[methodName]; + if (typeof orig !== "function") + this.fail("Cannot find method to override: " + methodName); + var test = this; + receiver[methodName] = function(var_args) { + try { + var result = orig.apply(this, arguments); + } finally { + if (!opt_sticky) + receiver[methodName] = orig; + } + // In case of exception the override won't be called. + try { + override.apply(this, arguments); + } catch (e) { + test.fail("Exception in overriden method '" + methodName + "': " + e); + } + return result; + }; +}; + + +// UI Tests + + +/** + * Tests that the real injected host is present in the context. + */ +TestSuite.prototype.testHostIsPresent = function() +{ + this.assertTrue(typeof InspectorFrontendHost === "object" && !InspectorFrontendHost.isStub); +}; + + +/** + * Tests elements tree has an "HTML" root. + */ +TestSuite.prototype.testElementsTreeRoot = function() +{ + var doc = WebInspector.domAgent.document; + this.assertEquals("HTML", doc.documentElement.nodeName); + this.assertTrue(doc.documentElement.hasChildNodes()); +}; + + +/** + * Tests that main resource is present in the system and that it is + * the only resource. + */ +TestSuite.prototype.testMainResource = function() +{ + var tokens = []; + var resources = WebInspector.resources; + for (var id in resources) + tokens.push(resources[id].lastPathComponent); + this.assertEquals("simple_page.html", tokens.join(",")); +}; + + +/** + * Tests that resources tab is enabled when corresponding item is selected. + */ +TestSuite.prototype.testEnableResourcesTab = function() +{ + this.showPanel("resources"); + + var test = this; + this.addSniffer(WebInspector, "updateResource", + function(identifier, payload) { + test.assertEquals("simple_page.html", payload.lastPathComponent); + WebInspector.panels.resources.refresh(); + WebInspector.panels.resources.revealAndSelectItem(WebInspector.resources[identifier]); + + test.releaseControl(); + }); + + // Following call should lead to reload that we capture in the + // addResource override. + WebInspector.panels.resources._enableResourceTracking(); + + // We now have some time to report results to controller. + this.takeControl(); +}; + + +/** + * Tests that correct content length is reported for resources. + */ +TestSuite.prototype.testResourceContentLength = function() +{ + this.showPanel("resources"); + var test = this; + + var png = false; + var html = false; + this.addSniffer(WebInspector, "updateResource", + function(identifier, payload) { + if (!payload.didLengthChange) + return; + var resource = WebInspector.resources[identifier]; + if (!resource || !resource.url) + return; + if (resource.url.search("image.html$") !== -1) { + var expectedLength = 87; + test.assertTrue( + resource.contentLength <= expectedLength, + "image.html content length is greater thatn expected."); + if (expectedLength === resource.contentLength) + html = true; + } else if (resource.url.search("image.png") !== -1) { + var expectedLength = 257796; + test.assertTrue( + resource.contentLength <= expectedLength, + "image.png content length is greater than expected."); + if (expectedLength === resource.contentLength) + png = true; + } + if (html && png) { + // Wait 1 second before releasing control to check that the content + // lengths are not updated anymore. + setTimeout(function() { + test.releaseControl(); + }, 1000); + } + }, true); + + // Make sure resource tracking is on. + WebInspector.panels.resources._enableResourceTracking(); + // Reload inspected page to update all resources. + test.evaluateInConsole_( + "window.location.reload(true);", + function(resultText) { + test.assertEquals("undefined", resultText, "Unexpected result of reload()."); + }); + + // We now have some time to report results to controller. + this.takeControl(); +}; + + +/** + * Tests resource headers. + */ +TestSuite.prototype.testResourceHeaders = function() +{ + this.showPanel("resources"); + + var test = this; + + var responseOk = false; + var timingOk = false; + + this.addSniffer(WebInspector, "updateResource", + function(identifier, payload) { + var resource = this.resources[identifier]; + if (!resource || resource.mainResource) { + // We are only interested in secondary resources in this test. + return; + } + + var requestHeaders = JSON.stringify(resource.requestHeaders); + test.assertContains(requestHeaders, "Accept"); + + if (payload.didResponseChange) { + var responseHeaders = JSON.stringify(resource.responseHeaders); + test.assertContains(responseHeaders, "Content-type"); + test.assertContains(responseHeaders, "Content-Length"); + test.assertTrue(typeof resource.responseReceivedTime !== "undefined"); + responseOk = true; + } + + if (payload.didTimingChange) { + test.assertTrue(typeof resource.startTime !== "undefined"); + timingOk = true; + } + + if (payload.didCompletionChange) { + test.assertTrue(responseOk); + test.assertTrue(timingOk); + test.assertTrue(typeof resource.endTime !== "undefined"); + test.releaseControl(); + } + }, true); + + WebInspector.panels.resources._enableResourceTracking(); + this.takeControl(); +}; + + +/** + * Tests the mime type of a cached (HTTP 304) resource. + */ +TestSuite.prototype.testCachedResourceMimeType = function() +{ + this.showPanel("resources"); + + var test = this; + var hasReloaded = false; + + this.addSniffer(WebInspector, "updateResource", + function(identifier, payload) { + var resource = this.resources[identifier]; + if (!resource || resource.mainResource) { + // We are only interested in secondary resources in this test. + return; + } + + if (payload.didResponseChange) { + // Test server uses a default mime type for JavaScript files. + test.assertEquals("text/html", payload.mimeType); + if (!hasReloaded) { + hasReloaded = true; + // Reload inspected page to update all resources. + test.evaluateInConsole_("window.location.reload(true);", function() {}); + } else + test.releaseControl(); + } + + }, true); + + WebInspector.panels.resources._enableResourceTracking(); + this.takeControl(); +}; + + +/** + * Tests that profiler works. + */ +TestSuite.prototype.testProfilerTab = function() +{ + this.showPanel("profiles"); + + var test = this; + this.addSniffer(WebInspector.panels.profiles, "addProfileHeader", + function(typeOrProfile, profile) { + if (!profile) + profile = typeOrProfile; + var panel = WebInspector.panels.profiles; + panel.showProfile(profile); + var node = panel.visibleView.profileDataGridTree.children[0]; + // Iterate over displayed functions and search for a function + // that is called "fib" or "eternal_fib". If found, it will mean + // that we actually have profiled page's code. + while (node) { + if (node.functionName.indexOf("fib") !== -1) + test.releaseControl(); + node = node.traverseNextNode(true, null, true); + } + + test.fail(); + }); + var ticksCount = 0; + var tickRecord = "\nt,"; + this.addSniffer(RemoteProfilerAgent, "didGetLogLines", + function(posIgnored, log) { + var pos = 0; + while ((pos = log.indexOf(tickRecord, pos)) !== -1) { + pos += tickRecord.length; + ticksCount++; + } + if (ticksCount > 100) + InspectorBackend.stopProfiling(); + }, true); + + InspectorBackend.startProfiling(); + this.takeControl(); +}; + + +/** + * Tests that scripts tab can be open and populated with inspected scripts. + */ +TestSuite.prototype.testShowScriptsTab = function() +{ + this.showPanel("scripts"); + var test = this; + // There should be at least main page script. + this._waitUntilScriptsAreParsed(["debugger_test_page.html$"], + function() { + test.releaseControl(); + }); + // Wait until all scripts are added to the debugger. + this.takeControl(); +}; + + +/** + * Tests that scripts tab is populated with inspected scripts even if it + * hadn't been shown by the moment inspected paged refreshed. + * @see http://crbug.com/26312 + */ +TestSuite.prototype.testScriptsTabIsPopulatedOnInspectedPageRefresh = function() +{ + var test = this; + this.assertEquals(WebInspector.panels.elements, WebInspector.currentPanel, "Elements panel should be current one."); + + this.addSniffer(devtools.DebuggerAgent.prototype, "reset", waitUntilScriptIsParsed); + + // Reload inspected page. It will reset the debugger agent. + test.evaluateInConsole_( + "window.location.reload(true);", + function(resultText) { + test.assertEquals("undefined", resultText, "Unexpected result of reload()."); + }); + + function waitUntilScriptIsParsed() { + var parsed = devtools.tools.getDebuggerAgent().parsedScripts_; + for (var id in parsed) { + var url = parsed[id].getUrl(); + if (url && url.search("debugger_test_page.html$") !== -1) { + checkScriptsPanel(); + return; + } + } + test.addSniffer(devtools.DebuggerAgent.prototype, "addScriptInfo_", waitUntilScriptIsParsed); + } + + function checkScriptsPanel() { + test.showPanel("scripts"); + test.assertTrue(test._scriptsAreParsed(["debugger_test_page.html$"]), "Inspected script not found in the scripts list"); + test.releaseControl(); + } + + // Wait until all scripts are added to the debugger. + this.takeControl(); +}; + + +/** + * Tests that scripts list contains content scripts. + */ +TestSuite.prototype.testContentScriptIsPresent = function() +{ + this.showPanel("scripts"); + var test = this; + + test._waitUntilScriptsAreParsed( + ["page_with_content_script.html$", "simple_content_script.js$"], + function() { + test.releaseControl(); + }); + + // Wait until all scripts are added to the debugger. + this.takeControl(); +}; + + +/** + * Tests that scripts are not duplicaed on Scripts tab switch. + */ +TestSuite.prototype.testNoScriptDuplicatesOnPanelSwitch = function() +{ + var test = this; + + // There should be two scripts: one for the main page and another + // one which is source of console API(see + // InjectedScript._ensureCommandLineAPIInstalled). + var expectedScriptsCount = 2; + var parsedScripts = []; + + this.showPanel("scripts"); + + + function switchToElementsTab() { + test.showPanel("elements"); + setTimeout(switchToScriptsTab, 0); + } + + function switchToScriptsTab() { + test.showPanel("scripts"); + setTimeout(checkScriptsPanel, 0); + } + + function checkScriptsPanel() { + test.assertTrue(!!WebInspector.panels.scripts.visibleView, "No visible script view."); + test.assertTrue(test._scriptsAreParsed(["debugger_test_page.html$"]), "Some scripts are missing."); + checkNoDuplicates(); + test.releaseControl(); + } + + function checkNoDuplicates() { + var scriptSelect = document.getElementById("scripts-files"); + var options = scriptSelect.options; + for (var i = 0; i < options.length; i++) { + var scriptName = options[i].text; + for (var j = i + 1; j < options.length; j++) + test.assertTrue(scriptName !== options[j].text, "Found script duplicates: " + test.optionsToString_(options)); + } + } + + test._waitUntilScriptsAreParsed( + ["debugger_test_page.html$"], + function() { + checkNoDuplicates(); + setTimeout(switchToElementsTab, 0); + }); + + + // Wait until all scripts are added to the debugger. + this.takeControl(); +}; + + +/** + * Tests that a breakpoint can be set. + */ +TestSuite.prototype.testSetBreakpoint = function() +{ + var test = this; + this.showPanel("scripts"); + + var breakpointLine = 12; + + this._waitUntilScriptsAreParsed(["debugger_test_page.html"], + function() { + test.showMainPageScriptSource_( + "debugger_test_page.html", + function(view, url) { + view._addBreakpoint(breakpointLine); + // Force v8 execution. + RemoteToolsAgent.executeVoidJavaScript(); + test.waitForSetBreakpointResponse_(url, breakpointLine, + function() { + test.releaseControl(); + }); + }); + }); + + this.takeControl(); +}; + + +/** + * Tests that pause on exception works. + */ +TestSuite.prototype.testPauseOnException = function() +{ + this.showPanel("scripts"); + var test = this; + + // TODO(yurys): remove else branch once the states are supported. + if (WebInspector.ScriptsPanel.PauseOnExceptionsState) { + while (WebInspector.currentPanel.pauseOnExceptionButton.state !== WebInspector.ScriptsPanel.PauseOnExceptionsState.PauseOnUncaughtExceptions) + WebInspector.currentPanel.pauseOnExceptionButton.element.click(); + } else { + // Make sure pause on exceptions is on. + if (!WebInspector.currentPanel.pauseOnExceptionButton.toggled) + WebInspector.currentPanel.pauseOnExceptionButton.element.click(); + } + + this._executeCodeWhenScriptsAreParsed("handleClick()", ["pause_on_exception.html$"]); + + this._waitForScriptPause( + { + functionsOnStack: ["throwAnException", "handleClick", "(anonymous function)"], + lineNumber: 6, + lineText: " return unknown_var;" + }, + function() { + test.releaseControl(); + }); + + this.takeControl(); +}; + + +// Tests that debugger works correctly if pause event occurs when DevTools +// frontend is being loaded. +TestSuite.prototype.testPauseWhenLoadingDevTools = function() +{ + this.showPanel("scripts"); + var test = this; + + var expectations = { + functionsOnStack: ["callDebugger"], + lineNumber: 8, + lineText: " debugger;" + }; + + + // Script execution can already be paused. + if (WebInspector.currentPanel.paused) { + var callFrame = WebInspector.currentPanel.sidebarPanes.callstack.selectedCallFrame; + this.assertEquals(expectations.functionsOnStack[0], callFrame.functionName); + var callbackInvoked = false; + this._checkSourceFrameWhenLoaded(expectations, function() { + callbackInvoked = true; + if (test.controlTaken_) + test.releaseControl(); + }); + if (!callbackInvoked) { + test.takeControl(); + } + return; + } + + this._waitForScriptPause( + { + functionsOnStack: ["callDebugger"], + lineNumber: 8, + lineText: " debugger;" + }, + function() { + test.releaseControl(); + }); + this.takeControl(); +}; + + +// Tests that pressing "Pause" will pause script execution if the script +// is already running. +TestSuite.prototype.testPauseWhenScriptIsRunning = function() +{ + this.showPanel("scripts"); + var test = this; + + test.evaluateInConsole_( + 'setTimeout("handleClick()" , 0)', + function(resultText) { + test.assertTrue(!isNaN(resultText), "Failed to get timer id: " + resultText); + testScriptPauseAfterDelay(); + }); + + // Wait for some time to make sure that inspected page is running the + // infinite loop. + function testScriptPauseAfterDelay() { + setTimeout(testScriptPause, 300); + } + + function testScriptPause() { + // The script should be in infinite loop. Click "Pause" button to + // pause it and wait for the result. + WebInspector.panels.scripts.pauseButton.click(); + + test._waitForScriptPause( + { + functionsOnStack: ["handleClick", "(anonymous function)"], + lineNumber: 5, + lineText: " while(true) {" + }, + function() { + test.releaseControl(); + }); + } + + this.takeControl(); +}; + + +/** + * Serializes options collection to string. + * @param {HTMLOptionsCollection} options + * @return {string} + */ +TestSuite.prototype.optionsToString_ = function(options) +{ + var names = []; + for (var i = 0; i < options.length; i++) + names.push('"' + options[i].text + '"'); + return names.join(","); +}; + + +/** + * Ensures that main HTML resource is selected in Scripts panel and that its + * source frame is setup. Invokes the callback when the condition is satisfied. + * @param {HTMLOptionsCollection} options + * @param {function(WebInspector.SourceView,string)} callback + */ +TestSuite.prototype.showMainPageScriptSource_ = function(scriptName, callback) +{ + var test = this; + + var scriptSelect = document.getElementById("scripts-files"); + var options = scriptSelect.options; + + test.assertTrue(options.length, "Scripts list is empty"); + + // Select page's script if it's not current option. + var scriptResource; + if (options[scriptSelect.selectedIndex].text === scriptName) + scriptResource = options[scriptSelect.selectedIndex].representedObject; + else { + var pageScriptIndex = -1; + for (var i = 0; i < options.length; i++) { + if (options[i].text === scriptName) { + pageScriptIndex = i; + break; + } + } + test.assertTrue(-1 !== pageScriptIndex, "Script with url " + scriptName + " not found among " + test.optionsToString_(options)); + scriptResource = options[pageScriptIndex].representedObject; + + // Current panel is "Scripts". + WebInspector.currentPanel._showScriptOrResource(scriptResource); + test.assertEquals(pageScriptIndex, scriptSelect.selectedIndex, "Unexpected selected option index."); + } + + test.assertTrue(scriptResource instanceof WebInspector.Resource, + "Unexpected resource class."); + test.assertTrue(!!scriptResource.url, "Resource URL is null."); + test.assertTrue(scriptResource.url.search(scriptName + "$") !== -1, "Main HTML resource should be selected."); + + var scriptsPanel = WebInspector.panels.scripts; + + var view = scriptsPanel.visibleView; + test.assertTrue(view instanceof WebInspector.SourceView); + + if (!view.sourceFrame._loaded) { + test.addSniffer(view, "_sourceFrameSetupFinished", function(event) { + callback(view, scriptResource.url); + }); + } else + callback(view, scriptResource.url); +}; + + +/* + * Evaluates the code in the console as if user typed it manually and invokes + * the callback when the result message is received and added to the console. + * @param {string} code + * @param {function(string)} callback + */ +TestSuite.prototype.evaluateInConsole_ = function(code, callback) +{ + WebInspector.console.visible = true; + WebInspector.console.prompt.text = code; + WebInspector.console.promptElement.dispatchEvent( TestSuite.createKeyEvent("Enter")); + + this.addSniffer(WebInspector.ConsoleView.prototype, "addMessage", + function(commandResult) { + callback(commandResult.toMessageElement().textContent); + }); +}; + + +/* + * Waits for "setbreakpoint" response, checks that corresponding breakpoint + * was successfully set and invokes the callback if it was. + * @param {string} scriptUrl + * @param {number} breakpointLine + * @param {function()} callback + */ +TestSuite.prototype.waitForSetBreakpointResponse_ = function(scriptUrl, breakpointLine, callback) +{ + var test = this; + test.addSniffer( + devtools.DebuggerAgent.prototype, + "handleSetBreakpointResponse_", + function(msg) { + var bps = this.urlToBreakpoints_[scriptUrl]; + test.assertTrue(!!bps, "No breakpoints for line " + breakpointLine); + var line = devtools.DebuggerAgent.webkitToV8LineNumber_(breakpointLine); + test.assertTrue(!!bps[line].getV8Id(), "Breakpoint id was not assigned."); + callback(); + }); +}; + + +/** + * Tests eval on call frame. + */ +TestSuite.prototype.testEvalOnCallFrame = function() +{ + this.showPanel("scripts"); + + var breakpointLine = 16; + + var test = this; + this.addSniffer(devtools.DebuggerAgent.prototype, "handleScriptsResponse_", + function(msg) { + test.showMainPageScriptSource_( + "debugger_test_page.html", + function(view, url) { + view._addBreakpoint(breakpointLine); + // Force v8 execution. + RemoteToolsAgent.executeVoidJavaScript(); + test.waitForSetBreakpointResponse_(url, breakpointLine, setBreakpointCallback); + }); + }); + + function setBreakpointCallback() { + // Since breakpoints are ignored in evals' calculate() function is + // execute after zero-timeout so that the breakpoint is hit. + test.evaluateInConsole_( + 'setTimeout("calculate(123)" , 0)', + function(resultText) { + test.assertTrue(!isNaN(resultText), "Failed to get timer id: " + resultText); + waitForBreakpointHit(); + }); + } + + function waitForBreakpointHit() { + test.addSniffer( + devtools.DebuggerAgent.prototype, + "handleBacktraceResponse_", + function(msg) { + test.assertEquals(2, this.callFrames_.length, "Unexpected stack depth on the breakpoint. " + JSON.stringify(msg)); + test.assertEquals("calculate", this.callFrames_[0].functionName, "Unexpected top frame function."); + // Evaluate "e+1" where "e" is an argument of "calculate" function. + test.evaluateInConsole_( + "e+1", + function(resultText) { + test.assertEquals("124", resultText, 'Unexpected "e+1" value.'); + test.releaseControl(); + }); + }); + } + + this.takeControl(); +}; + + +/** + * Tests that console auto completion works when script execution is paused. + */ +TestSuite.prototype.testCompletionOnPause = function() +{ + this.showPanel("scripts"); + var test = this; + this._executeCodeWhenScriptsAreParsed("handleClick()", ["completion_on_pause.html$"]); + + this._waitForScriptPause( + { + functionsOnStack: ["innerFunction", "handleClick", "(anonymous function)"], + lineNumber: 9, + lineText: " debugger;" + }, + showConsole); + + function showConsole() { + test.addSniffer(WebInspector.console, "afterShow", testLocalsCompletion); + WebInspector.showConsole(); + } + + function testLocalsCompletion() { + checkCompletions("th", ["parameter1", "closureLocal", "p", "createClosureLocal"], testThisCompletion); + } + + function testThisCompletion() { + checkCompletions("this.", ["field1", "field2", "m"], testFieldCompletion); + } + + function testFieldCompletion() { + checkCompletions("this.field1.", ["id", "name"], function() { test.releaseControl(); }); + } + + function checkCompletions(expression, expectedProperties, callback) { + test.addSniffer(WebInspector.console, "_reportCompletions", + function(bestMatchOnly, completionsReadyCallback, dotNotation, bracketNotation, prefix, result, isException) { + test.assertTrue(!isException, "Exception while collecting completions"); + for (var i = 0; i < expectedProperties.length; i++) { + var name = expectedProperties[i]; + test.assertTrue(result[name], "Name " + name + " not found among the completions: " + JSON.stringify(result)); + } + setTimeout(callback, 0); + }); + WebInspector.console.prompt.text = expression; + WebInspector.console.prompt.autoCompleteSoon(); + } + + this.takeControl(); +}; + + +/** + * Tests that inspected page doesn't hang on reload if it contains a syntax + * error and DevTools window is open. + */ +TestSuite.prototype.testAutoContinueOnSyntaxError = function() +{ + this.showPanel("scripts"); + var test = this; + + function checkScriptsList() { + var scriptSelect = document.getElementById("scripts-files"); + var options = scriptSelect.options; + // There should be only console API source (see + // InjectedScript._ensureCommandLineAPIInstalled) since the page script + // contains a syntax error. + for (var i = 0 ; i < options.length; i++) { + if (options[i].text.search("script_syntax_error.html$") !== -1) + test.fail("Script with syntax error should not be in the list of parsed scripts."); + } + } + + this.addSniffer(devtools.DebuggerAgent.prototype, "handleScriptsResponse_", + function(msg) { + checkScriptsList(); + + // Reload inspected page. + test.evaluateInConsole_( + "window.location.reload(true);", + function(resultText) { + test.assertEquals("undefined", resultText, "Unexpected result of reload()."); + waitForExceptionEvent(); + }); + }); + + function waitForExceptionEvent() { + var exceptionCount = 0; + test.addSniffer( + devtools.DebuggerAgent.prototype, + "handleExceptionEvent_", + function(msg) { + exceptionCount++; + test.assertEquals(1, exceptionCount, "Too many exceptions."); + test.assertEquals(undefined, msg.getBody().script, "Unexpected exception: " + JSON.stringify(msg)); + test.releaseControl(); + }); + + // Check that the script is not paused on parse error. + test.addSniffer( + WebInspector, + "pausedScript", + function(callFrames) { + test.fail("Script execution should not pause on syntax error."); + }); + } + + this.takeControl(); +}; + + +/** + * Checks current execution line against expectations. + * @param {WebInspector.SourceFrame} sourceFrame + * @param {number} lineNumber Expected line number + * @param {string} lineContent Expected line text + */ +TestSuite.prototype._checkExecutionLine = function(sourceFrame, lineNumber, lineContent) +{ + this.assertEquals(lineNumber, sourceFrame.executionLine, "Unexpected execution line number."); + this.assertEquals(lineContent, sourceFrame._textModel.line(lineNumber - 1), "Unexpected execution line text."); +} + + +/** + * Checks that all expected scripts are present in the scripts list + * in the Scripts panel. + * @param {Array.<string>} expected Regular expressions describing + * expected script names. + * @return {boolean} Whether all the scripts are in "scripts-files" select + * box + */ +TestSuite.prototype._scriptsAreParsed = function(expected) +{ + var scriptSelect = document.getElementById("scripts-files"); + var options = scriptSelect.options; + + // Check that at least all the expected scripts are present. + var missing = expected.slice(0); + for (var i = 0 ; i < options.length; i++) { + for (var j = 0; j < missing.length; j++) { + if (options[i].text.search(missing[j]) !== -1) { + missing.splice(j, 1); + break; + } + } + } + return missing.length === 0; +}; + + +/** + * Waits for script pause, checks expectations, and invokes the callback. + * @param {Object} expectations Dictionary of expectations + * @param {function():void} callback + */ +TestSuite.prototype._waitForScriptPause = function(expectations, callback) +{ + var test = this; + // Wait until script is paused. + test.addSniffer( + WebInspector, + "pausedScript", + function(callFrames) { + var functionsOnStack = []; + for (var i = 0; i < callFrames.length; i++) + functionsOnStack.push(callFrames[i].functionName); + + test.assertEquals(expectations.functionsOnStack.join(","), functionsOnStack.join(","), "Unexpected stack."); + + // Check that execution line where the script is paused is + // expected one. + test._checkSourceFrameWhenLoaded(expectations, callback); + }); +}; + + +/** + * Waits for current source frame to load, checks expectations, and invokes + * the callback. + * @param {Object} expectations Dictionary of expectations + * @param {function():void} callback + */ +TestSuite.prototype._checkSourceFrameWhenLoaded = function(expectations, callback) +{ + var test = this; + + var frame = WebInspector.currentPanel.visibleView.sourceFrame; + if (frame._loaded) + checkExecLine(); + else { + setTimeout(function() { + test._checkSourceFrameWhenLoaded(expectations, callback); + }, 100); + } + function checkExecLine() { + test._checkExecutionLine(frame, expectations.lineNumber, expectations.lineText); + callback(); + } +}; + + +/** + * Performs sequence of steps. + * @param {Array.<Object|Function>} Array [expectations1,action1,expectations2, + * action2,...,actionN]. + */ +TestSuite.prototype._performSteps = function(actions) +{ + var test = this; + var i = 0; + function doNextAction() { + if (i > 0) + actions[i++](); + if (i < actions.length - 1) + test._waitForScriptPause(actions[i++], doNextAction); + } + doNextAction(); +}; + + +/** + * Waits until all the scripts are parsed and asynchronously executes the code + * in the inspected page. + */ +TestSuite.prototype._executeCodeWhenScriptsAreParsed = function(code, expectedScripts) +{ + var test = this; + + function executeFunctionInInspectedPage() { + // Since breakpoints are ignored in evals' calculate() function is + // execute after zero-timeout so that the breakpoint is hit. + test.evaluateInConsole_( + 'setTimeout("' + code + '" , 0)', + function(resultText) { + test.assertTrue(!isNaN(resultText), "Failed to get timer id: " + resultText); + }); + } + + test._waitUntilScriptsAreParsed(expectedScripts, executeFunctionInInspectedPage); +}; + + +/** + * Waits until all the scripts are parsed and invokes the callback. + */ +TestSuite.prototype._waitUntilScriptsAreParsed = function(expectedScripts, callback) +{ + var test = this; + + function waitForAllScripts() { + if (test._scriptsAreParsed(expectedScripts)) + callback(); + else + test.addSniffer(WebInspector, "parsedScriptSource", waitForAllScripts); + } + + waitForAllScripts(); +}; + + +/** + * Waits until all debugger scripts are parsed and executes "a()" in the + * inspected page. + */ +TestSuite.prototype._executeFunctionForStepTest = function() +{ + this._executeCodeWhenScriptsAreParsed("a()", ["debugger_step.html$", "debugger_step.js$"]); +}; + + +/** + * Tests step over in the debugger. + */ +TestSuite.prototype.testStepOver = function() +{ + this.showPanel("scripts"); + var test = this; + + this._executeFunctionForStepTest(); + + this._performSteps([ + { + functionsOnStack: ["d","a","(anonymous function)"], + lineNumber: 3, + lineText: " debugger;" + }, + function() { + document.getElementById("scripts-step-over").click(); + }, + { + functionsOnStack: ["d","a","(anonymous function)"], + lineNumber: 5, + lineText: " var y = fact(10);" + }, + function() { + document.getElementById("scripts-step-over").click(); + }, + { + functionsOnStack: ["d","a","(anonymous function)"], + lineNumber: 6, + lineText: " return y;" + }, + function() { + test.releaseControl(); + } + ]); + + test.takeControl(); +}; + + +/** + * Tests step out in the debugger. + */ +TestSuite.prototype.testStepOut = function() +{ + this.showPanel("scripts"); + var test = this; + + this._executeFunctionForStepTest(); + + this._performSteps([ + { + functionsOnStack: ["d","a","(anonymous function)"], + lineNumber: 3, + lineText: " debugger;" + }, + function() { + document.getElementById("scripts-step-out").click(); + }, + { + functionsOnStack: ["a","(anonymous function)"], + lineNumber: 8, + lineText: " printResult(result);" + }, + function() { + test.releaseControl(); + } + ]); + + test.takeControl(); +}; + + +/** + * Tests step in in the debugger. + */ +TestSuite.prototype.testStepIn = function() +{ + this.showPanel("scripts"); + var test = this; + + this._executeFunctionForStepTest(); + + this._performSteps([ + { + functionsOnStack: ["d","a","(anonymous function)"], + lineNumber: 3, + lineText: " debugger;" + }, + function() { + document.getElementById("scripts-step-over").click(); + }, + { + functionsOnStack: ["d","a","(anonymous function)"], + lineNumber: 5, + lineText: " var y = fact(10);" + }, + function() { + document.getElementById("scripts-step-into").click(); + }, + { + functionsOnStack: ["fact","d","a","(anonymous function)"], + lineNumber: 15, + lineText: " return r;" + }, + function() { + test.releaseControl(); + } + ]); + + test.takeControl(); +}; + + +/** + * Gets a XPathResult matching given xpath. + * @param {string} xpath + * @param {number} resultType + * @param {Node} opt_ancestor Context node. If not specified documentElement + * will be used + * @return {XPathResult} Type of returned value is determined by "resultType" parameter + */ + +TestSuite.prototype._evaluateXpath = function(xpath, resultType, opt_ancestor) +{ + if (!opt_ancestor) + opt_ancestor = document.documentElement; + try { + return document.evaluate(xpath, opt_ancestor, null, resultType, null); + } catch(e) { + this.fail('Error in expression: "' + xpath + '".' + e); + } +}; + + +/** + * Gets first Node matching given xpath. + * @param {string} xpath + * @param {Node} opt_ancestor Context node. If not specified documentElement + * will be used + * @return {?Node} + */ +TestSuite.prototype._findNode = function(xpath, opt_ancestor) +{ + var result = this._evaluateXpath(xpath, XPathResult.FIRST_ORDERED_NODE_TYPE, opt_ancestor).singleNodeValue; + this.assertTrue(!!result, "Cannot find node on path: " + xpath); + return result; +}; + + +/** + * Gets a text matching given xpath. + * @param {string} xpath + * @param {Node} opt_ancestor Context node. If not specified documentElement + * will be used + * @return {?string} + */ +TestSuite.prototype._findText = function(xpath, opt_ancestor) +{ + var result = this._evaluateXpath(xpath, XPathResult.STRING_TYPE, opt_ancestor).stringValue; + this.assertTrue(!!result, "Cannot find text on path: " + xpath); + return result; +}; + + +/** + * Gets an iterator over nodes matching given xpath. + * @param {string} xpath + * @param {Node} opt_ancestor Context node. If not specified, documentElement + * will be used + * @return {XPathResult} Iterator over the nodes + */ +TestSuite.prototype._nodeIterator = function(xpath, opt_ancestor) +{ + return this._evaluateXpath(xpath, XPathResult.ORDERED_NODE_ITERATOR_TYPE, opt_ancestor); +}; + + +/** + * Checks the scopeSectionDiv against the expectations. + * @param {Node} scopeSectionDiv The section div + * @param {Object} expectations Expectations dictionary + */ +TestSuite.prototype._checkScopeSectionDiv = function(scopeSectionDiv, expectations) +{ + var scopeTitle = this._findText('./div[@class="header"]/div[@class="title"]/text()', scopeSectionDiv); + this.assertEquals(expectations.title, scopeTitle, "Unexpected scope section title."); + if (!expectations.properties) + return; + this.assertTrue(scopeSectionDiv.hasStyleClass("expanded"), 'Section "' + scopeTitle + '" is collapsed.'); + + var propertyIt = this._nodeIterator("./ol/li", scopeSectionDiv); + var propertyLi; + var foundProps = []; + while (propertyLi = propertyIt.iterateNext()) { + var name = this._findText('./span[@class="name"]/text()', propertyLi); + var value = this._findText('./span[@class="value"]/text()', propertyLi); + this.assertTrue(!!name, 'Invalid variable name: "' + name + '"'); + this.assertTrue(name in expectations.properties, "Unexpected property: " + name); + this.assertEquals(expectations.properties[name], value, 'Unexpected "' + name + '" property value.'); + delete expectations.properties[name]; + foundProps.push(name + " = " + value); + } + + // Check that all expected properties were found. + for (var p in expectations.properties) + this.fail('Property "' + p + '" was not found in scope "' + scopeTitle + '". Found properties: "' + foundProps.join(",") + '"'); +}; + + +/** + * Expands scope sections matching the filter and invokes the callback on + * success. + * @param {function(WebInspector.ObjectPropertiesSection, number):boolean} + * filter + * @param {Function} callback + */ +TestSuite.prototype._expandScopeSections = function(filter, callback) +{ + var sections = WebInspector.currentPanel.sidebarPanes.scopechain.sections; + + var toBeUpdatedCount = 0; + function updateListener() { + --toBeUpdatedCount; + if (toBeUpdatedCount === 0) { + // Report when all scopes are expanded and populated. + callback(); + } + } + + // Global scope is always the last one. + for (var i = 0; i < sections.length - 1; i++) { + var section = sections[i]; + if (!filter(sections, i)) + continue; + ++toBeUpdatedCount; + var populated = section.populated; + + this._hookGetPropertiesCallback(updateListener, + function() { + section.expand(); + if (populated) { + // Make sure "updateProperties" callback will be called at least once + // after it was overridden. + section.update(); + } + }); + } +}; + + +/** + * Tests that scopes can be expanded and contain expected data. + */ +TestSuite.prototype.testExpandScope = function() +{ + this.showPanel("scripts"); + var test = this; + + this._executeCodeWhenScriptsAreParsed("handleClick()", ["debugger_closure.html$"]); + + this._waitForScriptPause( + { + functionsOnStack: ["innerFunction", "handleClick", "(anonymous function)"], + lineNumber: 8, + lineText: " debugger;" + }, + expandAllSectionsExceptGlobal); + + // Expanding Global scope takes for too long so we skeep it. + function expandAllSectionsExceptGlobal() { + test._expandScopeSections(function(sections, i) { + return i < sections.length - 1; + }, + examineScopes /* When all scopes are expanded and populated check them. */); + } + + // Check scope sections contents. + function examineScopes() { + var scopeVariablesSection = test._findNode('//div[@id="scripts-sidebar"]/div[div[@class="title"]/text()="Scope Variables"]'); + var expectedScopes = [ + { + title: "Local", + properties: { + x:"2009", + innerFunctionLocalVar:"2011", + "this": "global", + } + }, + { + title: "Closure", + properties: { + n:"TextParam", + makeClosureLocalVar:"local.TextParam", + } + }, + { + title: "Global", + }, + ]; + var it = test._nodeIterator('./div[@class="body"]/div', scopeVariablesSection); + var scopeIndex = 0; + var scopeDiv; + while (scopeDiv = it.iterateNext()) { + test.assertTrue(scopeIndex < expectedScopes.length, "Too many scopes."); + test._checkScopeSectionDiv(scopeDiv, expectedScopes[scopeIndex]); + ++scopeIndex; + } + test.assertEquals(expectedScopes.length, scopeIndex, "Unexpected number of scopes."); + + test.releaseControl(); + } + + test.takeControl(); +}; + + +/** + * Returns child tree element for a property with given name. + * @param {TreeElement} parent Parent tree element. + * @param {string} childName + * @param {string} objectPath Path to the object. Will be printed in the case + * of failure. + * @return {TreeElement} + */ +TestSuite.prototype._findChildProperty = function(parent, childName, objectPath) +{ + var children = parent.children; + for (var i = 0; i < children.length; i++) { + var treeElement = children[i]; + var property = treeElement.property; + if (property.name === childName) + return treeElement; + } + this.fail('Cannot find property "' + childName + '" in ' + objectPath); +}; + + +/** + * Executes the 'code' with InjectedScriptAccess.getProperties overriden + * so that all callbacks passed to InjectedScriptAccess.getProperties are + * extended with the "hook". + * @param {Function} hook The hook function. + * @param {Function} code A code snippet to be executed. + */ +TestSuite.prototype._hookGetPropertiesCallback = function(hook, code) +{ + var accessor = InjectedScriptAccess.prototype; + var orig = accessor.getProperties; + accessor.getProperties = function(objectProxy, ignoreHasOwnProperty, abbreviate, callback) { + orig.call(this, objectProxy, ignoreHasOwnProperty, abbreviate, + function() { + callback.apply(this, arguments); + hook(); + }); + }; + try { + code(); + } finally { + accessor.getProperties = orig; + } +}; + + +/** + * Tests that all elements in prototype chain of an object have expected + * intrinic proprties(__proto__, constructor, prototype). + */ +TestSuite.prototype.testDebugIntrinsicProperties = function() +{ + this.showPanel("scripts"); + var test = this; + + this._executeCodeWhenScriptsAreParsed("handleClick()", ["debugger_intrinsic_properties.html$"]); + + this._waitForScriptPause( + { + functionsOnStack: ["callDebugger", "handleClick", "(anonymous function)"], + lineNumber: 29, + lineText: " debugger;" + }, + expandLocalScope); + + var localScopeSection = null; + function expandLocalScope() { + test._expandScopeSections(function(sections, i) { + if (i === 0) { + test.assertTrue(sections[i].object.isLocal, "Scope #0 is not Local."); + localScopeSection = sections[i]; + return true; + } + return false; + }, + examineLocalScope); + } + + function examineLocalScope() { + var scopeExpectations = [ + "a", "Object", [ + "constructor", "function Child()", [ + "constructor", "function Function()", null, + "name", "Child", null, + "prototype", "Object", [ + "childProtoField", 21, null + ] + ], + + "__proto__", "Object", [ + "__proto__", "Object", [ + "__proto__", "Object", [ + "__proto__", "null", null, + "constructor", "function Object()", null, + ], + "constructor", "function Parent()", [ + "name", "Parent", null, + "prototype", "Object", [ + "parentProtoField", 11, null, + ] + ], + "parentProtoField", 11, null, + ], + "constructor", "function Child()", null, + "childProtoField", 21, null, + ], + + "parentField", 10, null, + "childField", 20, null, + ] + ]; + + checkProperty(localScopeSection.propertiesTreeOutline, "<Local Scope>", scopeExpectations); + } + + var propQueue = []; + var index = 0; + var expectedFinalIndex = 8; + + function expandAndCheckNextProperty() { + if (index === propQueue.length) { + test.assertEquals(expectedFinalIndex, index, "Unexpected number of expanded objects."); + test.releaseControl(); + return; + } + + // Read next property data from the queue. + var treeElement = propQueue[index].treeElement; + var path = propQueue[index].path; + var expectations = propQueue[index].expectations; + index++; + + // Expand the property. + test._hookGetPropertiesCallback(function() { + checkProperty(treeElement, path, expectations); + }, + function() { + treeElement.expand(); + }); + } + + function checkProperty(treeElement, path, expectations) { + for (var i = 0; i < expectations.length; i += 3) { + var name = expectations[i]; + var description = expectations[i+1]; + var value = expectations[i+2]; + + var propertyPath = path + "." + name; + var propertyTreeElement = test._findChildProperty(treeElement, name, path); + test.assertTrue(propertyTreeElement, 'Property "' + propertyPath + '" not found.'); + test.assertEquals(description, propertyTreeElement.property.value.description, 'Unexpected "' + propertyPath + '" description.'); + if (value) { + // Schedule property content check. + propQueue.push({ + treeElement: propertyTreeElement, + path: propertyPath, + expectations: value, + }); + } + } + // Check next property in the queue. + expandAndCheckNextProperty(); + } + + test.takeControl(); +}; + + +/** + * Tests "Pause" button will pause debugger when a snippet is evaluated. + */ +TestSuite.prototype.testPauseInEval = function() +{ + this.showPanel("scripts"); + + var test = this; + + var pauseButton = document.getElementById("scripts-pause"); + pauseButton.click(); + + devtools.tools.evaluateJavaScript("fib(10)"); + + this.addSniffer(WebInspector, "pausedScript", + function() { + test.releaseControl(); + }); + + test.takeControl(); +}; + + +/** + * Key event with given key identifier. + */ +TestSuite.createKeyEvent = function(keyIdentifier) +{ + var evt = document.createEvent("KeyboardEvent"); + evt.initKeyboardEvent("keydown", true /* can bubble */, true /* can cancel */, null /* view */, keyIdentifier, ""); + return evt; +}; + + +/** + * Tests console eval. + */ +TestSuite.prototype.testConsoleEval = function() +{ + var test = this; + this.evaluateInConsole_("123", + function(resultText) { + test.assertEquals("123", resultText); + test.releaseControl(); + }); + + this.takeControl(); +}; + + +/** + * Tests console log. + */ +TestSuite.prototype.testConsoleLog = function() +{ + WebInspector.console.visible = true; + var messages = WebInspector.console.messages; + var index = 0; + + var test = this; + var assertNext = function(line, message, opt_class, opt_count, opt_substr) { + var elem = messages[index++].toMessageElement(); + var clazz = elem.getAttribute("class"); + var expectation = (opt_count || '') + 'console_test_page.html:' + line + message; + if (opt_substr) + test.assertContains(elem.textContent, expectation); + else + test.assertEquals(expectation, elem.textContent); + if (opt_class) + test.assertContains(clazz, "console-" + opt_class); + }; + + assertNext("5", "log", "log-level"); + assertNext("7", "debug", "log-level"); + assertNext("9", "info", "log-level"); + assertNext("11", "warn", "warning-level"); + assertNext("13", "error", "error-level"); + assertNext("15", "Message format number 1, 2 and 3.5"); + assertNext("17", "Message format for string"); + assertNext("19", "Object Object"); + assertNext("22", "repeated", "log-level", 5); + assertNext("26", "count: 1"); + assertNext("26", "count: 2"); + assertNext("29", "group", "group-title"); + index++; + assertNext("33", "timer:", "log-level", "", true); + assertNext("35", "1 2 3", "log-level"); + assertNext("37", "HTMLDocument", "log-level"); + assertNext("39", "<html>", "log-level", "", true); +}; + + +/** + * Tests eval of global objects. + */ +TestSuite.prototype.testEvalGlobal = function() +{ + WebInspector.console.visible = true; + + var inputs = ["foo", "foobar"]; + var expectations = ["foo", "fooValue", "foobar", "ReferenceError: foobar is not defined"]; + + // Do not change code below - simply add inputs and expectations above. + var initEval = function(input) { + WebInspector.console.prompt.text = input; + WebInspector.console.promptElement.dispatchEvent( TestSuite.createKeyEvent("Enter")); + }; + var test = this; + var messagesCount = 0; + var inputIndex = 0; + this.addSniffer(WebInspector.ConsoleView.prototype, "addMessage", + function(commandResult) { + messagesCount++; + if (messagesCount === expectations.length) { + var messages = WebInspector.console.messages; + for (var i = 0; i < expectations; ++i) { + var elem = messages[i++].toMessageElement(); + test.assertEquals(elem.textContent, expectations[i]); + } + test.releaseControl(); + } else if (messagesCount % 2 === 0) + initEval(inputs[inputIndex++]); + }, true); + + initEval(inputs[inputIndex++]); + this.takeControl(); +}; + + +/** + * Tests that Storage panel can be open and that local DOM storage is added + * to the panel. + */ +TestSuite.prototype.testShowStoragePanel = function() +{ + var test = this; + this.addSniffer(WebInspector.panels.storage, "addDOMStorage", + function(storage) { + var orig = storage.getEntries; + storage.getEntries = function(callback) { + orig.call(this, function(entries) { + callback(entries); + test.releaseControl(); + }); + }; + try { + WebInspector.currentPanel.selectDOMStorage(storage.id); + storage.getEntries = orig; + } catch (e) { + test.fail("Exception in selectDOMStorage: " + e); + } + }); + this.showPanel("storage"); + + // Access localStorage so that it's pushed to the frontend. + this.evaluateInConsole_( + 'setTimeout("localStorage.x = 10" , 0)', + function(resultText) { + test.assertTrue(!isNaN(resultText), "Failed to get timer id: " + resultText); + }); + + // Wait until DOM storage is added to the panel. + this.takeControl(); +}; + + +/** + * Test runner for the test suite. + */ +var uiTests = {}; + + +/** + * Run each test from the test suit on a fresh instance of the suite. + */ +uiTests.runAllTests = function() +{ + // For debugging purposes. + for (var name in TestSuite.prototype) { + if (name.substring(0, 4) === "test" && typeof TestSuite.prototype[name] === "function") + uiTests.runTest(name); + } +}; + + +/** + * Run specified test on a fresh instance of the test suite. + * @param {string} name Name of a test method from TestSuite class. + */ +uiTests.runTest = function(name) +{ + new TestSuite().runTest(name); +}; + + +} diff --git a/webkit/glue/webkit_resources.grd b/webkit/glue/webkit_resources.grd index 8322236..29dd456 100644 --- a/webkit/glue/webkit_resources.grd +++ b/webkit/glue/webkit_resources.grd @@ -18,7 +18,7 @@ <include name="IDR_SEARCH_MAGNIFIER" file="resources\search_magnifier.png" type="BINDATA" /> <include name="IDR_SEARCH_MAGNIFIER_RESULTS" file="resources\search_magnifier_results.png" type="BINDATA" /> <include name="IDR_TEXTAREA_RESIZER" file="resources\textarea_resize_corner.png" type="BINDATA" /> - <include name="IDR_DEVTOOLS_INJECT_DISPATCH_JS" file="..\..\third_party\WebKit\WebKit\chromium\src\js\InjectDispatch.js" type="BINDATA"/> + <include name="IDR_DEVTOOLS_INJECT_DISPATCH_JS" file="devtools\js\inject_dispatch.js" type="BINDATA" /> <include name="IDR_DEVTOOLS_INJECT_WEBKIT_JS" file="..\..\third_party\WebKit\WebCore\inspector\front-end\InjectedScript.js" type="BINDATA"/> <include name="IDR_MEDIA_PLAY_BUTTON" file="resources\media_play.png" type="BINDATA" /> <include name="IDR_MEDIA_PLAY_BUTTON_DISABLED" file="resources\media_play_disabled.png" type="BINDATA" /> diff --git a/webkit/webkit.gyp b/webkit/webkit.gyp index 631b6a3..fbc5f8b 100644 --- a/webkit/webkit.gyp +++ b/webkit/webkit.gyp @@ -5,7 +5,6 @@ { 'includes': [ '../third_party/WebKit/WebKit/chromium/features.gypi', - '../third_party/WebKit/WebKit/chromium/WebKit.gypi', '../third_party/WebKit/WebCore/WebCore.gypi', ], 'variables': { @@ -23,7 +22,9 @@ # List of DevTools source files, ordered by dependencies. It is used both # for copying them to resource dir, and for generating 'devtools.html' file. 'devtools_files': [ - '<@(devtools_css_files)', + 'glue/devtools/js/devtools.css', + 'glue/devtools/js/inspector_controller_impl.js', + 'glue/devtools/js/debugger_agent.js', '../v8/tools/codemap.js', '../v8/tools/consarray.js', '../v8/tools/csvparser.js', @@ -31,7 +32,12 @@ '../v8/tools/profile.js', '../v8/tools/profile_view.js', '../v8/tools/splaytree.js', - '<@(devtools_js_files)', + 'glue/devtools/js/profiler_agent.js', + 'glue/devtools/js/profiler_processor.js', + 'glue/devtools/js/heap_profiler_panel.js', + 'glue/devtools/js/devtools.js', + 'glue/devtools/js/devtools_host_stub.js', + 'glue/devtools/js/tests.js', ], 'debug_devtools%': 0, @@ -477,8 +483,19 @@ { 'destination': '<(PRODUCT_DIR)/resources/inspector/Images', 'files': [ - '<@(webinspector_image_files)', - '<@(devtools_image_files)', + + '<@(webinspector_image_files)', + + 'glue/devtools/js/Images/segmentChromium.png', + 'glue/devtools/js/Images/segmentHoverChromium.png', + 'glue/devtools/js/Images/segmentHoverEndChromium.png', + 'glue/devtools/js/Images/segmentSelectedChromium.png', + 'glue/devtools/js/Images/segmentSelectedEndChromium.png', + 'glue/devtools/js/Images/statusbarBackgroundChromium.png', + 'glue/devtools/js/Images/statusbarBottomBackgroundChromium.png', + 'glue/devtools/js/Images/statusbarButtonsChromium.png', + 'glue/devtools/js/Images/statusbarMenuButtonChromium.png', + 'glue/devtools/js/Images/statusbarMenuButtonSelectedChromium.png', ], }, ], |