зеркало из https://github.com/mozilla/gecko-dev.git
Bug 828046 - Save/load profiles to/from disk; r=robcee
This commit is contained in:
Родитель
15dfc43e10
Коммит
58d0a3be29
|
@ -26,7 +26,9 @@ const { PROFILE_IDLE, PROFILE_COMPLETED, PROFILE_RUNNING } = require("devtools/p
|
|||
function Cleopatra(panel, opts) {
|
||||
let doc = panel.document;
|
||||
let win = panel.window;
|
||||
let { uid, name, showPlatformData } = opts;
|
||||
let { uid, name } = opts;
|
||||
let spd = opts.showPlatformData;
|
||||
let ext = opts.external;
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
|
@ -41,7 +43,7 @@ function Cleopatra(panel, opts) {
|
|||
this.iframe = doc.createElement("iframe");
|
||||
this.iframe.setAttribute("flex", "1");
|
||||
this.iframe.setAttribute("id", "profiler-cleo-" + uid);
|
||||
this.iframe.setAttribute("src", "cleopatra.html?uid=" + uid + "&showPlatformData=" + showPlatformData);
|
||||
this.iframe.setAttribute("src", "cleopatra.html?uid=" + uid + "&spd=" + spd + "&ext=" + ext);
|
||||
this.iframe.setAttribute("hidden", "true");
|
||||
|
||||
// Append our iframe and subscribe to postMessage events.
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
var gInstanceUID;
|
||||
var gParsedQS;
|
||||
var gHideSourceLinks;
|
||||
|
||||
function getParam(key) {
|
||||
if (gParsedQS)
|
||||
|
@ -93,6 +94,7 @@ window.addEventListener("message", onParentMessage);
|
|||
* in the light mode and creates all the UI we need.
|
||||
*/
|
||||
function initUI() {
|
||||
gHideSourceLinks = getParam("ext") === "true";
|
||||
gLightMode = true;
|
||||
|
||||
gFileList = { profileParsingFinished: function () {} };
|
||||
|
@ -106,25 +108,6 @@ function initUI() {
|
|||
|
||||
container.appendChild(gMainArea);
|
||||
document.body.appendChild(container);
|
||||
|
||||
var startButton = document.createElement("button");
|
||||
startButton.innerHTML = gStrings.getStr("profiler.start");
|
||||
startButton.addEventListener("click", function (event) {
|
||||
event.target.setAttribute("disabled", true);
|
||||
notifyParent("start");
|
||||
}, false);
|
||||
|
||||
var stopButton = document.createElement("button");
|
||||
stopButton.innerHTML = gStrings.getStr("profiler.stop");
|
||||
stopButton.addEventListener("click", function (event) {
|
||||
event.target.setAttribute("disabled", true);
|
||||
notifyParent("stop");
|
||||
}, false);
|
||||
|
||||
var message = document.createElement("div");
|
||||
message.className = "message";
|
||||
message.innerHTML = "To start profiling click the button above.";
|
||||
gMainArea.appendChild(message);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -224,7 +207,8 @@ function enterFinishedProfileUI() {
|
|||
}
|
||||
}
|
||||
|
||||
if (getParam("showPlatformData") !== "true")
|
||||
// Show platform data?
|
||||
if (getParam("spd") !== "true")
|
||||
toggleJavascriptOnly();
|
||||
}
|
||||
|
||||
|
|
|
@ -458,7 +458,7 @@ TreeView.prototype = {
|
|||
'<span class="resourceIcon" data-resource="' + node.library + '"></span> ' +
|
||||
'<span class="functionName">' + nodeName + '</span>' +
|
||||
'<span class="libraryName">' + libName + '</span>' +
|
||||
(nodeName === '(total)' ? '' :
|
||||
((nodeName === '(total)' || gHideSourceLinks) ? '' :
|
||||
'<input type="button" value="Focus Callstack" title="Focus Callstack" class="focusCallstackButton" tabindex="-1">');
|
||||
},
|
||||
_resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) {
|
||||
|
|
|
@ -4,14 +4,18 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const { Cu } = require("chrome");
|
||||
const { Cu, Cc, Ci, components } = require("chrome");
|
||||
|
||||
const {
|
||||
PROFILE_IDLE,
|
||||
PROFILE_RUNNING,
|
||||
PROFILE_COMPLETED,
|
||||
SHOW_PLATFORM_DATA
|
||||
SHOW_PLATFORM_DATA,
|
||||
L10N_BUNDLE
|
||||
} = require("devtools/profiler/consts");
|
||||
|
||||
const { TextEncoder } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
|
||||
|
||||
var EventEmitter = require("devtools/shared/event-emitter");
|
||||
var promise = require("sdk/core/promise");
|
||||
var Cleopatra = require("devtools/profiler/cleopatra");
|
||||
|
@ -21,6 +25,11 @@ var ProfilerController = require("devtools/profiler/controller");
|
|||
Cu.import("resource:///modules/devtools/gDevTools.jsm");
|
||||
Cu.import("resource://gre/modules/devtools/Console.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
|
||||
Cu.import("resource://gre/modules/osfile.jsm");
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
loader.lazyGetter(this, "L10N", () => new ViewHelpers.L10N(L10N_BUNDLE));
|
||||
|
||||
/**
|
||||
* Profiler panel. It is responsible for creating and managing
|
||||
|
@ -80,7 +89,8 @@ ProfilerPanel.prototype = {
|
|||
let doc = this.document;
|
||||
|
||||
return {
|
||||
get record() doc.querySelector("#profiler-start")
|
||||
get record() doc.querySelector("#profiler-start"),
|
||||
get import() doc.querySelector("#profiler-import"),
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -104,7 +114,7 @@ ProfilerPanel.prototype = {
|
|||
this._runningUid = profile ? profile.uid : null;
|
||||
|
||||
if (this._runningUid)
|
||||
btn.setAttribute("checked", true)
|
||||
btn.setAttribute("checked", true);
|
||||
else
|
||||
btn.removeAttribute("checked");
|
||||
},
|
||||
|
@ -152,13 +162,22 @@ ProfilerPanel.prototype = {
|
|||
let deferred = promise.defer();
|
||||
|
||||
this.controller = new ProfilerController(this.target);
|
||||
|
||||
this.sidebar = new Sidebar(this.document.querySelector("#profiles-list"));
|
||||
this.sidebar.widget.addEventListener("select", (ev) => {
|
||||
if (!ev.detail)
|
||||
return;
|
||||
|
||||
let profile = this.profiles.get(parseInt(ev.detail.value, 10));
|
||||
this.sidebar.on("save", (_, uid) => {
|
||||
let profile = this.profiles.get(uid);
|
||||
|
||||
if (!profile.data)
|
||||
return void Cu.reportError("Can't save profile because there's no data.");
|
||||
|
||||
this.openFileDialog({ mode: "save", name: profile.name }).then((file) => {
|
||||
if (file)
|
||||
this.saveProfile(file, profile.data);
|
||||
});
|
||||
});
|
||||
|
||||
this.sidebar.on("select", (_, uid) => {
|
||||
let profile = this.profiles.get(uid);
|
||||
this.activeProfile = profile;
|
||||
|
||||
if (profile.isReady) {
|
||||
|
@ -175,18 +194,18 @@ ProfilerPanel.prototype = {
|
|||
btn.addEventListener("click", () => this.toggleRecording(), false);
|
||||
btn.removeAttribute("disabled");
|
||||
|
||||
let imp = this.controls.import;
|
||||
imp.addEventListener("click", () => {
|
||||
this.openFileDialog({ mode: "open" }).then((file) => {
|
||||
if (file)
|
||||
this.loadProfile(file);
|
||||
});
|
||||
}, false);
|
||||
imp.removeAttribute("disabled");
|
||||
|
||||
// Import queued profiles.
|
||||
for (let [name, data] of this.controller.profiles) {
|
||||
let profile = this.createProfile(name);
|
||||
profile.isStarted = false;
|
||||
profile.isFinished = true;
|
||||
profile.data = data.data;
|
||||
profile.parse(data.data, () => this.emit("parsed"));
|
||||
|
||||
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
|
||||
if (!this.sidebar.selectedItem) {
|
||||
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
|
||||
}
|
||||
this.importProfile(name, data.data);
|
||||
}
|
||||
|
||||
this.isReady = true;
|
||||
|
@ -195,15 +214,7 @@ ProfilerPanel.prototype = {
|
|||
});
|
||||
|
||||
this.controller.on("profileEnd", (_, data) => {
|
||||
let profile = this.createProfile(data.name);
|
||||
profile.isStarted = false;
|
||||
profile.isFinished = true;
|
||||
profile.data = data.data;
|
||||
profile.parse(data.data, () => this.emit("parsed"));
|
||||
|
||||
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
|
||||
if (!this.sidebar.selectedItem)
|
||||
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
|
||||
this.importProfile(data.name, data.data);
|
||||
|
||||
if (this.recordingProfile && !data.fromConsole)
|
||||
this.recordingProfile = null;
|
||||
|
@ -227,9 +238,9 @@ ProfilerPanel.prototype = {
|
|||
* @param string name
|
||||
* (optional) name of the new profile
|
||||
*
|
||||
* @return ProfilerPanel
|
||||
* @return Profile
|
||||
*/
|
||||
createProfile: function (name) {
|
||||
createProfile: function (name, opts={}) {
|
||||
if (name && this.getProfileByName(name)) {
|
||||
return this.getProfileByName(name);
|
||||
}
|
||||
|
@ -239,7 +250,8 @@ ProfilerPanel.prototype = {
|
|||
let profile = new Cleopatra(this, {
|
||||
uid: uid,
|
||||
name: name,
|
||||
showPlatformData: this.showPlatformData
|
||||
showPlatformData: this.showPlatformData,
|
||||
external: opts.external
|
||||
});
|
||||
|
||||
this.profiles.set(uid, profile);
|
||||
|
@ -249,6 +261,30 @@ ProfilerPanel.prototype = {
|
|||
return profile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Imports profile data
|
||||
*
|
||||
* @param string name, new profile name
|
||||
* @param object data, profile data to import
|
||||
* @param object opts, (optional) if property 'external' is found
|
||||
* Cleopatra will hide arrow buttons.
|
||||
*
|
||||
* @return Profile
|
||||
*/
|
||||
importProfile: function (name, data, opts={}) {
|
||||
let profile = this.createProfile(name, { external: opts.external });
|
||||
profile.isStarted = false;
|
||||
profile.isFinished = true;
|
||||
profile.data = data;
|
||||
profile.parse(data, () => this.emit("parsed"));
|
||||
|
||||
this.sidebar.setProfileState(profile, PROFILE_COMPLETED);
|
||||
if (!this.sidebar.selectedItem)
|
||||
this.sidebar.selectedItem = this.sidebar.getItemByProfile(profile);
|
||||
|
||||
return profile;
|
||||
},
|
||||
|
||||
/**
|
||||
* Starts or stops profile recording.
|
||||
*/
|
||||
|
@ -422,7 +458,7 @@ ProfilerPanel.prototype = {
|
|||
*/
|
||||
displaySource: function PP_displaySource(data, onOpen=function() {}) {
|
||||
let win = this.window;
|
||||
let panelWin, timeout;
|
||||
let panelWin;
|
||||
|
||||
function onSourceShown(event) {
|
||||
if (event.detail.url !== data.uri) {
|
||||
|
@ -455,6 +491,81 @@ ProfilerPanel.prototype = {
|
|||
}.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Opens a normal file dialog.
|
||||
*
|
||||
* @params object opts, (optional) property 'mode' can be used to
|
||||
* specify which dialog to open. Can be either
|
||||
* 'save' or 'open' (default is 'open').
|
||||
* @return promise
|
||||
*/
|
||||
openFileDialog: function (opts={}) {
|
||||
let deferred = promise.defer();
|
||||
|
||||
let picker = Ci.nsIFilePicker;
|
||||
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(picker);
|
||||
let { name, mode } = opts;
|
||||
let save = mode === "save";
|
||||
let title = L10N.getStr(save ? "profiler.saveFileAs" : "profiler.openFile");
|
||||
|
||||
fp.init(this.window, title, save ? picker.modeSave : picker.modeOpen);
|
||||
fp.appendFilter("JSON", "*.json");
|
||||
fp.appendFilters(picker.filterText | picker.filterAll);
|
||||
|
||||
if (save)
|
||||
fp.defaultString = (name || "profile") + ".json";
|
||||
|
||||
fp.open((result) => {
|
||||
deferred.resolve(result === picker.returnCancel ? null : fp.file);
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Saves profile data to disk
|
||||
*
|
||||
* @param File file
|
||||
* @param object data
|
||||
*
|
||||
* @return promise
|
||||
*/
|
||||
saveProfile: function (file, data) {
|
||||
let encoder = new TextEncoder();
|
||||
let buffer = encoder.encode(JSON.stringify({ profile: data }, null, " "));
|
||||
let opts = { tmpPath: file.path + ".tmp" };
|
||||
|
||||
return OS.File.writeAtomic(file.path, buffer, opts);
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads profile data from disk
|
||||
*
|
||||
* @param File file
|
||||
* @return promise
|
||||
*/
|
||||
loadProfile: function (file) {
|
||||
let deferred = promise.defer();
|
||||
let ch = NetUtil.newChannel(file);
|
||||
ch.contentType = "application/json";
|
||||
|
||||
NetUtil.asyncFetch(ch, (input, status) => {
|
||||
if (!components.isSuccessCode(status)) throw new Error(status);
|
||||
|
||||
let conv = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
|
||||
.createInstance(Ci.nsIScriptableUnicodeConverter);
|
||||
conv.charset = "UTF-8";
|
||||
|
||||
let data = NetUtil.readInputStreamToString(input, input.available());
|
||||
data = conv.ConvertToUnicode(data);
|
||||
this.importProfile(file.leafName, JSON.parse(data).profile, { external: true });
|
||||
|
||||
deferred.resolve();
|
||||
});
|
||||
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/**
|
||||
* Cleanup.
|
||||
*/
|
||||
|
|
|
@ -20,7 +20,13 @@
|
|||
<vbox class="profiler-sidebar">
|
||||
<toolbar class="devtools-toolbar">
|
||||
<hbox id="profiler-controls">
|
||||
<toolbarbutton id="profiler-start" class="devtools-toolbarbutton"/>
|
||||
<toolbarbutton id="profiler-start"
|
||||
class="devtools-toolbarbutton"
|
||||
disabled="true"/>
|
||||
<toolbarbutton id="profiler-import"
|
||||
class="devtools-toolbarbutton"
|
||||
disabled="true"
|
||||
label="&importProfile.label;"/>
|
||||
</hbox>
|
||||
</toolbar>
|
||||
|
||||
|
|
|
@ -25,25 +25,57 @@ function Sidebar(el) {
|
|||
this.document = el.ownerDocument;
|
||||
this.widget = new SideMenuWidget(el);
|
||||
this.widget.notice = L10N.getStr("profiler.sidebarNotice");
|
||||
|
||||
this.widget.addEventListener("select", (ev) => {
|
||||
if (!ev.detail)
|
||||
return;
|
||||
|
||||
this.emit("select", parseInt(ev.detail.value, 10));
|
||||
});
|
||||
}
|
||||
|
||||
Sidebar.prototype = Heritage.extend(WidgetMethods, {
|
||||
/**
|
||||
* Adds a new item for a profile to the sidebar. Markup
|
||||
* example:
|
||||
*
|
||||
* <vbox id="profile-1" class="profiler-sidebar-item">
|
||||
* <h3>Profile 1</h3>
|
||||
* <hbox>
|
||||
* <span flex="1">Completed</span>
|
||||
* <a>Save</a>
|
||||
* </hbox>
|
||||
* </vbox>
|
||||
*
|
||||
*/
|
||||
addProfile: function (profile) {
|
||||
let doc = this.document;
|
||||
let box = doc.createElement("vbox");
|
||||
let vbox = doc.createElement("vbox");
|
||||
let hbox = doc.createElement("hbox");
|
||||
let h3 = doc.createElement("h3");
|
||||
let span = doc.createElement("span");
|
||||
let save = doc.createElement("a");
|
||||
|
||||
box.id = "profile-" + profile.uid;
|
||||
box.className = "profiler-sidebar-item";
|
||||
vbox.id = "profile-" + profile.uid;
|
||||
vbox.className = "profiler-sidebar-item";
|
||||
|
||||
h3.textContent = profile.name;
|
||||
span.setAttribute("flex", 1);
|
||||
span.textContent = L10N.getStr("profiler.stateIdle");
|
||||
|
||||
box.appendChild(h3);
|
||||
box.appendChild(span);
|
||||
save.textContent = L10N.getStr("profiler.save");
|
||||
save.addEventListener("click", (ev) => {
|
||||
ev.preventDefault();
|
||||
this.emit("save", profile.uid);
|
||||
});
|
||||
|
||||
this.push([box, profile.uid], {
|
||||
hbox.appendChild(span);
|
||||
hbox.appendChild(save);
|
||||
|
||||
vbox.appendChild(h3);
|
||||
vbox.appendChild(hbox);
|
||||
|
||||
this.push([vbox, profile.uid], {
|
||||
attachment: {
|
||||
name: profile.name,
|
||||
state: PROFILE_IDLE
|
||||
|
@ -61,16 +93,19 @@ Sidebar.prototype = Heritage.extend(WidgetMethods, {
|
|||
|
||||
setProfileState: function (profile, state) {
|
||||
let item = this.getItemByProfile(profile);
|
||||
let label = item.target.querySelector(".profiler-sidebar-item > span");
|
||||
let label = item.target.querySelector(".profiler-sidebar-item > hbox > span");
|
||||
|
||||
switch (state) {
|
||||
case PROFILE_IDLE:
|
||||
item.target.setAttribute("state", "idle");
|
||||
label.textContent = L10N.getStr("profiler.stateIdle");
|
||||
break;
|
||||
case PROFILE_RUNNING:
|
||||
item.target.setAttribute("state", "running");
|
||||
label.textContent = L10N.getStr("profiler.stateRunning");
|
||||
break;
|
||||
case PROFILE_COMPLETED:
|
||||
item.target.setAttribute("state", "completed");
|
||||
label.textContent = L10N.getStr("profiler.stateCompleted");
|
||||
break;
|
||||
default: // Wrong state, do nothing.
|
||||
|
|
|
@ -23,6 +23,7 @@ MOCHITEST_BROWSER_TESTS = \
|
|||
browser_profiler_console_api_content.js \
|
||||
browser_profiler_escape.js \
|
||||
browser_profiler_gecko_data.js \
|
||||
browser_profiler_io.js \
|
||||
head.js \
|
||||
$(NULL)
|
||||
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const URL = "data:text/html;charset=utf8,<p>browser_profiler_io</p>";
|
||||
|
||||
let temp = {};
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm", temp);
|
||||
let FileUtils = temp.FileUtils;
|
||||
let gTab, gPanel;
|
||||
|
||||
let gData = {
|
||||
"libs": "[]", // This property is not important for this test.
|
||||
"meta": {
|
||||
"version": 2,
|
||||
"interval": 1,
|
||||
"stackwalk": 0,
|
||||
"jank": 0,
|
||||
"processType": 0,
|
||||
"platform": "Macintosh",
|
||||
"oscpu": "Intel Mac OS X 10.8",
|
||||
"misc": "rv:25.0",
|
||||
"abi": "x86_64-gcc3",
|
||||
"toolkit": "cocoa",
|
||||
"product": "Firefox"
|
||||
},
|
||||
"threads": [
|
||||
{
|
||||
"samples": [
|
||||
{
|
||||
"name": "(root)",
|
||||
"frames": [
|
||||
{
|
||||
"location": "Startup::XRE_Main",
|
||||
"line": 3871
|
||||
},
|
||||
{
|
||||
"location": "Events::ProcessGeckoEvents",
|
||||
"line": 355
|
||||
},
|
||||
{
|
||||
"location": "Events::ProcessGeckoEvents",
|
||||
"line": 355
|
||||
}
|
||||
],
|
||||
"responsiveness": -0.002963,
|
||||
"time": 8.120823
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
|
||||
setUp(URL, function onSetUp(tab, browser, panel) {
|
||||
gTab = tab;
|
||||
gPanel = panel;
|
||||
|
||||
let file = FileUtils.getFile("TmpD", ["tmpprofile.json"]);
|
||||
file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("666", 8));
|
||||
|
||||
gPanel.saveProfile(file, gData).then(() => {dump("\n\nsup\n\n");}, () => {dump("\n\n:(((\n\n")})
|
||||
.then(gPanel.loadProfile.bind(gPanel, file))
|
||||
.then(checkData);
|
||||
});
|
||||
}
|
||||
|
||||
function checkData() {
|
||||
let profile = gPanel.activeProfile;
|
||||
let item = gPanel.sidebar.getItemByProfile(profile);
|
||||
let data = profile.data;
|
||||
|
||||
is(item.attachment.state, PROFILE_COMPLETED, "Profile is COMPLETED");
|
||||
is(gData.meta.oscpu, data.meta.oscpu, "Meta data is correct");
|
||||
is(gData.threads[0].samples.length, 1, "There's one sample");
|
||||
is(gData.threads[0].samples[0].name, "(root)", "Sample is correct");
|
||||
|
||||
tearDown(gTab, () => { gPanel = null; gTab = null; });
|
||||
}
|
|
@ -20,4 +20,8 @@
|
|||
|
||||
<!-- LOCALIZATION NOTE (profilerStop.label): This is the label for the
|
||||
- button that stops the profiler. -->
|
||||
<!ENTITY profilerStop.label "Stop">
|
||||
<!ENTITY profilerStop.label "Stop">
|
||||
|
||||
<!-- LOCALIZATION NOTE (profiler.importProfile): This string is displayed
|
||||
- on a button that opens a dialog to import a saved profile data file. -->
|
||||
<!ENTITY importProfile.label "Import…">
|
|
@ -102,4 +102,17 @@ profiler.stateCompleted=Completed
|
|||
# This string is displayed in the profiler sidebar when there are no
|
||||
# existing profiles to show (usually happens when the user opens the
|
||||
# profiler for the first time).
|
||||
profiler.sidebarNotice=There are no profiles yet.
|
||||
profiler.sidebarNotice=There are no profiles yet.
|
||||
|
||||
# LOCALIZATION NOTE (profiler.save)
|
||||
# This string is displayed as a label for a button that opens a Save File
|
||||
# dialog where user can save generated profiler to a file.
|
||||
profiler.save=Save
|
||||
|
||||
# LOCALIZATION NOTE (profiler.saveFileAs)
|
||||
# This string as a title for a Save File dialog.
|
||||
profiler.saveFileAs=Save Profile As
|
||||
|
||||
# LOCALIZATION NOTE (profiler.openFile)
|
||||
# This string as a title for a Open File dialog.
|
||||
profiler.openFile=Import Profile
|
|
@ -28,17 +28,32 @@
|
|||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.profiler-sidebar-item, .side-menu-widget-item-contents {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.profiler-sidebar-item > h3 {
|
||||
font-size: 13px;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profiler-sidebar-item > span {
|
||||
.profiler-sidebar-item > hbox {
|
||||
margin-top: 2px;
|
||||
color: rgb(140, 152, 165);
|
||||
}
|
||||
|
||||
.selected .profiler-sidebar-item > span {
|
||||
.profiler-sidebar-item > hbox > a {
|
||||
display: none;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
[state=completed].selected .profiler-sidebar-item > hbox > a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.selected .profiler-sidebar-item > hbox {
|
||||
color: rgb(128, 195, 228);
|
||||
}
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче