blob: 5c40b1e10ea9286ef2f21ecf3ce9a0c39fd5c7bd [file] [log] [blame]
// Copyright (c) 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/** @typedef {{range: !Common.TextRange, wasUsed: boolean}} */
Coverage.RangeUsage;
/** @typedef {{styleSheetHeader: !SDK.CSSStyleSheetHeader, ranges: !Array<!Coverage.RangeUsage>}} */
Coverage.StyleSheetUsage;
/** @typedef {{url: string, size: (number|undefined), unusedSize: (number|undefined), usedSize: (number|undefined),
* ranges: !Array<!Coverage.RangeUsage>}} */
Coverage.CoverageInfo;
Coverage.CoverageView = class extends UI.VBox {
constructor() {
super(true);
this.registerRequiredCSS('coverage/coverageView.css');
var toolbarContainer = this.contentElement.createChild('div', 'coverage-toolbar-container');
var topToolbar = new UI.Toolbar('coverage-toolbar', toolbarContainer);
this._toggleRecordAction =
/** @type {!UI.Action }*/ (UI.actionRegistry.action('coverage.toggle-recording'));
topToolbar.appendToolbarItem(UI.Toolbar.createActionButton(this._toggleRecordAction));
var clearButton = new UI.ToolbarButton(Common.UIString('Clear all'), 'largeicon-clear');
clearButton.addEventListener(UI.ToolbarButton.Events.Click, this._reset.bind(this));
topToolbar.appendToolbarItem(clearButton);
this._coverageResultsElement = this.contentElement.createChild('div', 'coverage-results');
this._progressElement = this._coverageResultsElement.createChild('div', 'progress-view');
this._listView = new Coverage.CoverageListView();
this._statusToolbarElement = this.contentElement.createChild('div', 'coverage-toolbar-summary');
this._statusMessageElement = this._statusToolbarElement.createChild('div', 'coverage-message');
}
_reset() {
Workspace.workspace.uiSourceCodes().forEach(
uiSourceCode => uiSourceCode.removeDecorationsForType(Coverage.CoverageView.LineDecorator.type));
this._listView.detach();
this._coverageResultsElement.removeChildren();
this._progressElement.textContent = '';
this._coverageResultsElement.appendChild(this._progressElement);
this._statusMessageElement.textContent = '';
}
_toggleRecording() {
var enable = !this._toggleRecordAction.toggled();
if (enable)
this._startRecording();
else
this._stopRecording();
}
_startRecording() {
this._reset();
var mainTarget = SDK.targetManager.mainTarget();
if (!mainTarget)
return;
var cssModel = mainTarget.model(SDK.CSSModel);
if (!cssModel)
return;
this._toggleRecordAction.setToggled(true);
cssModel.startRuleUsageTracking();
mainTarget.profilerAgent().startPreciseCoverage();
this._progressElement.textContent = Common.UIString('Recording...');
}
async _stopRecording() {
this._toggleRecordAction.setToggled(false);
this._progressElement.textContent = Common.UIString('Fetching results...');
var cssCoverageInfoPromise = this._stopCSSCoverage();
var jsCoverageInfoPromise = this._stopJSCoverage();
var cssCoverageInfo = await cssCoverageInfoPromise;
var jsCoverageInfo = await jsCoverageInfoPromise;
this._updateViews(cssCoverageInfo.concat(jsCoverageInfo));
}
/**
* @param {!Array<!Coverage.CoverageInfo>} coverageInfo
* @this {!Coverage.CoverageView}
*/
_updateViews(coverageInfo) {
coverageInfo = Coverage.CoverageView._coalesceByURL(coverageInfo);
this._updateStats(coverageInfo);
this._updateGutter(coverageInfo);
this._coverageResultsElement.removeChildren();
this._listView.update(coverageInfo);
this._listView.show(this._coverageResultsElement);
}
/**
* @param {!Array<!Coverage.CoverageInfo>} coverageInfo
* @return {!Array<!Coverage.CoverageInfo>}
*/
static _coalesceByURL(coverageInfo) {
coverageInfo.sort((a, b) => (a.url || '').localeCompare(b.url));
var result = [];
for (var entry of coverageInfo) {
if (!entry.url)
continue;
if (result.length && result.peekLast().url === entry.url) {
var lastEntry = result.peekLast();
lastEntry.size += entry.size;
lastEntry.usedSize += entry.usedSize;
lastEntry.unusedSize += entry.unusedSize;
} else {
result.push(entry);
}
}
return result;
}
/**
* @return {!Promise<!Array<!Coverage.CoverageInfo>>}
*/
async _stopJSCoverage() {
var mainTarget = SDK.targetManager.mainTarget();
var profilerAgent = mainTarget && mainTarget.profilerAgent();
if (!profilerAgent)
return [];
var coveragePromise = profilerAgent.takePreciseCoverage((error, result) => error ? [] : result);
profilerAgent.stopPreciseCoverage();
var rawCoverageData = await coveragePromise;
return Coverage.CoverageView._processJSCoverage(
/** @type !SDK.DebuggerModel */ (SDK.DebuggerModel.fromTarget(mainTarget)), rawCoverageData);
}
/**
* @param {!SDK.DebuggerModel} debuggerModel
* @param {!Array<!Protocol.Profiler.ScriptCoverage>} scriptsCoverage
* @return {!Promise<!Array<!Coverage.CoverageInfo>>}
*/
static async _processJSCoverage(debuggerModel, scriptsCoverage) {
var promises = [];
for (var entry of scriptsCoverage) {
var ranges = [];
var script = debuggerModel.scriptForId(entry.scriptId);
if (!script)
continue;
for (var func of entry.functions) {
for (var range of func.ranges) {
var textRange = new Common.TextRange(
range.startLineNumber, range.startColumnNumber, range.endLineNumber, range.endColumnNumber);
ranges.push({range: textRange, wasUsed: !!range.count});
}
}
promises.push(convertToCoverageInfo(script, ranges));
}
return Promise.all(promises);
/**
* @param {!SDK.Script} script
* @param {!Array<!Coverage.RangeUsage>} ranges
* @return {!Promise<!Coverage.CoverageInfo>}
*/
function convertToCoverageInfo(script, ranges) {
return script.requestContent().then(
content => Coverage.CoverageView._coverageInfoForText(
script.contentURL(), script.lineOffset, script.columnOffset, content, ranges));
}
}
/**
* @return {!Promise<!Array<!Coverage.CoverageInfo>>}
*/
async _stopCSSCoverage() {
var mainTarget = SDK.targetManager.mainTarget();
const cssModel = mainTarget && mainTarget.model(SDK.CSSModel);
if (!cssModel)
return Promise.resolve([]);
var rawCoverageData = await cssModel.ruleListPromise();
return Coverage.CoverageView._processCSSCoverage(
/** @type !SDK.CSSModel */ (cssModel), rawCoverageData);
}
/**
* @param {!SDK.CSSModel} cssModel
* @param {!Array<!SDK.CSSModel.RuleUsage>} ruleUsageList
* @return {!Promise<!Array<!Coverage.CoverageInfo>>}
*/
static async _processCSSCoverage(cssModel, ruleUsageList) {
/** @type {!Map<?SDK.CSSStyleSheetHeader, !Array<!Coverage.RangeUsage>>} */
var rulesByStyleSheet = new Map();
for (var rule of ruleUsageList) {
var styleSheetHeader = cssModel.styleSheetHeaderForId(rule.styleSheetId);
var ranges = rulesByStyleSheet.get(styleSheetHeader);
if (!ranges) {
ranges = [];
rulesByStyleSheet.set(styleSheetHeader, ranges);
}
var textRange = new Common.TextRange(
rule.range.startLine + styleSheetHeader.startLine,
rule.range.startColumn + (rule.range.startLine ? 0 : styleSheetHeader.startColumn),
rule.range.endLine + styleSheetHeader.startLine,
rule.range.endColumn + (rule.range.endLine ? 0 : styleSheetHeader.startColumn));
ranges.push({range: textRange, wasUsed: rule.wasUsed});
}
return Promise.all(Array.from(rulesByStyleSheet.entries(), entry => convertToCoverageInfo(entry[0], entry[1])));
/**
* @param {!SDK.CSSStyleSheetHeader} styleSheetHeader
* @param {!Array<!Coverage.RangeUsage>} ranges
* @return {!Promise<!Coverage.CoverageInfo>}
*/
function convertToCoverageInfo(styleSheetHeader, ranges) {
return styleSheetHeader.requestContent().then(
content => Coverage.CoverageView._coverageInfoForText(
styleSheetHeader.sourceURL, styleSheetHeader.startLine, styleSheetHeader.startColumn, content, ranges));
}
}
/**
* @param {string} url
* @param {number} startLine
* @param {number} startColumn
* @param {?string} content
* @param {!Array<!Coverage.RangeUsage>} ranges
* @return {!Coverage.CoverageInfo}
*/
static _coverageInfoForText(url, startLine, startColumn, content, ranges) {
var coverageInfo = {
url: url,
ranges: ranges,
};
if (!content)
return coverageInfo;
var text = new Common.Text(content);
var offsetRanges = ranges.map(r => {
var range = r.range.relativeTo(startLine, startColumn);
return {
start: text.offsetFromPosition(range.startLine, range.startColumn),
end: text.offsetFromPosition(range.endLine, range.endColumn),
wasUsed: r.wasUsed
};
});
var stack = [];
offsetRanges.sort((a, b) => a.start - b.start);
for (var entry of offsetRanges) {
while (stack.length && stack.peekLast().end <= entry.start)
stack.pop();
entry.ownSize = entry.end - entry.start;
var top = stack.peekLast();
if (top) {
if (top.end < entry.end) {
console.assert(
false, `Overlapping coverage entries in ${url}: ${top.start}-${top.end} vs. ${entry.start}-${entry.end}`);
}
top.ownSize -= entry.ownSize;
}
stack.push(entry);
}
var usedSize = 0;
var unusedSize = 0;
for (var entry of offsetRanges) {
if (entry.wasUsed)
usedSize += entry.ownSize;
else
unusedSize += entry.ownSize;
}
coverageInfo.size = content.length;
coverageInfo.usedSize = usedSize;
coverageInfo.unusedSize = unusedSize;
return coverageInfo;
}
/**
* @param {!Array<!Coverage.CoverageInfo>} coverageInfo
*/
_updateStats(coverageInfo) {
var total = 0;
var unused = 0;
for (var info of coverageInfo) {
total += info.size || 0;
unused += info.unusedSize || 0;
}
var percentUnused = total ? Math.round(100 * unused / total) : 0;
this._statusMessageElement.textContent = Common.UIString(
'%s of %s bytes are not used. (%d%%)', Number.bytesToString(unused), Number.bytesToString(total),
percentUnused);
}
/**
* @param {!Array<!Coverage.CoverageInfo>} coverageInfo
*/
_updateGutter(coverageInfo) {
for (var info of coverageInfo) {
var uiSourceCode = info.url && Workspace.workspace.uiSourceCodeForURL(info.url);
if (!uiSourceCode)
continue;
for (var range of info.ranges)
uiSourceCode.addDecoration(range.range, Coverage.CoverageView.LineDecorator.type, range.wasUsed);
}
}
};
/**
* @implements {SourceFrame.UISourceCodeFrame.LineDecorator}
*/
Coverage.CoverageView.LineDecorator = class {
/**
* @override
* @param {!Workspace.UISourceCode} uiSourceCode
* @param {!TextEditor.CodeMirrorTextEditor} textEditor
*/
decorate(uiSourceCode, textEditor) {
var gutterType = 'CodeMirror-gutter-coverage';
var decorations = uiSourceCode.decorationsForType(Coverage.CoverageView.LineDecorator.type);
textEditor.uninstallGutter(gutterType);
if (!decorations || !decorations.size)
return;
textEditor.installGutter(gutterType, false);
for (var decoration of decorations) {
for (var line = decoration.range().startLine; line <= decoration.range().endLine; ++line) {
var element = createElementWithClass('div');
if (decoration.data())
element.className = 'text-editor-coverage-used-marker';
else
element.className = 'text-editor-coverage-unused-marker';
textEditor.setGutterDecoration(line, gutterType, element);
}
}
}
};
Coverage.CoverageView.LineDecorator.type = 'coverage';
/**
* @implements {UI.ActionDelegate}
*/
Coverage.CoverageView.RecordActionDelegate = class {
/**
* @override
* @param {!UI.Context} context
* @param {string} actionId
* @return {boolean}
*/
handleAction(context, actionId) {
var coverageViewId = 'coverage';
UI.viewManager.showView(coverageViewId)
.then(() => UI.viewManager.view(coverageViewId).widget())
.then(widget => /** @type !Coverage.CoverageView} */ (widget)._toggleRecording());
return true;
}
};