Skip to content

Commit

Permalink
feat(replay): Use mobx for replay re-rendering [PoC]
Browse files Browse the repository at this point in the history
Do not merge, this was a quick 1-2 hr refactor to show what mobx + replayer would look like
  • Loading branch information
billyvg committed Jul 3, 2024
1 parent 8555853 commit d66ea90
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 71 deletions.
10 changes: 7 additions & 3 deletions static/app/components/replays/breadcrumbs/replayTimeline.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {useRef} from 'react';
import styled from '@emotion/styled';
import {observer} from 'mobx-react';

import Panel from 'sentry/components/panels/panel';
import Placeholder from 'sentry/components/placeholder';
Expand All @@ -16,8 +17,9 @@ import {divide} from 'sentry/components/replays/utils';
import toPercent from 'sentry/utils/number/toPercent';
import {useDimensions} from 'sentry/utils/useDimensions';

export default function ReplayTimeline() {
const {replay, currentTime, timelineScale} = useReplayContext();
const ReplayTimeline = observer(() => {
const {replay, timer, timelineScale} = useReplayContext();
const {currentTime} = timer!;

const panelRef = useRef<HTMLDivElement>(null);
const mouseTrackingProps = useTimelineScrubberMouseTracking(
Expand Down Expand Up @@ -76,7 +78,9 @@ export default function ReplayTimeline() {
</Stacked>
</VisiblePanel>
);
}
});

export default ReplayTimeline;

const VisiblePanel = styled(Panel)`
margin: 0;
Expand Down
67 changes: 43 additions & 24 deletions static/app/components/replays/player/scrubber.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Fragment} from 'react';
import styled from '@emotion/styled';
import {observer} from 'mobx-react';

import RangeSlider from 'sentry/components/forms/controls/rangeSlider';
import SliderAndInputWrapper from 'sentry/components/forms/controls/rangeSlider/sliderAndInputWrapper';
Expand All @@ -16,13 +17,12 @@ type Props = {
showZoomIndicators?: boolean;
};

function Scrubber({className, showZoomIndicators = false}: Props) {
const {replay, currentHoverTime, currentTime, setCurrentTime, timelineScale} =
useReplayContext();
const Scrubber = observer(({className, showZoomIndicators = false}: Props) => {
const {replay, timer, timelineScale} = useReplayContext();
const {currentTime} = timer;

const durationMs = replay?.getDurationMs() ?? 0;
const percentComplete = divide(currentTime, durationMs);
const hoverPlace = divide(currentHoverTime || 0, durationMs);

const initialTranslate = 0.5 / timelineScale;

Expand Down Expand Up @@ -56,37 +56,56 @@ function Scrubber({className, showZoomIndicators = false}: Props) {
</Fragment>
) : null}
<Meter>
{currentHoverTime ? (
<div>
<TimelineTooltip labelText={formatTime(currentHoverTime)} />
<MouseTrackingValue
style={{
width: toPercent(hoverPlace),
}}
/>
</div>
) : null}
<HoverTracker durationMs={durationMs} />
<PlaybackTimeValue
style={{
width: toPercent(percentComplete),
}}
/>
</Meter>
<RangeWrapper>
<Range
name="replay-timeline"
min={0}
max={durationMs}
value={Math.round(currentTime)}
onChange={value => setCurrentTime(value || 0)}
showLabel={false}
aria-label={t('Seek slider')}
/>
<SeekSlider durationMs={durationMs} />
</RangeWrapper>
</Wrapper>
);
}
});

const HoverTracker = observer(({durationMs}) => {
const {timer} = useReplayContext();
const {currentHoverTime} = timer;
if (currentHoverTime === undefined) {
return null;
}

const hoverPlace = divide(currentHoverTime || 0, durationMs);
return (
<div>
<TimelineTooltip labelText={formatTime(currentHoverTime)} />
<MouseTrackingValue
style={{
width: toPercent(hoverPlace),
}}
/>
</div>
);
});

const SeekSlider = observer(({durationMs}) => {
const {timer, setCurrentTime} = useReplayContext();
const {currentTime} = timer;

return (
<Range
name="replay-timeline"
min={0}
max={durationMs}
value={Math.round(currentTime)}
onChange={value => setCurrentTime(value || 0)}
showLabel={false}
aria-label={t('Seek slider')}
/>
);
});
const Meter = styled(Progress.Meter)`
background: ${p => p.theme.gray200};
`;
Expand Down
148 changes: 106 additions & 42 deletions static/app/components/replays/replayContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
// useLayoutEffect,
useRef,
useState,
} from 'react';
Expand Down Expand Up @@ -186,12 +186,14 @@ interface ReplayPlayerContextProps extends HighlightCallbacks {
*/
timelineScale: number;

timer: Timer;
/**
* Start or stop playback
*
* @param play
*/
togglePlayPause: (play: boolean) => void;

/**
* Allow RRWeb to use Fast-Forward mode for idle moments in the video
*
Expand All @@ -200,6 +202,47 @@ interface ReplayPlayerContextProps extends HighlightCallbacks {
toggleSkipInactive: (skip: boolean) => void;
}

class Timer {
currentPlayerTime = 0;
currentHoverTime: number | undefined = undefined;
bufferTarget = -1;
bufferPrevious = -1;
startTimeOffsetMs = -1;

constructor() {
makeAutoObservable(this);
}

setPlayerTime = action((time: number) => {
this.currentPlayerTime = time;
});

setCurrentTimer() {}
setHoverTime = action((time: number | undefined) => {
this.currentHoverTime = time;
});

setStartTimeOffsetMs = action((time: number) => {
this.startTimeOffsetMs = time;
});

get isBuffering() {
return (
this.bufferTarget !== -1 &&
this.bufferPrevious === this.currentPlayerTime &&
this.bufferTarget !== this.bufferPrevious
);
}

get currentTime() {
return (
(this.isBuffering ? this.bufferTarget : this.currentPlayerTime) -
this.startTimeOffsetMs
);
}
}
const timer = new Timer();

const ReplayPlayerContext = createContext<ReplayPlayerContextProps>({
analyticsContext: '',
clearAllHighlights: () => {},
Expand All @@ -224,6 +267,7 @@ const ReplayPlayerContext = createContext<ReplayPlayerContextProps>({
setSpeed: () => {},
setTimelineScale: () => {},
speed: 1,
timer,
timelineScale: 1,
togglePlayPause: () => {},
toggleSkipInactive: () => {},
Expand Down Expand Up @@ -266,11 +310,20 @@ type Props = {
value?: Partial<ReplayPlayerContextProps>;
};

function useCurrentTime(callback: () => number) {
const [currentTime, setCurrentTime] = useState(0);
useRAF(() => setCurrentTime(callback));
return currentTime;
}
// function useCurrentTime(callback: () => number) {
// const [currentTime, setCurrentTime] = useState(0);
// useRAF(() => setCurrentTime(callback));
// return currentTime;
// }

import {action, makeAutoObservable} from 'mobx';

// autorun(() => {
// if (!timer.isBuffering && timer.bufferTarget !== -1) {
// timer.bufferTarget = -1;
// timer.bufferPrevious = -1;
// }
// });

export function Provider({
analyticsContext,
Expand All @@ -296,15 +349,15 @@ export function Provider({
const hasNewEvents = events !== oldEvents;
const replayerRef = useRef<Replayer>(null);
const [dimensions, setDimensions] = useState<Dimensions>({height: 0, width: 0});
const [currentHoverTime, setCurrentHoverTime] = useState<undefined | number>();
// const [currentHoverTime, setCurrentHoverTime] = useState<undefined | number>();
const [isPlaying, setIsPlaying] = useState(false);
const [finishedAtMS, setFinishedAtMS] = useState<number>(-1);
const [isSkippingInactive, setIsSkippingInactive] = useState(
savedReplayConfigRef.current.isSkippingInactive
);
const [speed, setSpeedState] = useState(savedReplayConfigRef.current.playbackSpeed);
const [fastForwardSpeed, setFFSpeed] = useState(0);
const [buffer, setBufferTime] = useState({target: -1, previous: -1});
// const [buffer, setBufferTime] = useState({target: -1, previous: -1});
const [isVideoBuffering, setVideoBuffering] = useState(false);
const playTimer = useRef<number | undefined>(undefined);
const didApplyInitialOffset = useRef(false);
Expand All @@ -314,6 +367,7 @@ export function Provider({
const durationMs = replay?.getDurationMs() ?? 0;
const clipWindow = replay?.getClipWindow() ?? undefined;
const startTimeOffsetMs = replay?.getStartOffsetMs() ?? 0;
timer.setStartTimeOffsetMs(startTimeOffsetMs);
const videoEvents = replay?.getVideoEvents();
const startTimestampMs = replay?.getStartTimestampMs();
const isVideoReplay = Boolean(
Expand Down Expand Up @@ -341,6 +395,18 @@ export function Provider({
[]
);

const tick = useCallback(() => {
return window.requestAnimationFrame(() => {
timer.setPlayerTime(getCurrentPlayerTime());
tick();
});
}, [getCurrentPlayerTime]);

useEffect(() => {
const id = tick();
return () => window.cancelAnimationFrame(id);
}, [tick]);

const isFinished = getCurrentPlayerTime() === finishedAtMS;
const setReplayFinished = useCallback(() => {
setFinishedAtMS(getCurrentPlayerTime());
Expand All @@ -367,7 +433,9 @@ export function Provider({

// Sometimes rrweb doesn't get to the exact target time, as long as it has
// changed away from the previous time then we can hide then buffering message.
setBufferTime({target: time, previous: getCurrentPlayerTime()});
// setBufferTime({target: time, previous: getCurrentPlayerTime()});
timer.bufferTarget = time;
timer.bufferPrevious = getCurrentPlayerTime();

// Clear previous timers. Without this (but with the setTimeout) multiple
// requests to set the currentTime could finish out of order and cause jumping.
Expand Down Expand Up @@ -700,48 +768,43 @@ export function Provider({
[prefsStrategy]
);

const currentPlayerTime = useCurrentTime(getCurrentPlayerTime);

const [isBuffering, currentBufferedPlayerTime] =
buffer.target !== -1 &&
buffer.previous === currentPlayerTime &&
buffer.target !== buffer.previous
? [true, buffer.target]
: [false, currentPlayerTime];

const currentTime = currentBufferedPlayerTime - startTimeOffsetMs;

useEffect(() => {
if (!isBuffering && events && events.length >= 2 && replayerRef.current) {
applyInitialOffset();
}
}, [isBuffering, events, applyInitialOffset]);

useLayoutEffect(() => {
replayPlayerTimestampEmitter.emit('replay timestamp change', {
currentTime,
currentHoverTime,
});
}, [currentTime, currentHoverTime]);

useEffect(() => {
if (!isBuffering && buffer.target !== -1) {
setBufferTime({target: -1, previous: -1});
}
}, [isBuffering, buffer.target]);
useRAF(() => timer.setPlayerTime(getCurrentPlayerTime()));

// const [isBuffering, currentBufferedPlayerTime] =
// buffer.target !== -1 &&
// buffer.previous === currentPlayerTime &&
// buffer.target !== buffer.previous
// ? [true, buffer.target]
// : [false, timer.currentPlayerTime];
//
// const currentTime = currentBufferedPlayerTime - startTimeOffsetMs;

// TODO
// useEffect(() => {
// if (!isBuffering && events && events.length >= 2 && replayerRef.current) {
// applyInitialOffset();
// }
// }, [isBuffering, events, applyInitialOffset]);

// useLayoutEffect(() => {
// replayPlayerTimestampEmitter.emit('replay timestamp change', {
// currentTime,
// currentHoverTime,
// });
// }, [currentTime, currentHoverTime]);

return (
<ReplayPlayerContext.Provider
value={{
analyticsContext,
clearAllHighlights,
currentHoverTime,
currentTime,
currentHoverTime: undefined,
currentTime: 0,
dimensions,
fastForwardSpeed,
addHighlight,
setRoot,
isBuffering: isBuffering && !isVideoReplay,
isBuffering: timer.isBuffering && !isVideoReplay,
isVideoBuffering,
isFetching,
isVideoReplay,
Expand All @@ -751,11 +814,12 @@ export function Provider({
removeHighlight,
replay,
restart,
setCurrentHoverTime,
setCurrentHoverTime: time => timer.setHoverTime(time),
setCurrentTime,
setSpeed,
setTimelineScale,
speed,
timer,
timelineScale,
togglePlayPause,
toggleSkipInactive,
Expand Down
Loading

0 comments on commit d66ea90

Please sign in to comment.