add skipInactive option

Skip inactive time is an important and useful feature. We consider
user interaction events as active, and check next user interaction
event after apply incremental snapshot.
If next user interaction event has a time gap larger than the
threshold, we will set a dynamic speed value which will skip the
inactive time interval in about 5 seconds.
This commit is contained in:
Yanzhen Yu
2018-12-26 18:05:55 +08:00
parent 063f226918
commit aaa8bb9e94
3 changed files with 61 additions and 6 deletions

View File

@@ -21,6 +21,9 @@ import { mirror } from '../utils';
import injectStyleRules from './styles/inject-style'; import injectStyleRules from './styles/inject-style';
import './styles/style.css'; import './styles/style.css';
const SKIP_TIME_THRESHOLD = 10 * 1000;
const SKIP_TIME_INTERVAL = 5 * 1000;
smoothscroll.polyfill(); smoothscroll.polyfill();
// https://github.com/rollup/rollup/issues/1267#issuecomment-296395734 // https://github.com/rollup/rollup/issues/1267#issuecomment-296395734
@@ -31,6 +34,7 @@ const defaultConfig: playerConfig = {
speed: 1, speed: 1,
root: document.body, root: document.body,
loadTimeout: 0, loadTimeout: 0,
skipInactive: false,
}; };
export class Replayer { export class Replayer {
@@ -48,6 +52,9 @@ export class Replayer {
// record last played event timestamp when paused // record last played event timestamp when paused
private lastPlayedEvent: eventWithTime; private lastPlayedEvent: eventWithTime;
private nextUserInteractionEvent: eventWithTime | null;
private noramlSpeed: number;
private timer: Timer; private timer: Timer;
private missingNodeRetryMap: missingNodeMap = {}; private missingNodeRetryMap: missingNodeMap = {};
@@ -167,8 +174,10 @@ export class Replayer {
const firstOffset = event.data.positions[0].timeOffset; const firstOffset = event.data.positions[0].timeOffset;
// timeOffset is a negative offset to event.timestamp // timeOffset is a negative offset to event.timestamp
const firstTimestamp = event.timestamp + firstOffset; const firstTimestamp = event.timestamp + firstOffset;
event.delay = firstTimestamp - this.baselineTime;
return firstTimestamp - this.baselineTime; return firstTimestamp - this.baselineTime;
} }
event.delay = event.timestamp - this.baselineTime;
return event.timestamp - this.baselineTime; return event.timestamp - this.baselineTime;
} }
@@ -194,6 +203,36 @@ export class Replayer {
case EventType.IncrementalSnapshot: case EventType.IncrementalSnapshot:
castFn = () => { castFn = () => {
this.applyIncremental(event, isSync); this.applyIncremental(event, isSync);
if (event === this.nextUserInteractionEvent) {
this.nextUserInteractionEvent = null;
this.setConfig({ speed: this.noramlSpeed });
this.emitter.emit('skip-end');
}
if (this.config.skipInactive && !this.nextUserInteractionEvent) {
for (const _event of this.events) {
if (_event.delay! <= event.delay!) {
continue;
}
if (this.isUserInteraction(_event)) {
if (
_event.delay! - event.delay! >
SKIP_TIME_THRESHOLD * this.config.speed
) {
this.nextUserInteractionEvent = _event;
}
break;
}
}
if (this.nextUserInteractionEvent) {
this.noramlSpeed = this.config.speed;
const skipTime =
this.nextUserInteractionEvent.delay! - event.delay!;
this.setConfig({
speed: Math.round(skipTime / SKIP_TIME_INTERVAL),
});
this.emitter.emit('skip-start');
}
}
}; };
break; break;
default: default:
@@ -457,8 +496,12 @@ export class Replayer {
const target: HTMLInputElement = (mirror.getNode( const target: HTMLInputElement = (mirror.getNode(
d.id, d.id,
) as Node) as HTMLInputElement; ) as Node) as HTMLInputElement;
target.checked = d.isChecked; try {
target.value = d.text; target.checked = d.isChecked;
target.value = d.text;
} catch (error) {
// for safe
}
break; break;
} }
default: default:
@@ -506,4 +549,14 @@ export class Replayer {
currentEl = currentEl.parentElement; currentEl = currentEl.parentElement;
} }
} }
private isUserInteraction(event: eventWithTime): boolean {
if (event.type !== EventType.IncrementalSnapshot) {
return false;
}
return (
event.data.source > IncrementalSource.Mutation &&
event.data.source <= IncrementalSource.Input
);
}
} }

View File

@@ -28,15 +28,15 @@ export default class Timer {
public start() { public start() {
this.actions.sort((a1, a2) => a1.delay - a2.delay); this.actions.sort((a1, a2) => a1.delay - a2.delay);
let delayed = 0; let delayed = 0;
const start = performance.now(); let lastTimestamp = performance.now();
const { actions, config } = this; const { actions, config } = this;
const self = this; const self = this;
function check(time: number) { function check(time: number) {
delayed = time - start; delayed += (time - lastTimestamp) * config.speed;
lastTimestamp = time;
while (actions.length) { while (actions.length) {
const action = actions[0]; const action = actions[0];
const delayNeeded = action.delay / config.speed; if (delayed >= action.delay) {
if (delayed >= delayNeeded) {
actions.shift(); actions.shift();
action.doAction(); action.doAction();
} else { } else {

View File

@@ -95,6 +95,7 @@ export type event =
export type eventWithTime = event & { export type eventWithTime = event & {
timestamp: number; timestamp: number;
delay?: number;
}; };
export type recordOptions = { export type recordOptions = {
@@ -227,6 +228,7 @@ export type playerConfig = {
speed: number; speed: number;
root: Element; root: Element;
loadTimeout: number; loadTimeout: number;
skipInactive: Boolean;
}; };
export type playerMetaData = { export type playerMetaData = {