mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-29 15:52:07 +00:00
Bug 1581245 - Add a frame timeline to web replay
Differential Revision: https://phabricator.services.mozilla.com/D49698 --HG-- extra : moz-landing-system : lando
This commit is contained in:
parent
97ec12425c
commit
ba28db66a0
@ -26,6 +26,7 @@ import type {
|
||||
ThreadId,
|
||||
Context,
|
||||
ThreadContext,
|
||||
ExecutionPoint,
|
||||
} from "../../types";
|
||||
import type { ThunkArgs } from "../types";
|
||||
import type { Command } from "../../reducers/types";
|
||||
@ -71,6 +72,19 @@ export function command(cx: ThreadContext, type: Command) {
|
||||
};
|
||||
}
|
||||
|
||||
export function seekToPosition(position: ExecutionPoint) {
|
||||
return ({ dispatch, getState, client }: ThunkArgs) => {
|
||||
const cx = getThreadContext(getState());
|
||||
client.timeWarp(position);
|
||||
dispatch({
|
||||
type: "COMMAND",
|
||||
command: "timeWarp",
|
||||
status: "start",
|
||||
thread: cx.thread,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* StepIn
|
||||
* @memberof actions/pause
|
||||
|
@ -17,6 +17,7 @@ export {
|
||||
resume,
|
||||
rewind,
|
||||
reverseStepOver,
|
||||
seekToPosition,
|
||||
} from "./commands";
|
||||
export { fetchScopes } from "./fetchScopes";
|
||||
export { paused } from "./paused";
|
||||
@ -34,3 +35,4 @@ export {
|
||||
previewPausedLocation,
|
||||
clearPreviewPausedLocation,
|
||||
} from "./previewPausedLocation";
|
||||
export { setFramePositions } from "./setFramePositions";
|
||||
|
@ -147,6 +147,7 @@ async function expandFrames(
|
||||
id,
|
||||
displayName: originalFrame.displayName,
|
||||
location: originalFrame.location,
|
||||
index: frame.index,
|
||||
source: null,
|
||||
thread: frame.thread,
|
||||
scope: frame.scope,
|
||||
|
@ -22,5 +22,6 @@ CompiledModules(
|
||||
'previewPausedLocation.js',
|
||||
'resumed.js',
|
||||
'selectFrame.js',
|
||||
'setFramePositions.js',
|
||||
'skipPausing.js',
|
||||
)
|
||||
|
@ -22,7 +22,11 @@ export function previewPausedLocation(location: Location) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sourceLocation = { ...location, sourceId: source.id };
|
||||
const sourceLocation = {
|
||||
line: location.line,
|
||||
column: location.column,
|
||||
sourceId: source.id,
|
||||
};
|
||||
dispatch(selectLocation(cx, sourceLocation));
|
||||
|
||||
dispatch({
|
||||
|
@ -8,6 +8,7 @@ import { selectLocation } from "../sources";
|
||||
import { evaluateExpressions } from "../expressions";
|
||||
import { fetchScopes } from "./fetchScopes";
|
||||
import assert from "../../utils/assert";
|
||||
import { getCanRewind } from "../../reducers/threads";
|
||||
|
||||
import type { Frame, ThreadContext } from "../../types";
|
||||
import type { ThunkArgs } from "../types";
|
||||
@ -27,6 +28,10 @@ export function selectFrame(cx: ThreadContext, frame: Frame) {
|
||||
frame,
|
||||
});
|
||||
|
||||
if (getCanRewind(getState())) {
|
||||
client.fetchAncestorFramePositions(frame.index);
|
||||
}
|
||||
|
||||
dispatch(selectLocation(cx, frame.location));
|
||||
dispatch(evaluateExpressions(cx));
|
||||
dispatch(fetchScopes(cx));
|
||||
|
@ -0,0 +1,23 @@
|
||||
/* 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/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
import type { ActorId, ExecutionPoint } from "../../types";
|
||||
import type { ThunkArgs } from "../types";
|
||||
|
||||
export function setFramePositions(
|
||||
positions: Array<ExecutionPoint>,
|
||||
frame: ActorId,
|
||||
thread: ActorId
|
||||
) {
|
||||
return ({ dispatch }: ThunkArgs) => {
|
||||
dispatch({
|
||||
type: "SET_FRAME_POSITIONS",
|
||||
positions,
|
||||
frame,
|
||||
thread,
|
||||
});
|
||||
};
|
||||
}
|
@ -13,6 +13,7 @@ import type {
|
||||
Why,
|
||||
ThreadContext,
|
||||
Previews,
|
||||
ExecutionPoint,
|
||||
} from "../../types";
|
||||
|
||||
import type { PromiseAction } from "../utils/middleware/promise";
|
||||
@ -161,4 +162,9 @@ export type PauseAction =
|
||||
+thread: string,
|
||||
+frame: Frame,
|
||||
+previews: Previews,
|
||||
|}
|
||||
| {|
|
||||
+type: "SET_FRAME_POSITIONS",
|
||||
+frame: Frame,
|
||||
+positions: Array<ExecutionPoint>,
|
||||
|};
|
||||
|
@ -25,6 +25,7 @@ import type {
|
||||
Range,
|
||||
Thread,
|
||||
ThreadType,
|
||||
ExecutionPoint,
|
||||
} from "../../types";
|
||||
|
||||
import type {
|
||||
@ -545,6 +546,14 @@ function getFrontByID(actorID: String) {
|
||||
return debuggerClient.getFrontByID(actorID);
|
||||
}
|
||||
|
||||
function timeWarp(position: ExecutionPoint) {
|
||||
currentThreadFront.timeWarp(position);
|
||||
}
|
||||
|
||||
function fetchAncestorFramePositions(index: number) {
|
||||
currentThreadFront.fetchAncestorFramePositions(index);
|
||||
}
|
||||
|
||||
const clientCommands = {
|
||||
autocomplete,
|
||||
blackBox,
|
||||
@ -592,6 +601,8 @@ const clientCommands = {
|
||||
detachWorkers,
|
||||
lookupTarget,
|
||||
getFrontByID,
|
||||
timeWarp,
|
||||
fetchAncestorFramePositions,
|
||||
};
|
||||
|
||||
export { setupCommands, clientCommands };
|
||||
|
@ -30,7 +30,11 @@ export function prepareSourcePayload(
|
||||
return { thread: client.actor, source };
|
||||
}
|
||||
|
||||
export function createFrame(thread: ThreadId, frame: FramePacket): ?Frame {
|
||||
export function createFrame(
|
||||
thread: ThreadId,
|
||||
frame: FramePacket,
|
||||
index: number = 0
|
||||
): ?Frame {
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
@ -50,6 +54,7 @@ export function createFrame(thread: ThreadId, frame: FramePacket): ?Frame {
|
||||
this: frame.this,
|
||||
source: null,
|
||||
scope: frame.environment,
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
@ -69,7 +74,9 @@ export function createPause(
|
||||
...packet,
|
||||
thread,
|
||||
frame: createFrame(thread, frame),
|
||||
frames: response.frames.map(createFrame.bind(null, thread)),
|
||||
frames: response.frames.map((currentFrame, i) =>
|
||||
createFrame(thread, currentFrame, i)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -103,10 +103,18 @@ function threadListChanged(type) {
|
||||
actions.updateThreads(type);
|
||||
}
|
||||
|
||||
function replayFramePositions(
|
||||
threadFront: ThreadFront,
|
||||
{ positions, frame, thread }: Object
|
||||
) {
|
||||
actions.setFramePositions(positions, frame, thread);
|
||||
}
|
||||
|
||||
const clientEvents = {
|
||||
paused,
|
||||
resumed,
|
||||
newSource,
|
||||
replayFramePositions,
|
||||
};
|
||||
|
||||
export { setupEvents, clientEvents, addThreadEventListeners };
|
||||
|
@ -186,6 +186,7 @@ export type Actions = {
|
||||
newQueuedSources: (QueuedSourceData[]) => void,
|
||||
fetchEventListeners: () => void,
|
||||
updateThreads: typeof actions.updateThreads,
|
||||
setFramePositions: typeof actions.setFramePositions,
|
||||
};
|
||||
|
||||
type ConsoleClient = {
|
||||
@ -385,6 +386,8 @@ export type ThreadFront = {
|
||||
getAvailableEventBreakpoints: () => Promise<EventListenerCategoryList>,
|
||||
skipBreakpoints: boolean => Promise<{| skip: boolean |}>,
|
||||
detach: () => Promise<void>,
|
||||
timeWarp: Function => Promise<*>,
|
||||
fetchAncestorFramePositions: Function => Promise<*>,
|
||||
};
|
||||
|
||||
export type Panel = {|
|
||||
|
@ -0,0 +1,48 @@
|
||||
.theme-light {
|
||||
--progress-playing-background: hsl(207, 100%, 97%);
|
||||
--progressbar-background: #ffffff;
|
||||
--replay-head-background: var(--purple-50);
|
||||
}
|
||||
|
||||
.theme-dark {
|
||||
--progress-playing-background: #071a2b;
|
||||
--progressbar-background: #0c0c0d;
|
||||
--replay-head-background: var(--theme-highlight-purple);
|
||||
}
|
||||
|
||||
.frame-timeline-container {
|
||||
border-bottom: 1px solid var(--theme-splitter-color);
|
||||
background-color: var(--accordion-header-background);
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.frame-timeline-bar {
|
||||
background-color: var(--progressbar-background);
|
||||
border: 1px solid var(--theme-splitter-color);
|
||||
width: 100%;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.frame-timeline-marker {
|
||||
background-color: var(--replay-head-background);
|
||||
display: inline-block;
|
||||
opacity: 0.4;
|
||||
width: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.scrubbing .frame-timeline-marker,
|
||||
.scrubbing .frame-timeline-bar,
|
||||
.scrubbing .frame-timeline-container,
|
||||
.frame-timeline-marker:hover {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.frame-timeline-progress {
|
||||
background-color: var(--progress-playing-background);
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
@ -0,0 +1,266 @@
|
||||
/* 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/>. */
|
||||
|
||||
// @flow
|
||||
|
||||
// import PropTypes from "prop-types";
|
||||
import React, { Component } from "react";
|
||||
import { isEqual } from "lodash";
|
||||
|
||||
import { connect } from "../../utils/connect";
|
||||
import { getFramePositions } from "../../selectors";
|
||||
import type { SourceLocation } from "../../types";
|
||||
|
||||
import { getSelectedLocation } from "../../reducers/sources";
|
||||
|
||||
import actions from "../../actions";
|
||||
|
||||
import classnames from "classnames";
|
||||
import "./FrameTimeline.css";
|
||||
|
||||
type Props = {
|
||||
framePositions: any,
|
||||
selectedLocation: ?SourceLocation,
|
||||
previewLocation: typeof actions.previewPausedLocation,
|
||||
seekToPosition: typeof actions.seekToPosition,
|
||||
};
|
||||
type OwnProps = {};
|
||||
|
||||
type State = {
|
||||
scrubbing: boolean,
|
||||
percentage: number,
|
||||
displayedLocation: any,
|
||||
};
|
||||
|
||||
function isSameLocation(
|
||||
frameLocation: SourceLocation,
|
||||
selectedLocation: ?SourceLocation
|
||||
) {
|
||||
if (!frameLocation.sourceUrl || !selectedLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
frameLocation.line === selectedLocation.line &&
|
||||
frameLocation.column === selectedLocation.column &&
|
||||
selectedLocation.sourceId.includes(frameLocation.sourceUrl)
|
||||
);
|
||||
}
|
||||
|
||||
function getBoundingClientRect(element: ?HTMLElement) {
|
||||
if (!element) {
|
||||
// $FlowIgnore
|
||||
return;
|
||||
}
|
||||
return element.getBoundingClientRect();
|
||||
}
|
||||
|
||||
class FrameTimeline extends Component<Props, State> {
|
||||
_timeline: ?HTMLElement;
|
||||
_marker: ?HTMLElement;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
state = {
|
||||
scrubbing: false,
|
||||
percentage: 0,
|
||||
displayedLocation: null,
|
||||
};
|
||||
|
||||
getProgress(clientX: number) {
|
||||
const { width, left } = getBoundingClientRect(this._timeline);
|
||||
const progress = ((clientX - left) / width) * 100;
|
||||
|
||||
if (progress < 0) {
|
||||
return 0;
|
||||
} else if (progress > 99) {
|
||||
return 99;
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
getPosition(percentage: ?number) {
|
||||
const { framePositions } = this.props;
|
||||
if (!framePositions) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!percentage) {
|
||||
percentage = this.state.percentage;
|
||||
}
|
||||
|
||||
const displayedPositions = framePositions.filter(
|
||||
point => point.position.kind === "OnStep"
|
||||
);
|
||||
const displayIndex = Math.floor(
|
||||
(percentage / 100) * displayedPositions.length
|
||||
);
|
||||
|
||||
return displayedPositions[displayIndex];
|
||||
}
|
||||
|
||||
displayPreview(percentage: number) {
|
||||
const { previewLocation } = this.props;
|
||||
|
||||
const position = this.getPosition(percentage);
|
||||
|
||||
if (position) {
|
||||
previewLocation(position.location);
|
||||
}
|
||||
}
|
||||
|
||||
onMouseDown = (event: SyntheticMouseEvent<>) => {
|
||||
const progress = this.getProgress(event.clientX);
|
||||
this.setState({ scrubbing: true, percentage: progress });
|
||||
};
|
||||
|
||||
onMouseUp = (event: SyntheticMouseEvent<>) => {
|
||||
const { seekToPosition, selectedLocation } = this.props;
|
||||
const { scrubbing } = this.state;
|
||||
|
||||
if (!scrubbing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const progress = this.getProgress(event.clientX);
|
||||
const position = this.getPosition(progress);
|
||||
this.setState({
|
||||
scrubbing: false,
|
||||
percentage: progress,
|
||||
displayedLocation: selectedLocation,
|
||||
});
|
||||
|
||||
if (position) {
|
||||
seekToPosition(position);
|
||||
}
|
||||
};
|
||||
|
||||
onMouseMove = (event: SyntheticMouseEvent<>) => {
|
||||
const { scrubbing } = this.state;
|
||||
|
||||
if (!scrubbing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { width, left } = getBoundingClientRect(this._timeline);
|
||||
const percentage = ((event.clientX - left) / width) * 100;
|
||||
|
||||
if (percentage < 0 || percentage > 100) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.displayPreview(percentage);
|
||||
this.setState({ percentage });
|
||||
};
|
||||
|
||||
getProgressForNewFrame() {
|
||||
const { framePositions, selectedLocation } = this.props;
|
||||
this.setState({ displayedLocation: selectedLocation });
|
||||
|
||||
const displayedPositions = framePositions.filter(
|
||||
point => point.position.kind === "OnStep"
|
||||
);
|
||||
const index = displayedPositions.findIndex(pos =>
|
||||
isSameLocation(pos.location, selectedLocation)
|
||||
);
|
||||
|
||||
let progress = 0;
|
||||
|
||||
if (index != -1) {
|
||||
progress = Math.floor((index / displayedPositions.length) * 100);
|
||||
this.setState({ percentage: progress });
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
getVisibleProgress() {
|
||||
const { percentage, displayedLocation, scrubbing } = this.state;
|
||||
const { selectedLocation } = this.props;
|
||||
|
||||
let progress = percentage;
|
||||
|
||||
if (!isEqual(displayedLocation, selectedLocation) && !scrubbing) {
|
||||
progress = this.getProgressForNewFrame();
|
||||
}
|
||||
|
||||
return progress;
|
||||
}
|
||||
|
||||
renderMarker() {
|
||||
return (
|
||||
<div className="frame-timeline-marker" ref={r => (this._marker = r)} />
|
||||
);
|
||||
}
|
||||
|
||||
renderProgress() {
|
||||
const progress = this.getVisibleProgress();
|
||||
let maxWidth = "100%";
|
||||
if (this._timeline && this._marker) {
|
||||
const timelineWidth = getBoundingClientRect(this._timeline).width;
|
||||
const markerWidth = getBoundingClientRect(this._timeline).width;
|
||||
maxWidth = timelineWidth - markerWidth - 2;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="frame-timeline-progress"
|
||||
style={{
|
||||
width: `${progress}%`,
|
||||
"max-width": maxWidth,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderTimeline() {
|
||||
return (
|
||||
<div
|
||||
className="frame-timeline-bar"
|
||||
onMouseDown={this.onMouseDown}
|
||||
ref={r => (this._timeline = r)}
|
||||
>
|
||||
{this.renderProgress()}
|
||||
{this.renderMarker()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { scrubbing } = this.state;
|
||||
const { framePositions } = this.props;
|
||||
|
||||
if (!framePositions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseUp={this.onMouseUp}
|
||||
onMouseMove={this.onMouseMove}
|
||||
className={classnames("frame-timeline-container", { scrubbing })}
|
||||
>
|
||||
{this.renderTimeline()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
framePositions: getFramePositions(state),
|
||||
selectedLocation: getSelectedLocation(state),
|
||||
};
|
||||
};
|
||||
|
||||
export default connect<Props, OwnProps, _, _, _, _>(
|
||||
mapStateToProps,
|
||||
{
|
||||
seekToPosition: actions.seekToPosition,
|
||||
previewLocation: actions.previewPausedLocation,
|
||||
}
|
||||
)(FrameTimeline);
|
@ -21,6 +21,7 @@ exports[`Frame getFrameTitle 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 10,
|
||||
"sourceId": "source",
|
||||
@ -74,6 +75,7 @@ exports[`Frame getFrameTitle 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 10,
|
||||
"sourceId": "source",
|
||||
@ -133,6 +135,7 @@ exports[`Frame library frame 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "3",
|
||||
"index": 0,
|
||||
"library": "backbone",
|
||||
"location": Object {
|
||||
"line": 12,
|
||||
@ -187,6 +190,7 @@ exports[`Frame library frame 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "3",
|
||||
"index": 0,
|
||||
"library": "backbone",
|
||||
"location": Object {
|
||||
"line": 12,
|
||||
@ -247,6 +251,7 @@ exports[`Frame user frame (not selected) 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 10,
|
||||
"sourceId": "source",
|
||||
@ -300,6 +305,7 @@ exports[`Frame user frame (not selected) 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 10,
|
||||
"sourceId": "source",
|
||||
@ -359,6 +365,7 @@ exports[`Frame user frame 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 10,
|
||||
"sourceId": "source",
|
||||
@ -412,6 +419,7 @@ exports[`Frame user frame 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 10,
|
||||
"sourceId": "source",
|
||||
|
@ -18,6 +18,7 @@ exports[`Frames Blackboxed Frames filters blackboxed frames 1`] = `
|
||||
"sourceId": "1",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
"sourceId": "1",
|
||||
@ -63,6 +64,7 @@ exports[`Frames Blackboxed Frames filters blackboxed frames 1`] = `
|
||||
"sourceId": "1",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
"sourceId": "1",
|
||||
@ -110,6 +112,7 @@ exports[`Frames Blackboxed Frames filters blackboxed frames 1`] = `
|
||||
"sourceId": "1",
|
||||
},
|
||||
"id": "3",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
"sourceId": "1",
|
||||
@ -155,6 +158,7 @@ exports[`Frames Blackboxed Frames filters blackboxed frames 1`] = `
|
||||
"sourceId": "1",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
"sourceId": "1",
|
||||
|
@ -24,6 +24,7 @@ exports[`Group displays a group 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
@ -97,6 +98,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -169,6 +171,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -216,6 +219,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
@ -276,6 +280,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "2",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -323,6 +328,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
@ -383,6 +389,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "3",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -430,6 +437,7 @@ exports[`Group passes the getFrameTitle prop to the Frame components 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
@ -495,6 +503,7 @@ exports[`Group renders group with anonymous functions 1`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -568,6 +577,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -640,6 +650,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "1",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -686,6 +697,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
@ -746,6 +758,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "2",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -792,6 +805,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
@ -852,6 +866,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "3",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 55,
|
||||
@ -898,6 +913,7 @@ exports[`Group renders group with anonymous functions 2`] = `
|
||||
"sourceId": "source",
|
||||
},
|
||||
"id": "frame",
|
||||
"index": 0,
|
||||
"library": "Back",
|
||||
"location": Object {
|
||||
"line": 4,
|
||||
|
@ -45,6 +45,7 @@ import XHRBreakpoints from "./XHRBreakpoints";
|
||||
import EventListeners from "./EventListeners";
|
||||
import DOMMutationBreakpoints from "./DOMMutationBreakpoints";
|
||||
import WhyPaused from "./WhyPaused";
|
||||
import FrameTimeline from "./FrameTimeline";
|
||||
|
||||
import Scopes from "./Scopes";
|
||||
|
||||
@ -520,6 +521,7 @@ class SecondaryPanes extends Component<Props, State> {
|
||||
return (
|
||||
<div className="secondary-panes-wrapper">
|
||||
<CommandBar horizontal={this.props.horizontal} />
|
||||
<FrameTimeline />
|
||||
<div
|
||||
className={classnames(
|
||||
"secondary-panes",
|
||||
|
@ -13,6 +13,7 @@ CompiledModules(
|
||||
'DOMMutationBreakpoints.js',
|
||||
'EventListeners.js',
|
||||
'Expressions.js',
|
||||
'FrameTimeline.js',
|
||||
'index.js',
|
||||
'Scopes.js',
|
||||
'Thread.js',
|
||||
@ -27,6 +28,7 @@ DevToolsModules(
|
||||
'DOMMutationBreakpoints.css',
|
||||
'EventListeners.css',
|
||||
'Expressions.css',
|
||||
'FrameTimeline.css',
|
||||
'Scopes.css',
|
||||
'SecondaryPanes.css',
|
||||
'Threads.css',
|
||||
|
@ -40,6 +40,7 @@
|
||||
@import url("./components/SecondaryPanes/EventListeners.css");
|
||||
@import url("./components/SecondaryPanes/DOMMutationBreakpoints.css");
|
||||
@import url("./components/SecondaryPanes/Expressions.css");
|
||||
@import url("./components/SecondaryPanes/FrameTimeline.css");
|
||||
@import url("./components/SecondaryPanes/Frames/Frames.css");
|
||||
@import url("./components/SecondaryPanes/Frames/Group.css");
|
||||
@import url("./components/SecondaryPanes/Scopes.css");
|
||||
|
@ -30,6 +30,7 @@ import type {
|
||||
ThreadContext,
|
||||
Previews,
|
||||
SourceLocation,
|
||||
ExecutionPoint,
|
||||
} from "../types";
|
||||
|
||||
export type Command =
|
||||
@ -46,6 +47,9 @@ type ThreadPauseState = {
|
||||
why: ?Why,
|
||||
isWaitingOnBreak: boolean,
|
||||
frames: ?(any[]),
|
||||
replayFramePositions: {
|
||||
[FrameId]: Array<ExecutionPoint>,
|
||||
},
|
||||
frameScopes: {
|
||||
generated: {
|
||||
[FrameId]: {
|
||||
@ -267,6 +271,14 @@ function update(
|
||||
});
|
||||
}
|
||||
|
||||
case "SET_FRAME_POSITIONS":
|
||||
return updateThreadState({
|
||||
replayFramePositions: {
|
||||
...threadState().replayFramePositions,
|
||||
[action.frame]: action.positions,
|
||||
},
|
||||
});
|
||||
|
||||
case "BREAK_ON_NEXT":
|
||||
return updateThreadState({ isWaitingOnBreak: true });
|
||||
|
||||
|
@ -53,6 +53,7 @@ export {
|
||||
getSelectedFrame,
|
||||
getSelectedFrames,
|
||||
getVisibleSelectedFrame,
|
||||
getFramePositions,
|
||||
} from "./pause";
|
||||
|
||||
// eslint-disable-next-line import/named
|
||||
|
@ -57,3 +57,19 @@ export const getVisibleSelectedFrame: Selector<?{
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
export function getFramePositions(state: State) {
|
||||
const threadId = getCurrentThread(state);
|
||||
const currentThread = state.pause.threads[threadId];
|
||||
|
||||
if (
|
||||
!currentThread ||
|
||||
!currentThread.selectedFrameId ||
|
||||
!currentThread.replayFramePositions
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const currentFrameId = currentThread.selectedFrameId;
|
||||
return currentThread.replayFramePositions[currentFrameId];
|
||||
}
|
||||
|
@ -115,6 +115,20 @@ export type PendingLocation = {
|
||||
+sourceUrl?: string,
|
||||
};
|
||||
|
||||
export type ExecutionPoint = {
|
||||
+checkpoint: number,
|
||||
+location: PendingLocation,
|
||||
+position: ExecutionPointPosition,
|
||||
+progress: number,
|
||||
};
|
||||
|
||||
export type ExecutionPointPosition = {
|
||||
+frameIndex: number,
|
||||
+kind: string,
|
||||
+offset: number,
|
||||
+script: number,
|
||||
};
|
||||
|
||||
// Type of location used when setting breakpoints in the server. Exactly one of
|
||||
// { sourceUrl, sourceId } must be specified. Soon this will replace
|
||||
// SourceLocation and PendingLocation, and SourceActorLocation will be removed
|
||||
@ -243,6 +257,7 @@ export type Frame = {
|
||||
originalDisplayName?: string,
|
||||
originalVariables?: XScopeVariables,
|
||||
library?: string,
|
||||
index: number,
|
||||
};
|
||||
|
||||
export type ChromeFrame = {
|
||||
|
@ -165,7 +165,8 @@ function makeMockFrame(
|
||||
source: Source = makeMockSource("url"),
|
||||
scope: Scope = makeMockScope(),
|
||||
line: number = 4,
|
||||
displayName: string = `display-${id}`
|
||||
displayName: string = `display-${id}`,
|
||||
index: number = 0
|
||||
): Frame {
|
||||
const location = { sourceId: source.id, line };
|
||||
return {
|
||||
@ -177,6 +178,7 @@ function makeMockFrame(
|
||||
source,
|
||||
scope,
|
||||
this: {},
|
||||
index,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1960,6 +1960,15 @@ const gControl = {
|
||||
return findFrameSteps(point);
|
||||
},
|
||||
|
||||
async findAncestorFrameEntryPoint(point, index) {
|
||||
const steps = await findFrameSteps(point);
|
||||
point = steps[0];
|
||||
while (index--) {
|
||||
point = await findParentFrameEntryPoint(point);
|
||||
}
|
||||
return point;
|
||||
},
|
||||
|
||||
// Return whether the active child is currently recording.
|
||||
childIsRecording() {
|
||||
return gActiveChild && gActiveChild.recording;
|
||||
|
@ -249,6 +249,14 @@ ReplayDebugger.prototype = {
|
||||
return this._control.findFrameSteps(point);
|
||||
},
|
||||
|
||||
async replayAncestorFramePositions(point, index) {
|
||||
const ancestor = await this._control.findAncestorFrameEntryPoint(
|
||||
point,
|
||||
index
|
||||
);
|
||||
return this._control.findFrameSteps(ancestor);
|
||||
},
|
||||
|
||||
replayRecordingEndpoint() {
|
||||
return this._control.recordingEndpoint();
|
||||
},
|
||||
|
@ -1533,7 +1533,11 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
|
||||
const point = this.dbg.replayCurrentExecutionPoint();
|
||||
packet.executionPoint = point;
|
||||
packet.recordingEndpoint = this.dbg.replayRecordingEndpoint();
|
||||
this.onFramePositions(point, frame);
|
||||
if (point) {
|
||||
this.dbg
|
||||
.replayFramePositions(point)
|
||||
.then(positions => this.onFramePositions(positions, frame));
|
||||
}
|
||||
}
|
||||
|
||||
if (poppedFrames) {
|
||||
@ -1543,12 +1547,20 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
|
||||
return packet;
|
||||
},
|
||||
|
||||
onFramePositions: function(point, frame) {
|
||||
if (!point) {
|
||||
return;
|
||||
fetchAncestorFramePositions: function(index) {
|
||||
const point = this.dbg.replayCurrentExecutionPoint();
|
||||
|
||||
let frame = this.youngestFrame;
|
||||
for (let i = 0; frame && i < index; i++) {
|
||||
frame = frame.older;
|
||||
}
|
||||
|
||||
this.dbg.replayFramePositions(point).then(positions => {
|
||||
this.dbg.replayAncestorFramePositions(point, index).then(points => {
|
||||
this.onFramePositions(points, frame);
|
||||
});
|
||||
},
|
||||
|
||||
onFramePositions: function(positions, frame) {
|
||||
if (!positions) {
|
||||
return;
|
||||
}
|
||||
@ -1563,6 +1575,7 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
|
||||
location = {
|
||||
line: offsetLocation.line,
|
||||
column: offsetLocation.column,
|
||||
sourceUrl: offsetLocation.url,
|
||||
};
|
||||
}
|
||||
return { ...mappedPoint, location };
|
||||
@ -1571,7 +1584,7 @@ const ThreadActor = ActorClassWithSpec(threadSpec, {
|
||||
this.emit("replayFramePositions", {
|
||||
positions: mappedPositions,
|
||||
frame: frame.actor.actorID,
|
||||
});
|
||||
thread: this.actorID,
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -69,6 +69,7 @@ const threadSpec = generateActorSpec({
|
||||
replayFramePositions: {
|
||||
positions: Option(0, "array:json"),
|
||||
frame: Option(0, "string"),
|
||||
thread: Option(0, "string"),
|
||||
},
|
||||
},
|
||||
|
||||
@ -119,6 +120,11 @@ const threadSpec = generateActorSpec({
|
||||
skip: Arg(0, "json"),
|
||||
},
|
||||
},
|
||||
fetchAncestorFramePositions: {
|
||||
request: {
|
||||
index: Arg(0, "number"),
|
||||
},
|
||||
},
|
||||
dumpThread: {
|
||||
request: {},
|
||||
response: RetVal("json"),
|
||||
|
Loading…
Reference in New Issue
Block a user