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:
jaril 2019-10-28 14:17:18 +00:00
parent 97ec12425c
commit ba28db66a0
29 changed files with 543 additions and 29 deletions

View File

@ -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

View File

@ -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";

View File

@ -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,

View File

@ -22,5 +22,6 @@ CompiledModules(
'previewPausedLocation.js',
'resumed.js',
'selectFrame.js',
'setFramePositions.js',
'skipPausing.js',
)

View File

@ -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({

View File

@ -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));

View File

@ -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,
});
};
}

View File

@ -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>,
|};

View File

@ -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 };

View File

@ -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)
),
};
}

View File

@ -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 };

View File

@ -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 = {|

View File

@ -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;
}

View File

@ -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);

View File

@ -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",

View File

@ -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",

View File

@ -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,

View File

@ -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",

View File

@ -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',

View File

@ -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");

View File

@ -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 });

View File

@ -53,6 +53,7 @@ export {
getSelectedFrame,
getSelectedFrames,
getVisibleSelectedFrame,
getFramePositions,
} from "./pause";
// eslint-disable-next-line import/named

View File

@ -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];
}

View File

@ -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 = {

View File

@ -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,
};
}

View File

@ -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;

View File

@ -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();
},

View File

@ -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,
});
},

View File

@ -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"),