mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-23 21:01:08 +00:00
Bug 1885054 - [devtools] Display tracer data in a debugger sidebar. r=devtools-reviewers,nchevobbe
Differential Revision: https://phabricator.services.mozilla.com/D204446
This commit is contained in:
parent
442ca2ea27
commit
aeaa6f03fb
@ -46,6 +46,11 @@
|
||||
type="text/css"
|
||||
href="chrome://devtools/content/shared/components/tabs/Tabs.css"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="chrome://devtools/skin/components-frame.css"
|
||||
/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
@ -72,6 +72,7 @@ class DebuggerPanel {
|
||||
constructor(iframeWindow, toolbox, commands) {
|
||||
this.panelWin = iframeWindow;
|
||||
this.panelWin.L10N = L10N;
|
||||
this.panelWin.sourceMapURLService = toolbox.sourceMapURLService;
|
||||
|
||||
this.toolbox = toolbox;
|
||||
this.commands = commands;
|
||||
@ -367,6 +368,10 @@ class DebuggerPanel {
|
||||
this._actions.selectThread(threadActorID);
|
||||
}
|
||||
|
||||
showTracerSidebar() {
|
||||
this._actions.setPrimaryPaneTab("tracer");
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.panelWin.Debugger.destroy();
|
||||
this.emit("destroyed");
|
||||
|
@ -36,6 +36,7 @@ import {
|
||||
hasSourceActor,
|
||||
hasPrettyTab,
|
||||
isSourceActorWithSourceMap,
|
||||
getSourceByActorId,
|
||||
} from "../../selectors/index";
|
||||
|
||||
// This is only used by jest tests (and within this module)
|
||||
@ -88,6 +89,24 @@ export function selectSourceURL(url, options) {
|
||||
};
|
||||
}
|
||||
|
||||
export function selectSourceBySourceActorID(sourceActorId, options) {
|
||||
return async thunkArgs => {
|
||||
const { dispatch, getState } = thunkArgs;
|
||||
const source = getSourceByActorId(getState(), sourceActorId);
|
||||
if (!source) {
|
||||
throw new Error(`Unable to find source actor with id ${sourceActorId}`);
|
||||
}
|
||||
|
||||
const generatedLocation = createLocation({ ...options, source });
|
||||
|
||||
const originalLocation = await getOriginalLocation(
|
||||
generatedLocation,
|
||||
thunkArgs
|
||||
);
|
||||
return dispatch(selectLocation(originalLocation));
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around selectLocation, which creates the location object for us.
|
||||
* Note that it ignores the currently selected source and will select
|
||||
|
@ -2,15 +2,68 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
import {
|
||||
getAllTraces,
|
||||
getTraceFrames,
|
||||
getIsCurrentlyTracing,
|
||||
} from "../selectors/index";
|
||||
import { selectSourceBySourceActorID } from "./sources/select.js";
|
||||
const {
|
||||
TRACER_FIELDS_INDEXES,
|
||||
} = require("resource://devtools/server/actors/tracer.js");
|
||||
|
||||
/**
|
||||
* Called when tracing is toggled ON/OFF on a particular thread.
|
||||
*/
|
||||
export function tracingToggled(thread, enabled) {
|
||||
return ({ dispatch }) => {
|
||||
dispatch({
|
||||
type: "TRACING_TOGGLED",
|
||||
thread,
|
||||
enabled,
|
||||
return {
|
||||
type: "TRACING_TOGGLED",
|
||||
thread,
|
||||
enabled,
|
||||
};
|
||||
}
|
||||
|
||||
export function clearTracerData() {
|
||||
return {
|
||||
type: "TRACING_CLEAR",
|
||||
};
|
||||
}
|
||||
|
||||
export function addTraces(traces) {
|
||||
return async function ({ dispatch, getState }) {
|
||||
if (!getIsCurrentlyTracing(getState())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return dispatch({
|
||||
type: "ADD_TRACES",
|
||||
traces,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function selectTrace(traceIndex) {
|
||||
return async function ({ dispatch, getState }) {
|
||||
dispatch({
|
||||
type: "SELECT_TRACE",
|
||||
traceIndex,
|
||||
});
|
||||
const traces = getAllTraces(getState());
|
||||
const trace = traces[traceIndex];
|
||||
// Ignore DOM Event traces, which aren't related to a particular location in source.
|
||||
if (!trace || trace[TRACER_FIELDS_INDEXES.TYPE] == "event") {
|
||||
return;
|
||||
}
|
||||
|
||||
const frameIndex = trace[TRACER_FIELDS_INDEXES.FRAME_INDEX];
|
||||
const frames = getTraceFrames(getState());
|
||||
const frame = frames[frameIndex];
|
||||
|
||||
await dispatch(
|
||||
selectSourceBySourceActorID(frame.sourceId, {
|
||||
line: frame.line,
|
||||
column: frame.column,
|
||||
})
|
||||
);
|
||||
};
|
||||
}
|
||||
|
@ -8,6 +8,9 @@ import { features } from "../utils/prefs";
|
||||
|
||||
import { recordEvent } from "../utils/telemetry";
|
||||
import sourceQueue from "../utils/source-queue";
|
||||
const {
|
||||
TRACER_LOG_METHODS,
|
||||
} = require("resource://devtools/shared/specs/tracer.js");
|
||||
|
||||
let actions;
|
||||
let commands;
|
||||
@ -72,10 +75,6 @@ export async function onConnect(_commands, _resourceCommand, _actions, store) {
|
||||
await resourceCommand.watchResources([resourceCommand.TYPES.THREAD_STATE], {
|
||||
onAvailable: onThreadStateAvailable,
|
||||
});
|
||||
await resourceCommand.watchResources([resourceCommand.TYPES.JSTRACER_STATE], {
|
||||
onAvailable: onTracingStateAvailable,
|
||||
});
|
||||
|
||||
await resourceCommand.watchResources([resourceCommand.TYPES.ERROR_MESSAGE], {
|
||||
onAvailable: actions.addExceptionFromResources,
|
||||
});
|
||||
@ -84,6 +83,17 @@ export async function onConnect(_commands, _resourceCommand, _actions, store) {
|
||||
// we only care about future events for DOCUMENT_EVENT
|
||||
ignoreExistingResources: true,
|
||||
});
|
||||
await resourceCommand.watchResources([resourceCommand.TYPES.JSTRACER_STATE], {
|
||||
onAvailable: onTracingStateAvailable,
|
||||
});
|
||||
await resourceCommand.watchResources([resourceCommand.TYPES.JSTRACER_TRACE], {
|
||||
onAvailable: actions.addTraces,
|
||||
});
|
||||
|
||||
// Also register a toggle listener, in addition to JSTRACER_TRACE in order
|
||||
// to be able to clear the tracer data on tracing start, that, even if the
|
||||
// tracer is waiting for next interation/load.
|
||||
commands.tracerCommand.on("toggle", onTracingToggled);
|
||||
}
|
||||
|
||||
export function onDisconnect() {
|
||||
@ -107,6 +117,13 @@ export function onDisconnect() {
|
||||
resourceCommand.unwatchResources([resourceCommand.TYPES.DOCUMENT_EVENT], {
|
||||
onAvailable: onDocumentEventAvailable,
|
||||
});
|
||||
resourceCommand.unwatchResources([resourceCommand.TYPES.JSTRACER_STATE], {
|
||||
onAvailable: onTracingStateAvailable,
|
||||
});
|
||||
resourceCommand.unwatchResources([resourceCommand.TYPES.JSTRACER_TRACE], {
|
||||
onAvailable: actions.addTraces,
|
||||
});
|
||||
commands.tracerCommand.off("toggle", onTracingToggled);
|
||||
sourceQueue.clear();
|
||||
}
|
||||
|
||||
@ -175,11 +192,29 @@ async function onTracingStateAvailable(resources) {
|
||||
if (resource.targetFront.isDestroyed()) {
|
||||
continue;
|
||||
}
|
||||
// Ignore if the tracer is logging to any other output
|
||||
if (resource.logMethod != TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) {
|
||||
continue;
|
||||
}
|
||||
// For now, only consider the top level target
|
||||
if (!resource.targetFront.isTopLevel) {
|
||||
continue;
|
||||
}
|
||||
const threadFront = await resource.targetFront.getFront("thread");
|
||||
await actions.tracingToggled(threadFront.actor, resource.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
async function onTracingToggled() {
|
||||
const { tracerCommand } = commands;
|
||||
if (!tracerCommand.isTracingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We only notify about global enabling of the tracer in order to clear data
|
||||
await actions.clearTracerData();
|
||||
}
|
||||
|
||||
function onDocumentEventAvailable(events) {
|
||||
for (const event of events) {
|
||||
// Only consider top level document, and ignore remote iframes top document
|
||||
|
334
devtools/client/debugger/src/components/PrimaryPanes/Tracer.css
Normal file
334
devtools/client/debugger/src/components/PrimaryPanes/Tracer.css
Normal file
@ -0,0 +1,334 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
/**
|
||||
* Store variable on the root element, as it may be used by HTMLTooltip elements,
|
||||
* which are added outside of .tracer-container.
|
||||
*/
|
||||
:root {
|
||||
--tracer-event-color: var(--grey-50);
|
||||
--tracer-mouse-event-color: var(--green-60);
|
||||
--tracer-key-event-color: var(--teal-60);
|
||||
--tracer-mutation-color: var(--purple-30);
|
||||
--tracer-mutation-darker-color: oklch(from var(--tracer-mutation-color) calc(l * 0.6) c h);
|
||||
--slider-bar-background: var(--blue-55);
|
||||
}
|
||||
|
||||
.tracer-container {
|
||||
height: 100%;
|
||||
max-height: 100%;
|
||||
|
||||
display: grid;
|
||||
grid-template-areas: "toolbar toolbar"
|
||||
"timeline-toolbar timeline-toolbar"
|
||||
"timeline tree";
|
||||
grid-template-columns: auto 1fr;
|
||||
grid-template-rows: auto auto 1fr;
|
||||
|
||||
& > .tracer-toolbar {
|
||||
grid-area: toolbar;
|
||||
}
|
||||
& > .tracer-timeline-toolbar {
|
||||
grid-area: timeline-toolbar;
|
||||
}
|
||||
& > .tracer-timeline {
|
||||
grid-area: timeline;
|
||||
}
|
||||
& > :is(.tracer-message, .tree) {
|
||||
grid-area: tree;
|
||||
}
|
||||
}
|
||||
|
||||
.tracer-toolbar .tracer-experimental-notice {
|
||||
--icon-size: 16px;
|
||||
--icon-inline-padding: 4px;
|
||||
|
||||
background-color: var(--theme-body-alternate-emphasized-background);
|
||||
border-block-end: 1px solid var(--theme-splitter-color);
|
||||
padding: 1em;
|
||||
padding-inline-start: calc(var(--icon-inline-padding) * 2 + var(--icon-size));
|
||||
background-image: url("chrome://global/skin/icons/experiments.svg");
|
||||
background-position-x: var(--icon-inline-padding);
|
||||
background-position-y: 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-size: var(--icon-size);
|
||||
-moz-context-properties: fill;
|
||||
fill: var(--theme-icon-checked-color);
|
||||
}
|
||||
|
||||
.tracer-tab .tracer-message {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
padding: 0.5em;
|
||||
font-size: 12px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tracer-tab .tree {
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
border-inline-start: 1px solid var(--theme-splitter-color);
|
||||
padding-inline-start: 4px !important;
|
||||
}
|
||||
|
||||
.tracer-tab .tree-node {
|
||||
/* This matches `itemHeight` set on VirtualizedTree component */
|
||||
height: var(--tree-node-height);
|
||||
line-height: var(--tree-node-height);
|
||||
text-wrap: nowrap;
|
||||
margin-inline-start: calc(5px * var(--tree-node-depth));
|
||||
/* make the tree node at least as wide as its content so the hover style isn't weird */
|
||||
width: min-content;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.tracer-tab .tree-node .trace-line {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tracer-tab .frame-link {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 0 10px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* Force the smart trace link to be light color when the focused style make the background blue */
|
||||
.tracer-tab .tree-node.focused .frame-link-source {
|
||||
color: var(--theme-selection-color);
|
||||
}
|
||||
|
||||
.tracer-tab .frame-link.match, .tracer-tab .frame-link.match .frame-link-source {
|
||||
background: var(--theme-contrast-background);
|
||||
color: var(--theme-contrast-color);
|
||||
}
|
||||
|
||||
.tracer-tab .tree-node:has(.frame-link.onstack) {
|
||||
background-color: light-dark(lightblue, var(--theme-body-alternate-emphasized-background));
|
||||
}
|
||||
|
||||
.tracer-tab .frame-link-source {
|
||||
max-width:200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-wrap: nowrap;
|
||||
color: var(--theme-internal-link-color);
|
||||
}
|
||||
|
||||
.tracer-tab .frame-link-function-display-name {
|
||||
flex: 1;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
font-family: var(--monospace-font-family);
|
||||
}
|
||||
|
||||
.tracer-dom-event, .tracer-dom-mutation {
|
||||
padding-inline: 4px;
|
||||
margin-inline: 5px;
|
||||
|
||||
&.mouse {
|
||||
--event-color: var(--tracer-mouse-event-color);
|
||||
}
|
||||
&.key {
|
||||
--event-color: var(--tracer-key-event-color);
|
||||
}
|
||||
&.tracer-dom-mutation {
|
||||
--event-color: var(--tracer-mutation-color);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
height: 8px;
|
||||
aspect-ratio: 1 / 1;
|
||||
margin-inline-end: 4px;
|
||||
background-color: var(--event-color, var(--tracer-event-color));
|
||||
display: inline-block;
|
||||
vertical-align: baseline;
|
||||
outline: 1px solid;
|
||||
}
|
||||
}
|
||||
|
||||
.tracer-timeline {
|
||||
width: 50px;
|
||||
padding-inline: 2px 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--theme-body-alternate-emphasized-background);
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tracer-timeline-toolbar {
|
||||
border-bottom: 1px solid var(--theme-splitter-color);
|
||||
|
||||
.tracer-reset-zoom {
|
||||
padding: 0.5em 2em;
|
||||
margin-inline: auto;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.tracer-slider-box {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/**
|
||||
* .selected-before and .selected-after are little blue arrows shown
|
||||
* on top and bottom of the slider to indicate when the selected trace
|
||||
* is outside of the current viewport.
|
||||
*/
|
||||
.tracer-slider-box.selected-before .tracer-slice-slider::before,
|
||||
.tracer-slider-box.selected-after .tracer-slice-slider::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
background-image: url("chrome://devtools/skin/images/arrow.svg");
|
||||
background-position: top center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 10px;
|
||||
|
||||
-moz-context-properties: fill;
|
||||
}
|
||||
|
||||
.tracer-slider-box.selected-before .tracer-slice-slider::before {
|
||||
top: 0;
|
||||
transform: rotate(180deg);
|
||||
fill: var(--slider-bar-background);
|
||||
}
|
||||
|
||||
.tracer-slider-box.selected-after .tracer-slice-slider::after {
|
||||
bottom: 0;
|
||||
left: -2px;
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.tracer-slider-box:is(.cut-start, .cut-end) {
|
||||
--shadow-size: 10px;
|
||||
--shadow-color: rgb(0 0 0 / 0.3);
|
||||
}
|
||||
|
||||
.tracer-slider-box.cut-start::before {
|
||||
content: "";
|
||||
height: var(--shadow-size);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to bottom, var(--shadow-color), transparent);
|
||||
}
|
||||
.tracer-slider-box.cut-end::after {
|
||||
content: "";
|
||||
height: var(--shadow-size);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(to top, var(--shadow-color), transparent);
|
||||
}
|
||||
|
||||
.tracer-slice-slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
/* ignore overflows of mutations icons going over limits on top or bottom */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tracer-slider-bar {
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
var(--slider-bar-background) var(--slider-bar-progress, 0%),
|
||||
transparent var(--slider-bar-progress, 0%)
|
||||
);
|
||||
background-color: var(--theme-body-background);
|
||||
border: 1px solid var(--theme-splitter-color);
|
||||
position: absolute;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.tracer-slider-position {
|
||||
position: absolute;
|
||||
top: var(--slider-bar-progress);
|
||||
z-index: 15;
|
||||
width: 100%;
|
||||
height: 3px;
|
||||
background-color: var(--slider-bar-background);
|
||||
border-inline-start: 1px solid var(--theme-splitter-color);
|
||||
}
|
||||
.tracer-slider-event, .tracer-slider-mutation {
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
left: 12px;
|
||||
z-index: 8;
|
||||
|
||||
}
|
||||
.tracer-slider-event {
|
||||
background-color: var(--tracer-event-color);
|
||||
--top-line-height: 2px;
|
||||
--left-line-width: 10px;
|
||||
clip-path: polygon(0% 0%, 100% 0%, 100% var(--top-line-height), var(--left-line-width) var(--top-line-height), var(--left-line-width) 100%, 0px 100%);
|
||||
width: 30px;
|
||||
|
||||
&.mouse {
|
||||
background-color: var(--tracer-mouse-event-color);
|
||||
}
|
||||
&.key {
|
||||
background-color: var(--tracer-key-event-color);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--blue-30);
|
||||
}
|
||||
}
|
||||
|
||||
.tracer-slider-mutation {
|
||||
position: absolute;
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 18px;
|
||||
align-content: center;
|
||||
text-align: center;
|
||||
|
||||
border: 1px solid var(--tracer-mutation-darker-color);
|
||||
|
||||
/**
|
||||
* Move the element at its middle of its coordinate so that the JS code
|
||||
* defining its coordinate doesn't have to care about its size
|
||||
*/
|
||||
transform: translateX(6px) translateY(-50%);
|
||||
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.tracer-slider-mutation, .tracer-slider-mutation div {
|
||||
background-color: var(--tracer-mutation-color);
|
||||
color: var(--tracer-mutation-darker-color);
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.event-tooltip .tooltip-panel {
|
||||
padding: 10px;
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-block-end: 1px solid var(--theme-splitter-color);
|
||||
}
|
||||
}
|
823
devtools/client/debugger/src/components/PrimaryPanes/Tracer.js
Normal file
823
devtools/client/debugger/src/components/PrimaryPanes/Tracer.js
Normal file
@ -0,0 +1,823 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
||||
import React, {
|
||||
Component,
|
||||
createFactory,
|
||||
} from "devtools/client/shared/vendor/react";
|
||||
import { div, button } from "devtools/client/shared/vendor/react-dom-factories";
|
||||
import SearchInput from "../shared/SearchInput";
|
||||
import { connect } from "devtools/client/shared/vendor/react-redux";
|
||||
import {
|
||||
getSelectedTraceIndex,
|
||||
getTopTraces,
|
||||
getAllTraces,
|
||||
getTraceChildren,
|
||||
getTraceParents,
|
||||
getTraceFrames,
|
||||
getAllMutationTraces,
|
||||
getAllTraceCount,
|
||||
getIsCurrentlyTracing,
|
||||
} from "../../selectors/index";
|
||||
const VirtualizedTree = require("resource://devtools/client/shared/components/VirtualizedTree.js");
|
||||
const FrameView = createFactory(
|
||||
require("resource://devtools/client/shared/components/Frame.js")
|
||||
);
|
||||
const {
|
||||
TRACER_FIELDS_INDEXES,
|
||||
} = require("resource://devtools/server/actors/tracer.js");
|
||||
const {
|
||||
HTMLTooltip,
|
||||
} = require("resource://devtools/client/shared/widgets/tooltip/HTMLTooltip.js");
|
||||
|
||||
import actions from "../../actions/index";
|
||||
|
||||
const isMacOS = AppConstants.platform == "macosx";
|
||||
const TREE_NODE_HEIGHT = 20;
|
||||
const DEBUG = false;
|
||||
|
||||
export class Tracer extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
// List of expanded traces in the tree
|
||||
expanded: new Set(),
|
||||
|
||||
// First visible trace's index.
|
||||
// Note that these two indexes aren't related to the VirtualizedTree viewport.
|
||||
// That's the possibly visible traces when scrolling top/bottom of the whole Tree.
|
||||
startIndex: 0,
|
||||
|
||||
// Last visible trace's index. -1 is we should show all of them at the end.
|
||||
// As soon as we start scrolling via the left slider, new traces are added outside of the selected viewport.
|
||||
endIndex: -1,
|
||||
|
||||
// Number of trace rendered in the timeline and the tree (considering the tree is expanded, less may be displayed based on collapsing)
|
||||
renderedTraceCount: 0,
|
||||
};
|
||||
|
||||
this.onSliderClick = this.onSliderClick.bind(this);
|
||||
this.onSliderWheel = this.onSliderWheel.bind(this);
|
||||
this.resetZoom = this.resetZoom.bind(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
const { traceParents } = this.props;
|
||||
if (
|
||||
nextProps.selectedTraceIndex != this.props.selectedTraceIndex &&
|
||||
nextProps.selectedTraceIndex != null
|
||||
) {
|
||||
const { expanded } = this.state;
|
||||
let index = traceParents[nextProps.selectedTraceIndex];
|
||||
while (index) {
|
||||
expanded.add(index);
|
||||
index = traceParents[index];
|
||||
}
|
||||
this.setState({ expanded });
|
||||
}
|
||||
|
||||
// Force update the renderedTraceCount when we receive new traces
|
||||
if (nextProps.traceCount != this.props.traceCount) {
|
||||
if (nextProps.traceCount == 0) {
|
||||
// Reset the indexes when the view is cleared (i.e. when we just started recording a new trace)
|
||||
this.updateIndexes(
|
||||
{
|
||||
startIndex: 0,
|
||||
endIndex: -1,
|
||||
},
|
||||
nextProps
|
||||
);
|
||||
} else {
|
||||
this.updateIndexes(
|
||||
{
|
||||
startIndex: this.state.startIndex,
|
||||
endIndex: this.state.endIndex,
|
||||
},
|
||||
nextProps
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// Prevent toolbox zooming when using Ctrl+Wheel on the slider.
|
||||
// (for some reason... React doesn't seem to register the right "wheel", event listener via `onWheel`,
|
||||
// which is the one to cancel to prevent toolbox zooming code to work.)
|
||||
this.refs.timeline.onwheel = e => e.preventDefault();
|
||||
|
||||
if (!this.tooltip) {
|
||||
this.instantiateTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
instantiateTooltip() {
|
||||
this.tooltip = new HTMLTooltip(this.refs.timeline.ownerDocument, {
|
||||
className: "event-tooltip",
|
||||
type: "arrow",
|
||||
// Avoid consuming the first click on the anchored UI element in the slider
|
||||
consumeOutsideClicks: false,
|
||||
});
|
||||
this.tooltip.setContentSize({ height: "auto" });
|
||||
this.tooltip.startTogglingOnHover(this.refs.timeline, (target, tooltip) => {
|
||||
if (target.classList.contains("tracer-slider-event")) {
|
||||
const { traceIndex } = target.dataset;
|
||||
const trace = this.props.allTraces[traceIndex];
|
||||
const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
|
||||
const eventType = getEventClassNameFromTraceEventName(eventName);
|
||||
tooltip.panel.innerHTML = "";
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("tracer-dom-event", eventType);
|
||||
el.textContent = eventName;
|
||||
tooltip.panel.append(
|
||||
el,
|
||||
document.createElement("hr"),
|
||||
document.createTextNode(
|
||||
"Double click to focus on the executions related to this event."
|
||||
)
|
||||
);
|
||||
return true;
|
||||
} else if (target.classList.contains("tracer-slider-mutation")) {
|
||||
const { traceIndex } = target.dataset;
|
||||
const trace = this.props.allTraces[traceIndex];
|
||||
const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE];
|
||||
tooltip.panel.innerHTML = "";
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("tracer-dom-mutation");
|
||||
el.textContent = `DOM Mutation | ${mutationType}`;
|
||||
tooltip.panel.append(
|
||||
el,
|
||||
document.createElement("hr"),
|
||||
document.createTextNode(
|
||||
"Click to find the call tree leading to this mutation."
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (DEBUG) {
|
||||
dump(
|
||||
` # start: ${this.state.startIndex} end: ${this.state.endIndex} rendered: ${this.state.renderedTraceCount} traceCount:${this.props.traceCount}\n`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderTree() {
|
||||
let {
|
||||
selectedTraceIndex,
|
||||
topTraces,
|
||||
allTraces,
|
||||
traceChildren,
|
||||
traceParents,
|
||||
} = this.props;
|
||||
if (!topTraces.length) {
|
||||
if (!this.props.isTracing) {
|
||||
return div(
|
||||
{ className: "tracer-message" },
|
||||
"Tracer is off, or pending for next interation/load."
|
||||
);
|
||||
}
|
||||
return div(
|
||||
{ className: "tracer-message" },
|
||||
"Waiting for the first JavaScript executions"
|
||||
);
|
||||
}
|
||||
|
||||
const { searchStrings, startIndex, endIndex } = this.state;
|
||||
|
||||
if (startIndex != 0 || endIndex != -1) {
|
||||
// When we start zooming, only consider traces whose top level frame
|
||||
// is in the zoomed section.
|
||||
|
||||
// Lookup for the first top trace after the start index
|
||||
let topTracesStartIndex = 0;
|
||||
if (startIndex != 0) {
|
||||
for (let i = 0; i < topTraces.length; i++) {
|
||||
const traceIndex = topTraces[i];
|
||||
if (traceIndex >= startIndex) {
|
||||
topTracesStartIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lookup for the first top trace from the end before the end index
|
||||
let topTracesEndIndex = topTraces.length;
|
||||
if (endIndex != -1) {
|
||||
for (let i = topTraces.length; i >= 0; i--) {
|
||||
const traceIndex = topTraces[i];
|
||||
if (traceIndex <= endIndex) {
|
||||
topTracesEndIndex = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
topTraces = topTraces.slice(topTracesStartIndex, topTracesEndIndex);
|
||||
|
||||
// When the top trace isn't the top most one (`!0`) and isn't a top trace (`!topTraces[0]`),
|
||||
// We need to add the current start trace as a top trace, as well as all its following siblings
|
||||
// and the following siblings of parent traces recursively.
|
||||
// This help show partial call tree when scrolling/zooming with a partial view on a call stack.
|
||||
//
|
||||
// Note that for endIndex, the cut is being done in VirtualizedTree's getChildren function.
|
||||
if (startIndex != 0 && topTraces[0] != startIndex) {
|
||||
const results = [];
|
||||
// indexes are floating number, so convert it to a decimal number as index in the trace array
|
||||
results.push(Math.floor(startIndex));
|
||||
collectAllSiblings(traceParents, traceChildren, startIndex, results);
|
||||
topTraces.unshift(...results);
|
||||
}
|
||||
}
|
||||
|
||||
if (searchStrings) {
|
||||
topTraces = topTraces.filter(traceIndex => {
|
||||
const trace = allTraces[traceIndex];
|
||||
if (trace[TRACER_FIELDS_INDEXES.TYPE] != "event") {
|
||||
return false;
|
||||
}
|
||||
let label = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
|
||||
if (!label) {
|
||||
return false;
|
||||
}
|
||||
label = label.toLowerCase();
|
||||
return searchStrings.some(search => label.includes(search));
|
||||
});
|
||||
if (!topTraces.length) {
|
||||
return div(
|
||||
{ className: "tracer-message" },
|
||||
"No trace matches for the current search"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return React.createElement(VirtualizedTree, {
|
||||
itemHeight: TREE_NODE_HEIGHT,
|
||||
autoExpandDepth: 1,
|
||||
getRoots() {
|
||||
return topTraces;
|
||||
},
|
||||
getKey(traceIndex) {
|
||||
return `${traceIndex}`;
|
||||
},
|
||||
getParent(traceIndex) {
|
||||
return traceParents[traceIndex];
|
||||
},
|
||||
getChildren(traceIndex) {
|
||||
// When we aren't displaying all children up to the end of the record,
|
||||
// we may need to remove children that are outside of the viewport.
|
||||
if (endIndex != -1) {
|
||||
return traceChildren[traceIndex].filter(index => {
|
||||
return index <= endIndex;
|
||||
});
|
||||
}
|
||||
return traceChildren[traceIndex];
|
||||
},
|
||||
|
||||
isExpanded: traceIndex => {
|
||||
return this.state.expanded.has(traceIndex);
|
||||
},
|
||||
onExpand: traceIndex => {
|
||||
const { expanded } = this.state;
|
||||
expanded.add(traceIndex);
|
||||
this.setState({ expanded });
|
||||
},
|
||||
onCollapse: traceIndex => {
|
||||
const { expanded } = this.state;
|
||||
expanded.delete(traceIndex);
|
||||
this.setState({ expanded });
|
||||
},
|
||||
|
||||
focused: selectedTraceIndex,
|
||||
onFocus: traceIndex => {
|
||||
this.props.selectTrace(traceIndex);
|
||||
},
|
||||
|
||||
shown: selectedTraceIndex,
|
||||
|
||||
renderItem: (traceIndex, _depth, isFocused, arrow, _isExpanded) => {
|
||||
const trace = allTraces[traceIndex];
|
||||
const type = trace[TRACER_FIELDS_INDEXES.TYPE];
|
||||
|
||||
if (type == "event") {
|
||||
// Trace for DOM Events are always top level trace (and do not need margin/indent)
|
||||
const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
|
||||
|
||||
const eventType = getEventClassNameFromTraceEventName(eventName);
|
||||
return div(
|
||||
{
|
||||
className: "trace-line",
|
||||
},
|
||||
arrow,
|
||||
div(
|
||||
{
|
||||
className: `tracer-dom-event ${eventType}${
|
||||
selectedTraceIndex == traceIndex ? " selected" : ""
|
||||
}`,
|
||||
|
||||
onDoubleClick: () => {
|
||||
this.focusOnTrace(traceIndex);
|
||||
},
|
||||
},
|
||||
eventName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (type == "dom-mutation") {
|
||||
// Trace for DOM Mutations are always a leaf and don't have children.
|
||||
const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE];
|
||||
return div(
|
||||
{
|
||||
className: `tracer-dom-mutation${
|
||||
selectedTraceIndex == trace ? " selected" : ""
|
||||
}`,
|
||||
},
|
||||
`DOM Mutation | ${mutationType}`
|
||||
);
|
||||
}
|
||||
|
||||
if (type == "exit") {
|
||||
return null;
|
||||
}
|
||||
|
||||
let className = "";
|
||||
if (selectedTraceIndex) {
|
||||
let idx = selectedTraceIndex;
|
||||
let onStack = false;
|
||||
while ((idx = traceParents[idx])) {
|
||||
if (idx == traceIndex) {
|
||||
onStack = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (onStack) {
|
||||
className += " onstack";
|
||||
}
|
||||
}
|
||||
const frameIndex = trace[TRACER_FIELDS_INDEXES.FRAME_INDEX];
|
||||
const frame = this.props.frames[frameIndex];
|
||||
return div(
|
||||
{
|
||||
className: "trace-line",
|
||||
onDoubleClick: () => {
|
||||
this.focusOnTrace(traceIndex);
|
||||
},
|
||||
},
|
||||
arrow,
|
||||
FrameView({
|
||||
className,
|
||||
showFunctionName: true,
|
||||
showAnonymousFunctionName: true,
|
||||
frame,
|
||||
sourceMapURLService: window.sourceMapURLService,
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onSliderClick(event) {
|
||||
const { top, height } = this.refs.sliceSlider.getBoundingClientRect();
|
||||
const yInSlider = event.clientY - top;
|
||||
const mousePositionRatio = yInSlider / height;
|
||||
|
||||
const index =
|
||||
this.state.startIndex +
|
||||
Math.floor(mousePositionRatio * this.state.renderedTraceCount);
|
||||
|
||||
this.props.selectTrace(index);
|
||||
}
|
||||
|
||||
searchInputOnChange = e => {
|
||||
// Support multiple search strings being comma separated,
|
||||
// ignore any extra white space and do a case insensitive search,
|
||||
// ignore empty search strings,
|
||||
const searchStrings = e.target.value
|
||||
.split(",")
|
||||
.map(search => search.trim().toLowerCase())
|
||||
.filter(search => !!search.length);
|
||||
if (searchStrings.length) {
|
||||
this.setState({
|
||||
searchStrings,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
searchStrings: null,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
renderSearchInput() {
|
||||
return React.createElement(SearchInput, {
|
||||
query: this.state.query,
|
||||
count: 0,
|
||||
placeholder: "Search DOM Events (comma separated list)",
|
||||
size: "small",
|
||||
showErrorEmoji: false,
|
||||
isLoading: false,
|
||||
onChange: this.searchInputOnChange,
|
||||
onKeyDown: () => {},
|
||||
showClose: false,
|
||||
showExcludePatterns: false,
|
||||
showSearchModifiers: false,
|
||||
searchOptions: {},
|
||||
});
|
||||
}
|
||||
|
||||
onSliderWheel(event) {
|
||||
const direction = event.deltaY > 0 ? 1 : -1;
|
||||
const scrolledDelta = Math.abs(event.deltaY) * 0.01;
|
||||
|
||||
let { startIndex, endIndex } = this.state;
|
||||
|
||||
if (isMacOS ? event.metaKey : event.ctrlKey) {
|
||||
// Handle zooming it/out as we are either using CtrlOrMeta+Wheel or zooming via the touchpad
|
||||
|
||||
// Compute the ratio (a percentage) of the position where the mouse or touch started zooming from
|
||||
const { top, height } = this.refs.sliceSlider.getBoundingClientRect();
|
||||
const yInSlider = event.clientY - top;
|
||||
const zoomOriginRatio = yInSlider / height;
|
||||
|
||||
// Compute the number of indexes we should add or remove to both indexes
|
||||
const shift = Math.floor(
|
||||
Math.max(this.state.renderedTraceCount * scrolledDelta, 2) * direction
|
||||
);
|
||||
|
||||
// Use the origin ratio in order to try to zoom where the cursor is
|
||||
// and distribute the shift between start and end according to its position.
|
||||
startIndex -= shift * zoomOriginRatio;
|
||||
if (endIndex == -1) {
|
||||
endIndex = this.props.traceCount + shift * (1 - zoomOriginRatio);
|
||||
} else {
|
||||
endIndex += shift * (1 - zoomOriginRatio);
|
||||
}
|
||||
} else {
|
||||
// Handle scrolling up/down as We are doing a simple scroll via wheel or touchpad
|
||||
|
||||
// Avoid scrolling if we already at top or bottomn
|
||||
if (
|
||||
(direction < 0 && startIndex == 0) ||
|
||||
(direction > 0 && endIndex == -1)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute the number of indexes we should add or remove to both indexes
|
||||
const shift =
|
||||
Math.max(1, this.state.renderedTraceCount * scrolledDelta) * direction;
|
||||
startIndex += shift;
|
||||
if (endIndex == -1) {
|
||||
endIndex = this.props.traceCount + shift;
|
||||
} else {
|
||||
endIndex += shift;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize the computed indexes.
|
||||
// start can't be lower than zero
|
||||
startIndex = Math.max(0, startIndex);
|
||||
if (endIndex != -1) {
|
||||
// end can't be lower than start + 1
|
||||
endIndex = Math.max(startIndex + 1, endIndex);
|
||||
// end also can't be higher than the total number of traces
|
||||
if (endIndex >= this.props.traceCount) {
|
||||
// -1 means, there is no end filtering
|
||||
endIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateIndexes({
|
||||
startIndex,
|
||||
endIndex,
|
||||
});
|
||||
}
|
||||
|
||||
updateIndexes({ startIndex, endIndex }, nextProps = this.props) {
|
||||
const renderedTraceCount =
|
||||
(endIndex == -1 ? nextProps.traceCount : endIndex) - startIndex;
|
||||
this.setState({
|
||||
startIndex,
|
||||
endIndex,
|
||||
renderedTraceCount,
|
||||
});
|
||||
if (this.tooltip) {
|
||||
this.tooltip.hide();
|
||||
}
|
||||
}
|
||||
|
||||
focusOnTrace(traceIndex) {
|
||||
const lastTraceIndex = findLastTraceIndex(
|
||||
this.props.traceChildren,
|
||||
traceIndex
|
||||
);
|
||||
this.updateIndexes({
|
||||
startIndex: traceIndex,
|
||||
endIndex: lastTraceIndex,
|
||||
});
|
||||
}
|
||||
|
||||
resetZoom() {
|
||||
this.updateIndexes({
|
||||
startIndex: 0,
|
||||
endIndex: -1,
|
||||
});
|
||||
}
|
||||
|
||||
tracePositionInPercent(traceIndex) {
|
||||
return Math.round(
|
||||
((traceIndex - this.state.startIndex) / this.state.renderedTraceCount) *
|
||||
100
|
||||
);
|
||||
}
|
||||
|
||||
renderMutationsInSlider() {
|
||||
const { mutationTraces, allTraces } = this.props;
|
||||
const { startIndex, endIndex } = this.state;
|
||||
|
||||
const displayedMutationTraces = [];
|
||||
for (const traceIndex of mutationTraces) {
|
||||
if (
|
||||
traceIndex >= startIndex &&
|
||||
(endIndex == -1 || traceIndex <= endIndex)
|
||||
) {
|
||||
displayedMutationTraces.push(traceIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return displayedMutationTraces.map(traceIndex => {
|
||||
const symbol = {
|
||||
add: "+",
|
||||
attributes: "=",
|
||||
remove: "-",
|
||||
};
|
||||
const trace = allTraces[traceIndex];
|
||||
const mutationType = trace[TRACER_FIELDS_INDEXES.DOM_MUTATION_TYPE];
|
||||
return div(
|
||||
{
|
||||
className: `tracer-slider-mutation`,
|
||||
"data-trace-index": traceIndex,
|
||||
style: {
|
||||
top: `${this.tracePositionInPercent(traceIndex)}%`,
|
||||
},
|
||||
onClick: event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.selectTrace(traceIndex);
|
||||
},
|
||||
},
|
||||
symbol[mutationType]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
renderEventsInSlider() {
|
||||
const { topTraces, allTraces, traceChildren } = this.props;
|
||||
const { startIndex, endIndex } = this.state;
|
||||
|
||||
const displayedTraceEvents = [];
|
||||
for (const traceIndex of topTraces) {
|
||||
// Match the last event index in order to allow showing partial event
|
||||
// which may not be complete at the beginning of the record when we are zoomed.
|
||||
const lastTraceIndex = findLastTraceIndex(traceChildren, traceIndex);
|
||||
if (
|
||||
lastTraceIndex >= startIndex &&
|
||||
(endIndex == -1 || traceIndex <= endIndex)
|
||||
) {
|
||||
displayedTraceEvents.push(traceIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return displayedTraceEvents.map(traceIndex => {
|
||||
const eventPositionInPercent = this.tracePositionInPercent(traceIndex);
|
||||
const lastTraceIndex = findLastTraceIndex(traceChildren, traceIndex);
|
||||
const eventHeightInPercent = Math.round(
|
||||
((lastTraceIndex - traceIndex) / this.state.renderedTraceCount) * 100
|
||||
);
|
||||
const trace = allTraces[traceIndex];
|
||||
if (trace[TRACER_FIELDS_INDEXES.TYPE] != "event") {
|
||||
return null;
|
||||
}
|
||||
const eventName = trace[TRACER_FIELDS_INDEXES.EVENT_NAME];
|
||||
const eventType = getEventClassNameFromTraceEventName(eventName);
|
||||
return div({
|
||||
className: `tracer-slider-event ${eventType}`,
|
||||
"data-trace-index": traceIndex,
|
||||
style: {
|
||||
top: `${eventPositionInPercent}%`,
|
||||
height: `${Math.max(
|
||||
Math.min(eventHeightInPercent, 100 - eventPositionInPercent),
|
||||
1
|
||||
)}%`,
|
||||
},
|
||||
onClick: event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.props.selectTrace(traceIndex);
|
||||
},
|
||||
onDoubleClick: () => {
|
||||
this.focusOnTrace(traceIndex);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderVerticalSliders() {
|
||||
if (!this.props.traceCount) {
|
||||
// Always return the top element so that componentDidMount can register its wheel listener
|
||||
return div({
|
||||
className: "tracer-timeline hidden",
|
||||
ref: "timeline",
|
||||
onWheel: this.onSliderWheel,
|
||||
});
|
||||
}
|
||||
|
||||
const { selectedTraceIndex } = this.props;
|
||||
|
||||
const { startIndex, endIndex } = this.state;
|
||||
|
||||
let selectedHighlightHeight;
|
||||
if (selectedTraceIndex > startIndex + this.state.renderedTraceCount) {
|
||||
selectedHighlightHeight = 100;
|
||||
} else if (selectedTraceIndex < startIndex) {
|
||||
selectedHighlightHeight = 0;
|
||||
} else {
|
||||
selectedHighlightHeight = this.tracePositionInPercent(selectedTraceIndex);
|
||||
}
|
||||
|
||||
const classnames = [];
|
||||
if (startIndex > 0) {
|
||||
classnames.push("cut-start");
|
||||
}
|
||||
if (endIndex != -1) {
|
||||
classnames.push("cut-end");
|
||||
}
|
||||
if (selectedTraceIndex) {
|
||||
if (selectedTraceIndex < startIndex) {
|
||||
classnames.push("selected-before");
|
||||
} else if (endIndex != -1 && selectedTraceIndex > endIndex) {
|
||||
classnames.push("selected-after");
|
||||
}
|
||||
}
|
||||
return div(
|
||||
{
|
||||
className: "tracer-timeline",
|
||||
ref: "timeline",
|
||||
onWheel: this.onSliderWheel,
|
||||
},
|
||||
div(
|
||||
{
|
||||
className: `tracer-slider-box ${classnames.join(" ")}`,
|
||||
},
|
||||
div(
|
||||
{
|
||||
className: "tracer-slice-slider ",
|
||||
ref: "sliceSlider",
|
||||
onClick: this.onSliderClick,
|
||||
style: {
|
||||
"--slider-bar-progress": `${selectedHighlightHeight}%`,
|
||||
},
|
||||
},
|
||||
selectedTraceIndex
|
||||
? div({
|
||||
className: "tracer-slider-bar",
|
||||
})
|
||||
: null,
|
||||
selectedTraceIndex &&
|
||||
selectedTraceIndex >= startIndex &&
|
||||
selectedTraceIndex <= startIndex + this.state.renderedTraceCount
|
||||
? div({
|
||||
className: "tracer-slider-position",
|
||||
})
|
||||
: null,
|
||||
this.renderEventsInSlider(),
|
||||
this.renderMutationsInSlider()
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const isZoomed = this.state.renderedTraceCount != this.props.traceCount;
|
||||
|
||||
return div(
|
||||
{
|
||||
className: "tracer-container",
|
||||
style: {
|
||||
"--tree-node-height": `${TREE_NODE_HEIGHT}px`,
|
||||
},
|
||||
},
|
||||
div(
|
||||
{ className: "tracer-toolbar" },
|
||||
this.props.traceCount == 0
|
||||
? div(
|
||||
{
|
||||
className: "tracer-experimental-notice",
|
||||
},
|
||||
"This panel is experimental. It may change, regress, be dropped or replaced."
|
||||
)
|
||||
: null,
|
||||
this.renderSearchInput()
|
||||
),
|
||||
isZoomed
|
||||
? div(
|
||||
{
|
||||
className: "tracer-timeline-toolbar",
|
||||
},
|
||||
button(
|
||||
{
|
||||
className: "tracer-reset-zoom",
|
||||
onClick: this.resetZoom,
|
||||
},
|
||||
"Reset zoom"
|
||||
)
|
||||
)
|
||||
: null,
|
||||
this.renderVerticalSliders(),
|
||||
this.renderTree()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk through the call tree to find the very last children frame
|
||||
* and return its trace index.
|
||||
*
|
||||
* @param {Object} traceChildren
|
||||
* The reducer data containing children trace indexes for all the traces.
|
||||
* @param {Number} traceIndex
|
||||
*/
|
||||
function findLastTraceIndex(traceChildren, traceIndex) {
|
||||
const children = traceChildren[traceIndex];
|
||||
if (!children.length) {
|
||||
return traceIndex;
|
||||
}
|
||||
return findLastTraceIndex(traceChildren, children.at(-1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store in the `results` attribute all following siblings for a given trace,
|
||||
* as well as for its parents, that, recursively up to the top traces.
|
||||
*
|
||||
* @param {Object} traceParents
|
||||
* The reducer data containing parent trace index for all the traces.
|
||||
* @param {Object} traceChildren
|
||||
* The reducer data containing children trace indexes for all the traces.
|
||||
* @param {Number} traceIndex
|
||||
* @param {Array} results
|
||||
*/
|
||||
function collectAllSiblings(traceParents, traceChildren, traceIndex, results) {
|
||||
const parentIndex = traceParents[traceIndex];
|
||||
if (parentIndex != null) {
|
||||
const parentChildren = traceChildren[parentIndex];
|
||||
const indexInItsParent = parentChildren.indexOf(traceIndex);
|
||||
const siblingTraces = parentChildren.slice(indexInItsParent + 1);
|
||||
if (siblingTraces.length) {
|
||||
results.push(...siblingTraces);
|
||||
}
|
||||
collectAllSiblings(traceParents, traceChildren, parentIndex, results);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the TRACER_FIELDS_INDEXES.EVENT_NAME field of a trace,
|
||||
* return the classname to use for a given event trace.
|
||||
*
|
||||
* @param {String} eventName
|
||||
*/
|
||||
function getEventClassNameFromTraceEventName(eventName) {
|
||||
let eventType = "other";
|
||||
// `eventName` looks like this: `DOM | ${domEventName}`
|
||||
// Use a space before each event name in order to be sure to match the beginning
|
||||
// of the dom event name.
|
||||
if (eventName.includes(" mouse") || eventName.includes(" click")) {
|
||||
eventType = "mouse";
|
||||
} else if (eventName.includes(" key")) {
|
||||
eventType = "key";
|
||||
}
|
||||
return eventType;
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
isTracing: getIsCurrentlyTracing(state),
|
||||
topTraces: getTopTraces(state),
|
||||
allTraces: getAllTraces(state),
|
||||
traceChildren: getTraceChildren(state),
|
||||
traceParents: getTraceParents(state),
|
||||
frames: getTraceFrames(state),
|
||||
mutationTraces: getAllMutationTraces(state),
|
||||
traceCount: getAllTraceCount(state),
|
||||
selectedTraceIndex: getSelectedTraceIndex(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, {
|
||||
selectTrace: actions.selectTrace,
|
||||
})(Tracer);
|
@ -7,24 +7,30 @@ import PropTypes from "devtools/client/shared/vendor/react-prop-types";
|
||||
|
||||
import actions from "../../actions/index";
|
||||
import { getSelectedPrimaryPaneTab } from "../../selectors/index";
|
||||
import { prefs } from "../../utils/prefs";
|
||||
import { prefs, features } from "../../utils/prefs";
|
||||
import { connect } from "devtools/client/shared/vendor/react-redux";
|
||||
import { primaryPaneTabs } from "../../constants";
|
||||
|
||||
import Outline from "./Outline";
|
||||
import SourcesTree from "./SourcesTree";
|
||||
import ProjectSearch from "./ProjectSearch";
|
||||
import Tracer from "./Tracer";
|
||||
const AppErrorBoundary = require("resource://devtools/client/shared/components/AppErrorBoundary.js");
|
||||
|
||||
const {
|
||||
TabPanel,
|
||||
Tabs,
|
||||
} = require("resource://devtools/client/shared/components/tabs/Tabs.js");
|
||||
|
||||
// Note that the following list should follow the same order as displayed
|
||||
const tabs = [
|
||||
primaryPaneTabs.SOURCES,
|
||||
primaryPaneTabs.OUTLINE,
|
||||
primaryPaneTabs.PROJECT_SEARCH,
|
||||
];
|
||||
if (features.javascriptTracing) {
|
||||
tabs.push(primaryPaneTabs.TRACER);
|
||||
}
|
||||
|
||||
class PrimaryPanes extends Component {
|
||||
constructor(props) {
|
||||
@ -112,7 +118,30 @@ class PrimaryPanes extends Component {
|
||||
title: L10N.getStr("search.header"),
|
||||
},
|
||||
React.createElement(ProjectSearch, null)
|
||||
)
|
||||
),
|
||||
features.javascriptTracing
|
||||
? React.createElement(
|
||||
TabPanel,
|
||||
{
|
||||
id: "tracer-tab",
|
||||
key: `tracer-tab${
|
||||
selectedTab === primaryPaneTabs.TRACER ? "-selected" : ""
|
||||
}`,
|
||||
className: "tab tracer-tab",
|
||||
title: L10N.getStr("tracer.header"),
|
||||
},
|
||||
// As the tracer is an application on its own (and is prototypish)
|
||||
// let's encapsulate it to track its own exceptions.
|
||||
React.createElement(
|
||||
AppErrorBoundary,
|
||||
{
|
||||
componentName: "Debugger",
|
||||
panel: "JavaScript Tracer",
|
||||
},
|
||||
React.createElement(Tracer)
|
||||
)
|
||||
)
|
||||
: null
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -12,4 +12,5 @@ CompiledModules(
|
||||
"ProjectSearch.js",
|
||||
"SourcesTree.js",
|
||||
"SourcesTreeItem.js",
|
||||
"Tracer.js",
|
||||
)
|
||||
|
@ -50,7 +50,9 @@ export class SearchInput extends Component {
|
||||
super(props);
|
||||
this.state = {
|
||||
history: [],
|
||||
excludePatterns: props.searchOptions.excludePatterns,
|
||||
excludePatterns: this.props.showSearchModifiers
|
||||
? props.searchOptions.excludePatterns
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -12,6 +12,7 @@ export const primaryPaneTabs = {
|
||||
SOURCES: "sources",
|
||||
OUTLINE: "outline",
|
||||
PROJECT_SEARCH: "project",
|
||||
TRACER: "tracer",
|
||||
};
|
||||
|
||||
export const markerTypes = {
|
||||
|
@ -37,6 +37,7 @@
|
||||
@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/OutlineFilter.css");
|
||||
@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/ProjectSearch.css");
|
||||
@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/Sources.css");
|
||||
@import url("chrome://devtools/content/debugger/src/components/PrimaryPanes/Tracer.css");
|
||||
@import url("chrome://devtools/content/debugger/src/components/QuickOpenModal.css");
|
||||
@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css");
|
||||
@import url("chrome://devtools/content/debugger/src/components/SecondaryPanes/CommandBar.css");
|
||||
|
@ -25,6 +25,7 @@ import eventListenerBreakpoints, {
|
||||
initialEventListenerState,
|
||||
} from "./event-listeners";
|
||||
import exceptions, { initialExceptionsState } from "./exceptions";
|
||||
import tracerFrames from "./tracer-frames";
|
||||
|
||||
import { objectInspector } from "devtools/client/shared/components/reps/index";
|
||||
|
||||
@ -52,6 +53,7 @@ export function initialState() {
|
||||
objectInspector: objectInspector.reducer.initialOIState(),
|
||||
eventListenerBreakpoints: initialEventListenerState(),
|
||||
exceptions: initialExceptionsState(),
|
||||
tracerFrames: {},
|
||||
};
|
||||
}
|
||||
|
||||
@ -73,4 +75,5 @@ export default {
|
||||
objectInspector: objectInspector.reducer.default,
|
||||
eventListenerBreakpoints,
|
||||
exceptions,
|
||||
tracerFrames,
|
||||
};
|
||||
|
@ -22,5 +22,6 @@ CompiledModules(
|
||||
"sources-tree.js",
|
||||
"tabs.js",
|
||||
"threads.js",
|
||||
"tracer-frames.js",
|
||||
"ui.js",
|
||||
)
|
||||
|
233
devtools/client/debugger/src/reducers/tracer-frames.js
Normal file
233
devtools/client/debugger/src/reducers/tracer-frames.js
Normal file
@ -0,0 +1,233 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
const {
|
||||
TRACER_FIELDS_INDEXES,
|
||||
} = require("resource://devtools/server/actors/tracer.js");
|
||||
|
||||
function initialState() {
|
||||
return {
|
||||
// These fields are mutable as they are large arrays and UI will rerender based on their size
|
||||
|
||||
// The three next array are always of the same size.
|
||||
// List of all trace resources, as defined by the server codebase (See the TracerActor)
|
||||
mutableTraces: [],
|
||||
// Array of arrays. This is of the same size as mutableTraces.
|
||||
// Store the indexes within mutableTraces of each children matching the same index in mutableTraces.
|
||||
mutableChildren: [],
|
||||
// Indexes of parents within mutableTraces.
|
||||
mutableParents: [],
|
||||
|
||||
// Frames are also a trace resources, but they are stored in a dedicated array.
|
||||
mutableFrames: [],
|
||||
|
||||
// List of indexes within mutableTraces of top level trace, without any parent.
|
||||
mutableTopTraces: [],
|
||||
|
||||
// List of all trace resources indexes within mutableTraces which are about dom mutations
|
||||
mutableMutationTraces: [],
|
||||
|
||||
// Index of the currently selected trace within `mutableTraces`.
|
||||
selectedTraceIndex: null,
|
||||
};
|
||||
}
|
||||
|
||||
function update(state = initialState(), action) {
|
||||
switch (action.type) {
|
||||
case "TRACING_TOGGLED": {
|
||||
if (action.enabled) {
|
||||
return initialState();
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
case "TRACING_CLEAR": {
|
||||
return initialState();
|
||||
}
|
||||
|
||||
case "ADD_TRACES": {
|
||||
addTraces(state, action.traces);
|
||||
return { ...state };
|
||||
}
|
||||
|
||||
case "SELECT_TRACE": {
|
||||
if (
|
||||
action.traceIndex < 0 ||
|
||||
action.traceIndex >= state.mutableTraces.length
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
selectedTraceIndex: action.traceIndex,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_SELECTED_LOCATION": {
|
||||
// Traces are reference to the generated location only, so ignore any original source being selected
|
||||
// and wait for SET_GENERATED_SELECTED_LOCATION instead.
|
||||
if (action.location.source.isOriginal) return state;
|
||||
|
||||
// Ignore if the currently selected trace matches the new location.
|
||||
if (
|
||||
state.selectedTrace &&
|
||||
locationMatchTrace(action.location, state.selectedTrace)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Lookup for a trace matching the newly selected location
|
||||
for (const trace of state.mutableTraces) {
|
||||
if (locationMatchTrace(action.location, trace)) {
|
||||
return {
|
||||
...state,
|
||||
selectedTrace: trace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedTrace: null,
|
||||
};
|
||||
}
|
||||
|
||||
case "SET_GENERATED_SELECTED_LOCATION": {
|
||||
// When selecting an original location, we have to wait for the newly selected original location
|
||||
// to be mapped to a generated location so that we can find a matching trace.
|
||||
|
||||
// Ignore if the currently selected trace matches the new location.
|
||||
if (
|
||||
state.selectedTrace &&
|
||||
locationMatchTrace(action.generatedLocation, state.selectedTrace)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
// Lookup for a trace matching the newly selected location
|
||||
for (const trace of state.mutableTraces) {
|
||||
if (locationMatchTrace(action.generatedLocation, trace)) {
|
||||
return {
|
||||
...state,
|
||||
selectedTrace: trace,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedTrace: null,
|
||||
};
|
||||
}
|
||||
|
||||
case "CLEAR_SELECTED_LOCATION": {
|
||||
return {
|
||||
...state,
|
||||
selectedTrace: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
return state;
|
||||
}
|
||||
|
||||
function addTraces(state, traces) {
|
||||
const {
|
||||
mutableTraces,
|
||||
mutableMutationTraces,
|
||||
mutableFrames,
|
||||
mutableTopTraces,
|
||||
mutableChildren,
|
||||
mutableParents,
|
||||
} = state;
|
||||
|
||||
function matchParent(traceIndex, depth) {
|
||||
// The very last element is the one matching traceIndex,
|
||||
// so pick the one added just before.
|
||||
// We consider that traces are reported by the server in the execution order.
|
||||
let idx = mutableTraces.length - 2;
|
||||
while (idx != null) {
|
||||
const trace = mutableTraces[idx];
|
||||
if (!trace) {
|
||||
break;
|
||||
}
|
||||
const currentDepth = trace[TRACER_FIELDS_INDEXES.DEPTH];
|
||||
if (currentDepth < depth) {
|
||||
mutableChildren[idx].push(traceIndex);
|
||||
mutableParents.push(idx);
|
||||
return;
|
||||
}
|
||||
idx = mutableParents[idx];
|
||||
}
|
||||
|
||||
// If no parent was found, flag it as top level trace
|
||||
mutableTopTraces.push(traceIndex);
|
||||
mutableParents.push(null);
|
||||
}
|
||||
for (const traceResource of traces) {
|
||||
// For now, only consider traces from the top level target/thread
|
||||
if (!traceResource.targetFront.isTopLevel) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const type = traceResource[TRACER_FIELDS_INDEXES.TYPE];
|
||||
|
||||
switch (type) {
|
||||
case "frame": {
|
||||
// Store the object used by SmartTraces
|
||||
mutableFrames.push({
|
||||
functionDisplayName: traceResource[TRACER_FIELDS_INDEXES.FRAME_NAME],
|
||||
source: traceResource[TRACER_FIELDS_INDEXES.FRAME_URL],
|
||||
sourceId: traceResource[TRACER_FIELDS_INDEXES.FRAME_SOURCEID],
|
||||
line: traceResource[TRACER_FIELDS_INDEXES.FRAME_LINE],
|
||||
column: traceResource[TRACER_FIELDS_INDEXES.FRAME_COLUMN],
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case "enter": {
|
||||
const traceIndex = mutableTraces.length;
|
||||
mutableTraces.push(traceResource);
|
||||
mutableChildren.push([]);
|
||||
const depth = traceResource[TRACER_FIELDS_INDEXES.DEPTH];
|
||||
matchParent(traceIndex, depth);
|
||||
break;
|
||||
}
|
||||
|
||||
case "exit": {
|
||||
// The sidebar doesn't use this information yet
|
||||
break;
|
||||
}
|
||||
|
||||
case "dom-mutation": {
|
||||
const traceIndex = mutableTraces.length;
|
||||
mutableTraces.push(traceResource);
|
||||
mutableChildren.push([]);
|
||||
mutableMutationTraces.push(traceIndex);
|
||||
|
||||
const depth = traceResource[TRACER_FIELDS_INDEXES.DEPTH];
|
||||
matchParent(traceIndex, depth);
|
||||
break;
|
||||
}
|
||||
|
||||
case "event": {
|
||||
const traceIndex = mutableTraces.length;
|
||||
mutableTraces.push(traceResource);
|
||||
mutableChildren.push([]);
|
||||
mutableParents.push(null);
|
||||
mutableTopTraces.push(traceIndex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function locationMatchTrace(location, trace) {
|
||||
return (
|
||||
trace.sourceId == location.sourceActor.id &&
|
||||
trace.lineNumber == location.line &&
|
||||
trace.columnNumber == location.column
|
||||
);
|
||||
}
|
||||
|
||||
export default update;
|
@ -26,6 +26,7 @@ export * from "./sources-tree";
|
||||
export * from "./sources";
|
||||
export * from "./tabs";
|
||||
export * from "./threads";
|
||||
export * from "./tracer";
|
||||
export * from "./ui";
|
||||
export {
|
||||
getVisibleBreakpoints,
|
||||
|
@ -26,6 +26,7 @@ CompiledModules(
|
||||
"sources.js",
|
||||
"tabs.js",
|
||||
"threads.js",
|
||||
"tracer.js",
|
||||
"visibleBreakpoints.js",
|
||||
"visibleColumnBreakpoints.js",
|
||||
"ui.js",
|
||||
|
@ -54,3 +54,7 @@ export function getThread(state, threadActor) {
|
||||
export function getIsThreadCurrentlyTracing(state, thread) {
|
||||
return state.threads.mutableTracingThreads.has(thread);
|
||||
}
|
||||
|
||||
export function getIsCurrentlyTracing(state) {
|
||||
return state.threads.mutableTracingThreads.size > 0;
|
||||
}
|
||||
|
28
devtools/client/debugger/src/selectors/tracer.js
Normal file
28
devtools/client/debugger/src/selectors/tracer.js
Normal file
@ -0,0 +1,28 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
export function getSelectedTraceIndex(state) {
|
||||
return state.tracerFrames?.selectedTraceIndex;
|
||||
}
|
||||
export function getTopTraces(state) {
|
||||
return state.tracerFrames?.mutableTopTraces || [];
|
||||
}
|
||||
export function getAllTraces(state) {
|
||||
return state.tracerFrames?.mutableTraces || [];
|
||||
}
|
||||
export function getTraceChildren(state) {
|
||||
return state.tracerFrames?.mutableChildren || [];
|
||||
}
|
||||
export function getTraceParents(state) {
|
||||
return state.tracerFrames?.mutableParents || [];
|
||||
}
|
||||
export function getTraceFrames(state) {
|
||||
return state.tracerFrames?.mutableFrames || [];
|
||||
}
|
||||
export function getAllMutationTraces(state) {
|
||||
return state.tracerFrames?.mutableMutationTraces || [];
|
||||
}
|
||||
export function getAllTraceCount(state) {
|
||||
return state.tracerFrames?.mutableTraces.length || 0;
|
||||
}
|
@ -292,6 +292,11 @@ skip-if = [
|
||||
"tsan", # Bug 1832135
|
||||
]
|
||||
|
||||
["browser_dbg-javascript-tracer-sidebar.js"]
|
||||
skip-if = [
|
||||
"tsan", # Bug 1832135
|
||||
]
|
||||
|
||||
["browser_dbg-javascript-tracer-values.js"]
|
||||
skip-if = [
|
||||
"tsan", # Bug 1832135
|
||||
|
@ -17,15 +17,9 @@ add_task(async function testTracingFunctionReturn() {
|
||||
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-function-return");
|
||||
|
||||
info("Enable tracing with function returns, but without values");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
const topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
invokeInTab("foo");
|
||||
await hasConsoleMessage(dbg, "⟶ interpreter λ foo");
|
||||
await hasConsoleMessage(dbg, "⟶ interpreter λ bar");
|
||||
@ -34,20 +28,11 @@ add_task(async function testTracingFunctionReturn() {
|
||||
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitForState(dbg, () => {
|
||||
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-log-values");
|
||||
|
||||
info("Re-enable with returned values");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
info("Wait for tracing to be re-enabled with logging of returned values");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
invokeInTab("foo");
|
||||
|
||||
await hasConsoleMessage(dbg, "⟶ interpreter λ foo");
|
||||
@ -70,9 +55,6 @@ add_task(async function testTracingFunctionReturn() {
|
||||
|
||||
info("Stop tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
await waitForState(dbg, () => {
|
||||
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
info("Toggle the two settings to the default value");
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-log-values");
|
||||
|
@ -54,12 +54,7 @@ add_task(async function testTracingOnNextInteraction() {
|
||||
);
|
||||
AccessibilityUtils.resetEnv();
|
||||
|
||||
let topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await hasConsoleMessage(dbg, "Started tracing to Web Console");
|
||||
|
||||
await hasConsoleMessage(dbg, "λ onmousedown");
|
||||
await hasConsoleMessage(dbg, "λ onclick");
|
||||
@ -85,15 +80,9 @@ add_task(async function testTracingOnNextInteraction() {
|
||||
await hasConsoleMessage(dbg, "λ foo");
|
||||
ok(true, "foo was traced as expected");
|
||||
|
||||
info("Stop tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitForState(dbg, () => {
|
||||
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
is(
|
||||
traceButton.getAttribute("aria-pressed"),
|
||||
"false",
|
||||
@ -116,17 +105,9 @@ add_task(async function testInteractionBetweenDebuggerAndConsole() {
|
||||
"data:text/html," + encodeURIComponent(`<script>${jsCode}</script>`)
|
||||
);
|
||||
|
||||
info("Enable the tracing via the debugger button");
|
||||
info("Enable the tracing via the toolbox button");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
const topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await hasConsoleMessage(dbg, "Started tracing to Web Console");
|
||||
|
||||
invokeInTab("foo");
|
||||
|
||||
await hasConsoleMessage(dbg, "λ foo");
|
||||
@ -136,10 +117,8 @@ add_task(async function testInteractionBetweenDebuggerAndConsole() {
|
||||
let msg = await evaluateExpressionInConsole(hud, ":trace", "console-api");
|
||||
is(msg.textContent.trim(), "Stopped tracing");
|
||||
|
||||
ok(
|
||||
!dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID),
|
||||
"Tracing is also reported as disabled in the debugger"
|
||||
);
|
||||
const button = dbg.toolbox.doc.getElementById("command-button-jstracer");
|
||||
await waitFor(() => !button.classList.contains("checked"));
|
||||
|
||||
info(
|
||||
"Clear the console output from the first tracing session started from the debugger"
|
||||
@ -154,24 +133,17 @@ add_task(async function testInteractionBetweenDebuggerAndConsole() {
|
||||
msg = await evaluateExpressionInConsole(hud, ":trace", "console-api");
|
||||
is(msg.textContent.trim(), "Started tracing to Web Console");
|
||||
|
||||
info("Wait for tracing to be also enabled in the debugger");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
ok(true, "Debugger also reports the tracing in progress");
|
||||
info("Wait for tracing to be also enabled in toolbox button");
|
||||
await waitFor(() => button.classList.contains("checked"));
|
||||
|
||||
invokeInTab("foo");
|
||||
|
||||
await hasConsoleMessage(dbg, "λ foo");
|
||||
|
||||
info("Disable the tracing via the debugger button");
|
||||
// togglejsTracer will assert that the console logged the "stopped tracing" message
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
info("Wait for tracing to be disabled per debugger button");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
info("Also wait for stop message in the console");
|
||||
await hasConsoleMessage(dbg, "Stopped tracing");
|
||||
info("Wait for tracing to be disabled per toolbox button");
|
||||
await waitFor(() => !button.classList.contains("checked"));
|
||||
});
|
||||
|
@ -22,15 +22,15 @@ add_task(async function testTracingOnNextLoad() {
|
||||
|
||||
let traceButton = dbg.toolbox.doc.getElementById("command-button-jstracer");
|
||||
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-next-load");
|
||||
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
ok(
|
||||
!traceButton.classList.contains("pending"),
|
||||
"Before toggling the trace button, it has no particular state"
|
||||
);
|
||||
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-next-load");
|
||||
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
info(
|
||||
"Wait for the split console to be automatically displayed when toggling this setting"
|
||||
);
|
||||
@ -70,12 +70,8 @@ add_task(async function testTracingOnNextLoad() {
|
||||
// Reload the page to trigger the tracer
|
||||
await reload(dbg);
|
||||
|
||||
let topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled after page reload");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await hasConsoleMessage(dbg, "Started tracing to Web Console");
|
||||
is(
|
||||
traceButton.getAttribute("aria-pressed"),
|
||||
"true",
|
||||
@ -94,14 +90,9 @@ add_task(async function testTracingOnNextLoad() {
|
||||
"The code ran before the reload isn't logged"
|
||||
);
|
||||
|
||||
info("Toggle OFF the tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await waitFor(() => {
|
||||
return !traceButton.classList.contains("active");
|
||||
}, "The tracer button is no longer active after stop request");
|
||||
|
@ -0,0 +1,98 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
|
||||
|
||||
// Tests the Javascript Tracing feature.
|
||||
|
||||
"use strict";
|
||||
|
||||
add_task(async function () {
|
||||
// This is preffed off for now, so ensure turning it on
|
||||
await pushPref("devtools.debugger.features.javascript-tracing", true);
|
||||
|
||||
const dbg = await initDebugger("doc-scripts.html");
|
||||
|
||||
info("Force the log method to be the debugger sidebar");
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-debugger-sidebar");
|
||||
|
||||
info("Enable the tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
is(
|
||||
dbg.selectors.getSelectedPrimaryPaneTab(),
|
||||
"tracer",
|
||||
"The tracer sidebar is automatically shown on start"
|
||||
);
|
||||
|
||||
const topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
const tracerMessage = findElementWithSelector(
|
||||
dbg,
|
||||
"#tracer-tab-panel .tracer-message"
|
||||
);
|
||||
is(tracerMessage.textContent, "Waiting for the first JavaScript executions");
|
||||
|
||||
invokeInTab("main");
|
||||
|
||||
info("Wait for the call tree to appear in the tracer panel");
|
||||
const tree = await waitForElementWithSelector(dbg, "#tracer-tab-panel .tree");
|
||||
|
||||
info("Wait for the expected traces to appear in the call tree");
|
||||
const traces = await waitFor(() => {
|
||||
const elements = tree.querySelectorAll(".trace-line");
|
||||
if (elements.length == 3) {
|
||||
return elements;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
is(traces[0].textContent, "λ main simple1.js:1:16");
|
||||
is(traces[1].textContent, "λ foo simple2.js:1:12");
|
||||
is(traces[2].textContent, "λ bar simple2.js:3:4");
|
||||
|
||||
// Trigger a click in the content page to verify we do trace DOM events
|
||||
BrowserTestUtils.synthesizeMouseAtCenter(
|
||||
"button",
|
||||
{},
|
||||
gBrowser.selectedBrowser
|
||||
);
|
||||
|
||||
const clickTrace = await waitFor(() =>
|
||||
tree.querySelector(".tracer-dom-event")
|
||||
);
|
||||
is(clickTrace.textContent, "DOM | click");
|
||||
|
||||
await BrowserTestUtils.synthesizeKey("x", {}, gBrowser.selectedBrowser);
|
||||
const keyTrace = await waitFor(() => {
|
||||
const elts = tree.querySelectorAll(".tracer-dom-event");
|
||||
if (elts.length == 2) {
|
||||
return elts[1];
|
||||
}
|
||||
return false;
|
||||
});
|
||||
is(keyTrace.textContent, "DOM | keypress");
|
||||
|
||||
// Assert the final content of the tree before stopping
|
||||
const finalTreeSize = 7;
|
||||
is(tree.querySelectorAll(".trace-line").length, finalTreeSize);
|
||||
|
||||
// Test Disabling tracing
|
||||
info("Disable the tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitForState(dbg, () => {
|
||||
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
invokeInTab("inline_script2");
|
||||
|
||||
// Let some time for the tracer to appear if we failed disabling the tracing
|
||||
await wait(1000);
|
||||
|
||||
info("Reset back to the default value");
|
||||
await toggleJsTracerMenuItem(dbg, "#jstracer-menu-item-console");
|
||||
});
|
@ -18,13 +18,6 @@ add_task(async function testTracingValues() {
|
||||
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
const topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
invokeInTab("foo");
|
||||
|
||||
await hasConsoleMessage(dbg, "λ foo()");
|
||||
|
@ -14,22 +14,9 @@ add_task(async function testTracingWorker() {
|
||||
const dbg = await initDebugger("doc-scripts.html");
|
||||
|
||||
info("Instantiate a worker");
|
||||
const { targetCommand } = dbg.toolbox.commands;
|
||||
let onAvailable;
|
||||
const onNewTarget = new Promise(resolve => {
|
||||
onAvailable = ({ targetFront }) => {
|
||||
resolve(targetFront);
|
||||
};
|
||||
});
|
||||
await targetCommand.watchTargets({
|
||||
types: [targetCommand.TYPES.FRAME],
|
||||
onAvailable,
|
||||
});
|
||||
await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
|
||||
content.worker = new content.Worker("simple-worker.js");
|
||||
});
|
||||
info("Wait for the worker target");
|
||||
const workerTarget = await onNewTarget;
|
||||
|
||||
await waitFor(
|
||||
() => findAllElements(dbg, "threadsPaneItems").length == 2,
|
||||
@ -40,12 +27,6 @@ add_task(async function testTracingWorker() {
|
||||
|
||||
info("Enable tracing on all threads");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
info("Wait for tracing to be enabled for the worker");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(
|
||||
workerTarget.threadFront.actorID
|
||||
);
|
||||
});
|
||||
|
||||
// `timer` is called within the worker via a setInterval of 1 second
|
||||
await hasConsoleMessage(dbg, "setIntervalCallback");
|
||||
|
@ -28,13 +28,6 @@ add_task(async function () {
|
||||
info("Enable the tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
const topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
|
||||
ok(
|
||||
dbg.toolbox.splitConsole,
|
||||
"Split console is automatically opened when tracing to the console"
|
||||
@ -134,11 +127,6 @@ add_task(async function () {
|
||||
// Test Disabling tracing
|
||||
info("Disable the tracing");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitForState(dbg, () => {
|
||||
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await hasConsoleMessage(dbg, "Stopped tracing");
|
||||
|
||||
invokeInTab("inline_script2");
|
||||
|
||||
@ -158,13 +146,6 @@ add_task(async function () {
|
||||
info("Re-enable the tracing after navigation");
|
||||
await toggleJsTracer(dbg.toolbox);
|
||||
|
||||
const newTopLevelThread =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
info("Wait for tracing to be re-enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(newTopLevelThread);
|
||||
});
|
||||
|
||||
invokeInTab("logMessage");
|
||||
|
||||
await hasConsoleMessage(dbg, "λ logMessage");
|
||||
@ -230,12 +211,8 @@ add_task(async function testPageKeyShortcut() {
|
||||
|
||||
const dbg = await initDebuggerWithAbsoluteURL("data:text/html,key-shortcut");
|
||||
|
||||
const topLevelThreadActorID =
|
||||
dbg.toolbox.commands.targetCommand.targetFront.threadFront.actorID;
|
||||
ok(
|
||||
!dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID),
|
||||
"Tracing is disabled on debugger opening"
|
||||
);
|
||||
const button = dbg.toolbox.doc.getElementById("command-button-jstracer");
|
||||
ok(!button.classList.contains("checked"), "The trace button is off on start");
|
||||
|
||||
info(
|
||||
"Focus the page in order to assert that the page keeps the focus when enabling the tracer"
|
||||
@ -262,9 +239,7 @@ add_task(async function testPageKeyShortcut() {
|
||||
});
|
||||
|
||||
info("Wait for tracing to be enabled");
|
||||
await waitForState(dbg, () => {
|
||||
return dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await waitFor(() => button.classList.contains("checked"));
|
||||
|
||||
is(
|
||||
Services.focus.focusedElement,
|
||||
@ -282,9 +257,7 @@ add_task(async function testPageKeyShortcut() {
|
||||
});
|
||||
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitForState(dbg, () => {
|
||||
return !dbg.selectors.getIsThreadCurrentlyTracing(topLevelThreadActorID);
|
||||
});
|
||||
await waitFor(() => !button.classList.contains("checked"));
|
||||
});
|
||||
|
||||
add_task(async function testPageKeyShortcutWithoutDebugger() {
|
||||
|
@ -70,7 +70,7 @@ add_task(async function testProjectSearchCloseOnNavigation() {
|
||||
|
||||
// Wait for the search to be updated against the new page
|
||||
await waitForSearchResults(dbg, 5);
|
||||
is(getExpandedResultsCount(dbg), 29);
|
||||
is(getExpandedResultsCount(dbg), 30);
|
||||
ok(
|
||||
!refreshButton.classList.contains("highlight"),
|
||||
"Refresh button is no longer highlighted after refreshing the search"
|
||||
|
@ -30,6 +30,7 @@
|
||||
// try to set a breakpoint on the line `var x = 3;` above.
|
||||
// See Bug 1592839.
|
||||
inline_script2 = function () { var x = 5; };
|
||||
window.onkeypress = function keyListener() { };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -646,6 +646,22 @@ exports.ToolboxButtons = [
|
||||
const menu = new Menu();
|
||||
const options = toolbox.commands.tracerCommand.getTracingOptions();
|
||||
const { logMethod } = options;
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "jstracer-menu-item-debugger-sidebar",
|
||||
label: l10n(
|
||||
"toolbox.buttons.jstracer.menu-item.trace-to-debugger-sidebar"
|
||||
),
|
||||
checked: logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR,
|
||||
type: "radio",
|
||||
click: () => {
|
||||
Services.prefs.setStringPref(
|
||||
"devtools.debugger.javascript-tracing-log-method",
|
||||
TRACER_LOG_METHODS.DEBUGGER_SIDEBAR
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
id: "jstracer-menu-item-console",
|
||||
|
@ -711,6 +711,9 @@ Toolbox.prototype = {
|
||||
const { logMethod } = this.commands.tracerCommand.getTracingOptions();
|
||||
if (logMethod == TRACER_LOG_METHODS.CONSOLE) {
|
||||
await this.openSplitConsole({ focusConsoleInput: false });
|
||||
} else if (logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) {
|
||||
const panel = await this.selectTool("jsdebugger");
|
||||
panel.showTracerSidebar();
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -318,6 +318,7 @@ devtools.jar:
|
||||
content/debugger/src/components/PrimaryPanes/OutlineFilter.css (debugger/src/components/PrimaryPanes/OutlineFilter.css)
|
||||
content/debugger/src/components/PrimaryPanes/ProjectSearch.css (debugger/src/components/PrimaryPanes/ProjectSearch.css)
|
||||
content/debugger/src/components/PrimaryPanes/Sources.css (debugger/src/components/PrimaryPanes/Sources.css)
|
||||
content/debugger/src/components/PrimaryPanes/Tracer.css (debugger/src/components/PrimaryPanes/Tracer.css)
|
||||
content/debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css (debugger/src/components/SecondaryPanes/Breakpoints/Breakpoints.css)
|
||||
content/debugger/src/components/SecondaryPanes/CommandBar.css (debugger/src/components/SecondaryPanes/CommandBar.css)
|
||||
content/debugger/src/components/SecondaryPanes/EventListeners.css (debugger/src/components/SecondaryPanes/EventListeners.css)
|
||||
|
@ -900,6 +900,9 @@ outline.header=Outline
|
||||
# LOCALIZATION NOTE (search.header): Search left sidebar header
|
||||
search.header=Search
|
||||
|
||||
# LOCALIZATION NOTE (tracer.header): Tracer left sidebar header
|
||||
tracer.header=Tracer
|
||||
|
||||
# LOCALIZATION NOTE (outline.placeholder): Placeholder text for the filter input
|
||||
# element
|
||||
outline.placeholder=Filter functions
|
||||
|
@ -255,6 +255,11 @@ toolbox.buttons.jstracer = JavaScript Tracer (%S)
|
||||
# The next keys starting with "trace" were moved from an existing file
|
||||
# they do not follow the typical toolbox.* naming in order to preserve existing translations.
|
||||
|
||||
# LOCALIZATION NOTE (toolbox.buttons.jstracer.menu-item.trace-to-debugger-sidebar): The label that is displayed in the context menu
|
||||
# of the trace button in the toolbox toolbar.
|
||||
# This is used to force logging JavaScript traces in the dedicated Debugger sidebar.
|
||||
toolbox.buttons.jstracer.menu-item.trace-to-debugger-sidebar=Trace in the debugger sidebar
|
||||
|
||||
# LOCALIZATION NOTE (traceInWebConsole): The label that is displayed in the context menu
|
||||
# of the trace button in the toolbox toolbar.
|
||||
# This is used to force logging JavaScript traces in the Web Console.
|
||||
|
@ -459,6 +459,10 @@ webconsole.message.commands.copyValueToClipboard=String was copied to clipboard.
|
||||
# Label displayed when :trace command was executed and the JavaScript tracer started to log to the web console.
|
||||
webconsole.message.commands.startTracingToWebConsole=Started tracing to Web Console
|
||||
|
||||
# LOCALIZATION NOTE (webconsole.message.commands.startTracingToDebuggerSidebar)
|
||||
# Label displayed when :trace command was executed and the JavaScript tracer started to log to the debugger sidebar.
|
||||
webconsole.message.commands.startTracingToDebuggerSidebar=Started tracing to Debugger Sidebar
|
||||
|
||||
# LOCALIZATION NOTE (webconsole.message.commands.startTracingToStdout)
|
||||
# Label displayed when :trace command was executed and the JavaScript tracer started to log to stdout.
|
||||
webconsole.message.commands.startTracingToStdout=Started tracing to stdout
|
||||
|
@ -259,10 +259,13 @@ class Frame extends Component {
|
||||
return {
|
||||
...sourceElConfig,
|
||||
onClick: e => {
|
||||
// We always need to prevent the default behavior of <a> link
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (onClick) {
|
||||
e.stopPropagation();
|
||||
|
||||
onClick(generatedLocation);
|
||||
onClick(generatedLocation);
|
||||
}
|
||||
},
|
||||
href: source,
|
||||
draggable: false,
|
||||
|
@ -300,7 +300,10 @@ class Tree extends Component {
|
||||
}
|
||||
|
||||
// FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
|
||||
UNSAFE_componentWillReceiveProps() {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.autoExpandDepth != this.props.autoExpandDepth) {
|
||||
this.setState({ seen: new Set() });
|
||||
}
|
||||
this._autoExpand();
|
||||
this._updateHeight();
|
||||
}
|
||||
@ -316,8 +319,10 @@ class Tree extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this._scrollItemIntoView();
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.shown != this.props.shown) {
|
||||
this._scrollItemIntoView();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
@ -498,7 +503,7 @@ class Tree extends Component {
|
||||
if (scrollTop >= elementTop + itemHeight) {
|
||||
scrollTo = elementTop;
|
||||
} else if (scrollTop + clientHeight <= elementTop) {
|
||||
scrollTo = elementTop + itemHeight - clientHeight;
|
||||
scrollTo = elementTop;
|
||||
}
|
||||
|
||||
if (scrollTo != undefined) {
|
||||
@ -1018,7 +1023,8 @@ class TreeNodeClass extends Component {
|
||||
"data-depth": this.props.depth,
|
||||
style: {
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
// This helps the CSS compute a margin based on the depth.
|
||||
"--tree-node-depth": this.props.depth,
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -91,11 +91,11 @@ window.onload = async function () {
|
||||
await forceRender(tree);
|
||||
|
||||
isRenderedTree(document.body.textContent, [
|
||||
"---K:false",
|
||||
"---L:false",
|
||||
"--F:false",
|
||||
"--G:false",
|
||||
"-C:false",
|
||||
"--H:false",
|
||||
"--I:false",
|
||||
], "Should render shown item correctly");
|
||||
|
||||
info("Test mid item shown when it's already rendered.");
|
||||
@ -103,11 +103,11 @@ window.onload = async function () {
|
||||
await forceRender(tree);
|
||||
|
||||
isRenderedTree(document.body.textContent, [
|
||||
"---L:false",
|
||||
"--F:false",
|
||||
"--G:false",
|
||||
"-C:false",
|
||||
"--H:false",
|
||||
"--I:false",
|
||||
], "Should render shown item correctly");
|
||||
|
||||
info("Test item that is not in traversal.");
|
||||
@ -115,11 +115,11 @@ window.onload = async function () {
|
||||
await forceRender(tree);
|
||||
|
||||
isRenderedTree(document.body.textContent, [
|
||||
"---L:false",
|
||||
"--F:false",
|
||||
"--G:false",
|
||||
"-C:false",
|
||||
"--H:false",
|
||||
"--I:false",
|
||||
], "Should render without changes");
|
||||
|
||||
info("Test item that is already shown.");
|
||||
|
@ -2391,7 +2391,46 @@ async function unregisterServiceWorker(workerUrl) {
|
||||
/**
|
||||
* Toggle the JavavaScript tracer via its toolbox toolbar button.
|
||||
*/
|
||||
function toggleJsTracer(toolbox) {
|
||||
async function toggleJsTracer(toolbox) {
|
||||
const { isTracingEnabled } = toolbox.commands.tracerCommand;
|
||||
const { logMethod, traceOnNextInteraction, traceOnNextLoad } =
|
||||
toolbox.commands.tracerCommand.getTracingOptions();
|
||||
const toolbarButton = toolbox.doc.getElementById("command-button-jstracer");
|
||||
toolbarButton.click();
|
||||
|
||||
const {
|
||||
TRACER_LOG_METHODS,
|
||||
} = require("resource://devtools/shared/specs/tracer.js");
|
||||
if (logMethod != TRACER_LOG_METHODS.CONSOLE) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We were tracing and just requested to stop it.
|
||||
// Wait for the stop message to appear in the console before clearing its content.
|
||||
// This simplifies writting tests toggling the tracer ON multiple times and checking
|
||||
// for the display of traces in the console.
|
||||
if (isTracingEnabled) {
|
||||
const { hud } = await toolbox.getPanel("webconsole");
|
||||
info("Wait for tracing to be disabled");
|
||||
await waitFor(() =>
|
||||
[...hud.ui.outputNode.querySelectorAll(".message")].some(msg =>
|
||||
msg.textContent.includes("Stopped tracing")
|
||||
)
|
||||
);
|
||||
|
||||
hud.ui.clearOutput();
|
||||
await waitFor(
|
||||
() => hud.ui.outputNode.querySelectorAll(".message").length === 0
|
||||
);
|
||||
} else {
|
||||
// We are enabling the tracing to the console, and the console may not be opened just yet.
|
||||
const { hud } = await toolbox.getPanelWhenReady("webconsole");
|
||||
if (!traceOnNextInteraction && !traceOnNextLoad) {
|
||||
await waitFor(() =>
|
||||
[...hud.ui.outputNode.querySelectorAll(".message")].some(msg =>
|
||||
msg.textContent.includes("Started tracing to Web Console")
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,13 @@ loader.lazyRequireGetter(
|
||||
true
|
||||
);
|
||||
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"TRACER_LOG_METHODS",
|
||||
"resource://devtools/shared/specs/tracer.js",
|
||||
true
|
||||
);
|
||||
|
||||
// URL Regex, common idioms:
|
||||
//
|
||||
// Lead-in (URL):
|
||||
@ -512,13 +519,17 @@ function transformTracerStateResource(stateResource) {
|
||||
const { targetFront, enabled, logMethod, timeStamp, reason } = stateResource;
|
||||
let message;
|
||||
if (enabled) {
|
||||
if (logMethod == "stdout") {
|
||||
if (logMethod == TRACER_LOG_METHODS.STDOUT) {
|
||||
message = l10n.getStr("webconsole.message.commands.startTracingToStdout");
|
||||
} else if (logMethod == "console") {
|
||||
message = l10n.getStr(
|
||||
"webconsole.message.commands.startTracingToWebConsole"
|
||||
);
|
||||
} else if (logMethod == "profiler") {
|
||||
} else if (logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR) {
|
||||
message = l10n.getStr(
|
||||
"webconsole.message.commands.startTracingToDebuggerSidebar"
|
||||
);
|
||||
} else if (logMethod == TRACER_LOG_METHODS.PROFILER) {
|
||||
message = l10n.getStr(
|
||||
"webconsole.message.commands.startTracingToProfiler"
|
||||
);
|
||||
|
@ -36,6 +36,12 @@ loader.lazyRequireGetter(
|
||||
true
|
||||
);
|
||||
const ZoomKeys = require("resource://devtools/client/shared/zoom-keys.js");
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"TRACER_LOG_METHODS",
|
||||
"resource://devtools/shared/specs/tracer.js",
|
||||
true
|
||||
);
|
||||
|
||||
const PREF_SIDEBAR_ENABLED = "devtools.webconsole.sidebarToggle";
|
||||
const PREF_BROWSERTOOLBOX_SCOPE = "devtools.browsertoolbox.scope";
|
||||
@ -472,6 +478,8 @@ class WebConsoleUI {
|
||||
return;
|
||||
}
|
||||
|
||||
const { logMethod } = this.hud.commands.tracerCommand.getTracingOptions();
|
||||
|
||||
const messages = [];
|
||||
for (const resource of resources) {
|
||||
const { TYPES } = this.hud.resourceCommand;
|
||||
@ -513,6 +521,12 @@ class WebConsoleUI {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
resource.resourceType === TYPES.JSTRACER_TRACE &&
|
||||
logMethod != TRACER_LOG_METHODS.CONSOLE
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (resource.resourceType === TYPES.NETWORK_EVENT_STACKTRACE) {
|
||||
this.networkDataProvider?.onStackTraceAvailable(resource);
|
||||
continue;
|
||||
|
@ -38,17 +38,35 @@ loader.lazyRequireGetter(
|
||||
true
|
||||
);
|
||||
|
||||
// Indexes of each data type within the array describing a frame
|
||||
// Indexes of each data type within the array describing a trace
|
||||
exports.TRACER_FIELDS_INDEXES = {
|
||||
// This is shared with all the data types
|
||||
TYPE: 0,
|
||||
|
||||
// Frame traces are slightly special and do not share any field with the other data types
|
||||
FRAME_IMPLEMENTATION: 1,
|
||||
FRAME_NAME: 2,
|
||||
FRAME_SOURCEID: 3,
|
||||
FRAME_LINE: 4,
|
||||
FRAME_COLUMN: 5,
|
||||
FRAME_URL: 6,
|
||||
|
||||
// These fields are shared with all but frame data types
|
||||
PREFIX: 1,
|
||||
FRAME_INDEX: 2,
|
||||
TIMESTAMP: 3,
|
||||
DEPTH: 4,
|
||||
|
||||
EVENT_NAME: 5,
|
||||
|
||||
ENTER_ARGS: 5,
|
||||
|
||||
EXIT_PARENT_FRAME_ID: 5,
|
||||
EXIT_RETURNED_VALUE: 6,
|
||||
EXIT_WHY: 7,
|
||||
|
||||
DOM_MUTATION_TYPE: 5,
|
||||
DOM_MUTATION_ELEMENT: 6,
|
||||
};
|
||||
|
||||
const VALID_LOG_METHODS = Object.values(TRACER_LOG_METHODS);
|
||||
@ -98,6 +116,7 @@ class TracerActor extends Actor {
|
||||
* Options used to configure JavaScriptTracer.
|
||||
* See `JavaScriptTracer.startTracing`.
|
||||
*/
|
||||
// eslint-disable-next-line complexity
|
||||
startTracing(options = {}) {
|
||||
if (options.logMethod && !VALID_LOG_METHODS.includes(options.logMethod)) {
|
||||
throw new Error(
|
||||
@ -140,6 +159,9 @@ class TracerActor extends Actor {
|
||||
ListenerClass = StdoutTracingListener;
|
||||
break;
|
||||
case TRACER_LOG_METHODS.CONSOLE:
|
||||
case TRACER_LOG_METHODS.DEBUGGER_SIDEBAR:
|
||||
// Console and debugger sidebar are both using JSTRACE_STATE/JSTRACE_TRACE resources
|
||||
// to receive tracing data.
|
||||
ListenerClass = ResourcesTracingListener;
|
||||
break;
|
||||
case TRACER_LOG_METHODS.PROFILER:
|
||||
|
@ -6,6 +6,13 @@
|
||||
|
||||
const EventEmitter = require("resource://devtools/shared/event-emitter.js");
|
||||
|
||||
loader.lazyRequireGetter(
|
||||
this,
|
||||
"TRACER_LOG_METHODS",
|
||||
"resource://devtools/shared/specs/tracer.js",
|
||||
true
|
||||
);
|
||||
|
||||
class TracerCommand extends EventEmitter {
|
||||
constructor({ commands }) {
|
||||
super();
|
||||
@ -41,9 +48,6 @@ class TracerCommand extends EventEmitter {
|
||||
if (resource.resourceType != this.#resourceCommand.TYPES.JSTRACER_STATE) {
|
||||
continue;
|
||||
}
|
||||
this.isTracingActive = resource.enabled;
|
||||
// In case the tracer is started without the DevTools frontend, also force it to be reported as enabled
|
||||
this.isTracingEnabled = resource.enabled;
|
||||
|
||||
// Clear the list of collected frames each time we start a new tracer record.
|
||||
// The tracer will reset its frame counter to zero on stop, but on the frontend
|
||||
@ -52,6 +56,17 @@ class TracerCommand extends EventEmitter {
|
||||
resource.targetFront.getJsTracerCollectedFramesArray().length = 0;
|
||||
}
|
||||
|
||||
if (
|
||||
resource.enabled == this.isTracingActive &&
|
||||
resource.enabled == this.isTracingEnabled
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.isTracingActive = resource.enabled;
|
||||
// In case the tracer is started without the DevTools frontend, also force it to be reported as enabled
|
||||
this.isTracingEnabled = resource.enabled;
|
||||
|
||||
this.emit("toggle");
|
||||
}
|
||||
};
|
||||
@ -64,11 +79,17 @@ class TracerCommand extends EventEmitter {
|
||||
* Configuration object.
|
||||
*/
|
||||
getTracingOptions() {
|
||||
const logMethod = Services.prefs.getStringPref(
|
||||
"devtools.debugger.javascript-tracing-log-method",
|
||||
""
|
||||
);
|
||||
return {
|
||||
logMethod: Services.prefs.getStringPref(
|
||||
"devtools.debugger.javascript-tracing-log-method",
|
||||
""
|
||||
),
|
||||
logMethod,
|
||||
// Force enabling DOM Mutation logging as soon as we selected the sidebar log output
|
||||
traceDOMMutations:
|
||||
logMethod == TRACER_LOG_METHODS.DEBUGGER_SIDEBAR
|
||||
? ["add", "attributes", "remove"]
|
||||
: null,
|
||||
traceValues: Services.prefs.getBoolPref(
|
||||
"devtools.debugger.javascript-tracing-values",
|
||||
false
|
||||
|
@ -15,6 +15,7 @@ types.addDictType("tracer.start.options", {
|
||||
traceValues: "boolean",
|
||||
traceOnNextInteraction: "boolean",
|
||||
traceOnNextLoad: "boolean",
|
||||
traceDOMMutations: "nullable:array:string",
|
||||
});
|
||||
|
||||
const tracerSpec = generateActorSpec({
|
||||
@ -35,6 +36,7 @@ const tracerSpec = generateActorSpec({
|
||||
exports.tracerSpec = tracerSpec;
|
||||
|
||||
const TRACER_LOG_METHODS = {
|
||||
DEBUGGER_SIDEBAR: "debugger-sidebar",
|
||||
STDOUT: "stdout",
|
||||
CONSOLE: "console",
|
||||
PROFILER: "profiler",
|
||||
|
Loading…
Reference in New Issue
Block a user