Compare commits

...
Sign in to create a new pull request.

6 commits

Author SHA1 Message Date
Alex Kaminskii
211852bafe mob-file reader-analyser (manual) 2023-04-21 18:48:46 +02:00
Alex Kaminskii
2a7f9d6cd2 whatewer tracker-plpugin ideas 2023-04-21 18:45:01 +02:00
Alex Kaminskii
ecf4e0e8a2 whatewer 2023-04-21 18:08:07 +02:00
Alex Kaminskii
607047f022 refactor(player): clean messageManager inside the class where it was created 2023-02-25 00:21:56 +01:00
Alex Kaminskii
5a966ca3de Merge branch 'dev' into player-ref-ph3 2023-02-25 00:04:13 +01:00
Alex Kaminskii
d6cac3bfda refactor(player): move contextmenu listener initiation to the corresponding context(InspectorController) 2023-02-14 17:53:12 +01:00
20 changed files with 722 additions and 225 deletions

View file

@ -24,19 +24,17 @@ function Overlay({
const { store } = React.useContext<ILivePlayerContext>(PlayerContext)
const {
messagesLoading,
cssLoading,
ready,
peerConnectionStatus,
livePlay,
calling,
remoteControl,
recordingState,
} = store.get()
const loading = messagesLoading || cssLoading
const liveStatusText = getStatusText(peerConnectionStatus)
const connectionStatus = peerConnectionStatus
const showLiveStatusText = livePlay && liveStatusText && !loading;
const showLiveStatusText = livePlay && liveStatusText && ready;
const showRequestWindow =
(calling === CallingState.Connecting ||
@ -66,7 +64,7 @@ function Overlay({
connectionStatus={closedLive ? ConnectionStatus.Closed : connectionStatus}
/>
)}
{loading ? <Loader /> : null}
{!ready ? <Loader /> : null}
</>
);
}

View file

@ -66,8 +66,7 @@ function Controls(props: any) {
completed,
skip,
speed,
cssLoading,
messagesLoading,
ready,
inspectorMode,
markedTargets,
exceptionsList,
@ -88,7 +87,7 @@ function Controls(props: any) {
} = props;
const storageType = selectStorageType(store.get());
const disabled = disabledRedux || cssLoading || messagesLoading || inspectorMode || markedTargets;
const disabled = !ready || disabledRedux || inspectorMode || markedTargets;
const profilesCount = profilesList.length;
const graphqlCount = graphqlList.length;
const showGraphql = graphqlCount > 0;
@ -326,7 +325,6 @@ export default connect(
// nextProps.showFetch !== props.showFetch ||
// nextProps.fetchCount !== props.fetchCount ||
// nextProps.graphqlCount !== props.graphqlCount ||
// nextProps.liveTimeTravel !== props.liveTimeTravel ||
// nextProps.skipInterval !== props.skipInterval
// )
// return true;

View file

@ -21,23 +21,21 @@ function Overlay({
const togglePlay = () => player.togglePlay()
const {
playing,
messagesLoading,
cssLoading,
ready,
completed,
autoplay,
inspectorMode,
markedTargets,
activeTargetIndex,
} = store.get()
const loading = messagesLoading || cssLoading
const showAutoplayTimer = completed && autoplay && nextId
const showPlayIconLayer = !isClickmap && !markedTargets && !inspectorMode && !loading && !showAutoplayTimer;
const showPlayIconLayer = !isClickmap && !markedTargets && !inspectorMode && ready && !showAutoplayTimer;
return (
<>
{showAutoplayTimer && <AutoplayTimer />}
{loading ? <Loader /> : null}
{!ready ? <Loader /> : null}
{showPlayIconLayer && <PlayIconLayer playing={playing} togglePlay={togglePlay} />}
{markedTargets && <ElementsMarker targets={markedTargets} activeIndex={activeTargetIndex} />}
</>

View file

@ -0,0 +1,23 @@
import { Store } from './types'
export default class StoreSubscriber<G, S=G> implements Store<G, S> {
constructor(private store: Store<G, S>) {}
get() { return this.store.get() }
update(newState: Partial<S>) {
this.store.update(newState)
this.subscriptions.forEach(sb => sb())
}
private subscriptions: Function[] = []
subscribe<T>(selector: (g: G) => T, cb: (val: T) => void) {
let prevVal = selector(this.get())
const checkSubscription = () => {
const newVal = selector(this.get())
if (newVal !== prevVal) {
prevVal = newVal
cb(newVal)
}
}
this.subscriptions.push(checkSubscription)
}
}

View file

@ -10,10 +10,6 @@ export interface Moveable {
move(time: number): void
}
export interface Cleanable {
clean(): void
}
export interface Interval {
contains(t: number): boolean
start: number

View file

@ -80,7 +80,7 @@ export default class Animator {
endTime,
live,
livePlay,
ready, // = messagesLoading || cssLoading || disconnected
ready,
lastMessageTime,
} = this.store.get()

View file

@ -1,6 +1,6 @@
import * as typedLocalStorage from './localStorage';
import type { Moveable, Cleanable, Store } from '../common/types';
import type { Moveable, Store } from '../common/types';
import Animator from './Animator';
import type { GetState as AnimatorGetState } from './Animator';
@ -25,40 +25,38 @@ export type State = typeof Player.INITIAL_STATE
export default class Player extends Animator {
static INITIAL_STATE = {
...Animator.INITIAL_STATE,
skipToIssue: initialSkipToIssue,
showEvents: initialShowEvents,
showEvents: initialShowEvents,
autoplay: initialAutoplay,
skip: initialSkip,
speed: initialSpeed,
} as const
constructor(private pState: Store<State & AnimatorGetState>, private manager: Moveable & Cleanable) {
constructor(private pState: Store<State & AnimatorGetState>, private manager: Moveable) {
super(pState, manager)
// Autoplay
if (pState.get().autoplay) {
let autoPlay = true;
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
const { playing } = pState.get();
autoPlay = playing
if (playing) {
this.pause();
}
} else if (autoPlay) {
this.play();
// Autostart
let autostart = true // TODO: configurable
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
const { playing } = pState.get();
autostart = playing
if (playing) {
this.pause();
}
})
if (!document.hidden) {
} else if (autostart) {
this.play();
}
})
if (!document.hidden) {
this.play();
}
}
/* === TODO: incapsulate in LSCache === */
//TODO: move to react part ("autoplay" responsible for auto-playing-next)
toggleAutoplay() {
const autoplay = !this.pState.get().autoplay
localStorage.setItem(AUTOPLAY_STORAGE_KEY, `${autoplay}`);
@ -72,13 +70,6 @@ export default class Player extends Animator {
this.pState.update({ showEvents })
}
// TODO: move to React part
toggleSkipToIssue() {
const skipToIssue = !this.pState.get().skipToIssue
localStorage.setItem(SKIP_TO_ISSUE_STORAGE_KEY, `${skipToIssue}`);
this.pState.update({ skipToIssue })
}
toggleSkip() {
const skip = !this.pState.get().skip
localStorage.setItem(SKIP_STORAGE_KEY, `${skip}`);
@ -108,7 +99,6 @@ export default class Player extends Animator {
clean() {
this.pause()
this.manager.clean()
}
}

View file

@ -0,0 +1,92 @@
import logger from 'App/logger';
import { decryptSessionBytes } from './network/crypto';
import MFileReader from './messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools } from './network/loadFiles';
import type {
Message,
} from './messages';
import type { Store } from '../common/types';
interface SessionFilesInfo {
startedAt: number
sessionId: string
domURL: string[]
devtoolsURL: string[]
mobsUrl: string[] // back-compatibility. TODO: Remove in the 1.11.0
fileKey: string | null
}
type State = typeof MessageLoader.INITIAL_STATE
export default class MessageLoader {
static INITIAL_STATE = {
firstFileLoading: false,
domLoading: false,
devtoolsLoading: false,
}
private lastMessageInFileTime: number = 0;
constructor(
private readonly session: SessionFilesInfo,
private store: Store<State>,
private distributeMessage: (msg: Message, index: number) => void,
) {}
private createNewParser(shouldDecrypt=true) {
const fKey = this.session.fileKey
const decrypt = shouldDecrypt && fKey
? (b: Uint8Array) => decryptSessionBytes(b, fKey)
: (b: Uint8Array) => Promise.resolve(b)
// Each time called - new fileReader created. TODO: reuseable decryptor instance
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt)
return (b: Uint8Array) => decrypt(b).then(b => {
fileReader.append(b)
const msgs: Array<Message> = []
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.distributeMessage(msg, msg._index)
msgs.push(msg)
}
logger.info("Messages loaded: ", msgs.length, msgs)
//this._sortMessagesHack(fileReader) // TODO
this.store.update({ firstFileLoading: false }) // How to do it more explicit: on the first file loading?
})
}
requestFallbackDOM = () =>
requestEFSDom(this.session.sessionId)
.then(this.createNewParser(false))
loadDOM() {
this.store.update({
domLoading: true,
firstFileLoading: true,
})
const loadMethod = this.session.domURL && this.session.domURL.length > 0
? { url: this.session.domURL, needsDecryption: true }
: { url: this.session.mobsUrl, needsDecryption: false }
return loadFiles(loadMethod.url, this.createNewParser(loadMethod.needsDecryption))
// EFS fallback
.catch((e) => this.requestFallbackDOM())
.finally(() => this.store.update({ domLoading: false }))
}
loadDevtools() {
this.store.update({ devtoolsLoading: true })
return loadFiles(this.session.devtoolsURL, this.createNewParser())
// EFS fallback
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(this.createNewParser(false))
)
.finally(() => this.store.update({ devtoolsLoading: false }))
}
}

View file

@ -6,8 +6,6 @@ import { TYPES as EVENT_TYPES } from 'Types/session/event';
import { Log } from './types/log';
import { Resource, ResourceType, getResourceFromResourceTiming, getResourceFromNetworkRequest } from './types/resource'
import { toast } from 'react-toastify';
import type { Store, Timed } from '../common/types';
import ListWalker from '../common/ListWalker';
@ -36,7 +34,6 @@ import { decryptSessionBytes } from './network/crypto';
import Lists, { INITIAL_STATE as LISTS_INITIAL_STATE, State as ListsState } from './Lists';
import Screen, {
INITIAL_STATE as SCREEN_INITIAL_STATE,
State as ScreenState,
} from './Screen/Screen';
@ -57,13 +54,9 @@ export interface State extends ScreenState, ListsState {
domContentLoadedTime?: { time: number, value: number },
domBuildingTime?: number,
loadTime?: { time: number, value: number },
error: boolean,
devtoolsLoading: boolean,
messagesLoading: boolean,
cssLoading: boolean,
ready: boolean,
lastMessageTime: number,
}
@ -80,16 +73,12 @@ const visualChanges = [
export default class MessageManager {
static INITIAL_STATE: State = {
...SCREEN_INITIAL_STATE,
...Screen.INITIAL_STATE,
...LISTS_INITIAL_STATE,
performanceChartData: [],
skipIntervals: [],
error: false,
devtoolsLoading: false,
messagesLoading: false,
cssLoading: false,
ready: false,
lastMessageTime: 0,
}
@ -137,117 +126,6 @@ export default class MessageManager {
this.activityManager = new ActivityManager(this.session.duration.milliseconds) // only if not-live
}
private setCSSLoading = (cssLoading: boolean) => {
this.screen.displayFrame(!cssLoading)
this.state.update({ cssLoading, ready: !this.state.get().messagesLoading && !cssLoading })
}
private _sortMessagesHack(msgs: Message[]) {
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
this.pagesManager.sortPages((m1, m2) => {
if (m1.time === m2.time) {
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m1.id)) {
return -1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
return -1;
} else if (m2FromHead && !m1FromHead) {
return 1;
}
}
}
return 0;
})
}
private waitingForFiles: boolean = false
private onFileReadSuccess = () => {
const stateToUpdate : Partial<State>= {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
...this.lists.getFullListsState(),
}
if (this.activityManager) {
this.activityManager.end()
stateToUpdate.skipIntervals = this.activityManager.list
}
this.state.update(stateToUpdate)
}
private onFileReadFailed = (e: any) => {
logger.error(e)
this.state.update({ error: true })
toast.error('Error requesting a session file')
}
private onFileReadFinally = () => {
this.waitingForFiles = false
// this.setMessagesLoading(false)
// this.state.update({ filesLoaded: true })
}
async loadMessages(isClickmap: boolean = false) {
this.setMessagesLoading(true)
// TODO: reusable decryptor instance
const createNewParser = (shouldDecrypt = true) => {
const decrypt = shouldDecrypt && this.session.fileKey
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey)
: (b: Uint8Array) => Promise.resolve(b)
// Each time called - new fileReader created
const fileReader = new MFileReader(new Uint8Array(), this.sessionStart)
return (b: Uint8Array) => decrypt(b).then(b => {
fileReader.append(b)
const msgs: Array<Message> = []
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.distributeMessage(msg, msg._index)
msgs.push(msg)
}
logger.info("Messages count: ", msgs.length, msgs)
this._sortMessagesHack(msgs)
this.setMessagesLoading(false)
})
}
this.waitingForFiles = true
const loadMethod = this.session.domURL && this.session.domURL.length > 0
? { url: this.session.domURL, parser: createNewParser }
: { url: this.session.mobsUrl, parser: () => createNewParser(false)}
loadFiles(loadMethod.url, loadMethod.parser())
// EFS fallback
.catch((e) =>
requestEFSDom(this.session.sessionId)
.then(createNewParser(false))
)
.then(this.onFileReadSuccess)
.catch(this.onFileReadFailed)
.finally(this.onFileReadFinally);
// load devtools (TODO: start after the first DOM file download)
if (isClickmap) return;
this.state.update({ devtoolsLoading: true })
loadFiles(this.session.devtoolsURL, createNewParser())
// EFS fallback
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(createNewParser(false))
)
.then(() => {
this.state.update(this.lists.getFullListsState()) // TODO: also in case of dynamic update through assist
})
.catch(e => logger.error("Can not download the devtools file", e))
.finally(() => this.state.update({ devtoolsLoading: false }))
}
resetMessageManagers() {
this.locationEventManager = new ListWalker();
this.locationManager = new ListWalker();
@ -327,10 +205,6 @@ export default class MessageManager {
this.screen.cursor.click();
}
})
if (this.waitingForFiles && this.lastMessageTime <= t && t !== this.session.duration.milliseconds) {
this.setMessagesLoading(true)
}
}
private decodeStateMessage(msg: any, keys: Array<string>) {
@ -347,7 +221,8 @@ export default class MessageManager {
return { ...msg, ...decoded };
}
distributeMessage(msg: Message, index: number): void {
distributeMessage = (msg: Message, index: number): void => {
this._handeleForSortHack(msg)
const lastMessageTime = Math.max(msg.time, this.lastMessageTime)
this.lastMessageTime = lastMessageTime
this.state.update({ lastMessageTime })
@ -471,9 +346,54 @@ export default class MessageManager {
}
}
setMessagesLoading(messagesLoading: boolean) {
this.screen.display(!messagesLoading);
this.state.update({ messagesLoading, ready: !messagesLoading && !this.state.get().cssLoading });
private _heeadChildrenID: number[] = []
private _handeleForSortHack(m: Message) {
// @ts-ignore
m.parentID === 1 && this._heeadChildrenID.push(m.id)
}
// Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
private _sortMessagesHack() {
const headChildrenIds = this._heeadChildrenID
this.pagesManager.sortPages((m1, m2) => {
if (m1.time === m2.time) {
if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m1.id)) {
return -1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
if (headChildrenIds.includes(m2.id)) {
return 1;
}
} else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
const m1FromHead = headChildrenIds.includes(m1.id);
const m2FromHead = headChildrenIds.includes(m2.id);
if (m1FromHead && !m2FromHead) {
return -1;
} else if (m2FromHead && !m1FromHead) {
return 1;
}
}
}
return 0;
})
}
onMessagesLoaded = () => {
const stateToUpdate : Partial<State>= {
performanceChartData: this.performanceTrackManager.chartData,
performanceAvaliability: this.performanceTrackManager.avaliability,
...this.lists.getFullListsState(),
}
if (this.activityManager) {
this.activityManager.end()
stateToUpdate.skipIntervals = this.activityManager.list
}
this.state.update(stateToUpdate)
this._sortMessagesHack()
}
private setCSSLoading = (cssLoading: boolean) => {
this.state.update({ cssLoading })
}
private setSize({ height, width }: { height: number, width: number }) {

View file

@ -6,19 +6,12 @@ import type { Point, Dimensions } from './types';
export type State = Dimensions
export const INITIAL_STATE: State = {
width: 0,
height: 0,
}
export enum ScaleMode {
Embed,
//AdjustParentWidth
AdjustParentHeight,
}
function getElementsFromInternalPoint(doc: Document, { x, y }: Point): Element[] {
// @ts-ignore (IE, Edge)
if (typeof doc.msElementsFromRect === 'function') {
@ -57,6 +50,11 @@ function isIframe(el: Element): el is HTMLIFrameElement {
}
export default class Screen {
static INITIAL_STATE: State = {
width: 0,
height: 0,
}
readonly overlay: HTMLDivElement
readonly cursor: Cursor
@ -91,20 +89,6 @@ export default class Screen {
parentElement.appendChild(this.screen);
this.parentElement = parentElement;
/* == For the Inspecting Document content == */
this.overlay.addEventListener('contextmenu', () => {
this.overlay.style.display = 'none'
const doc = this.document
if (!doc) { return }
const returnOverlay = () => {
this.overlay.style.display = 'block'
doc.removeEventListener('mousemove', returnOverlay)
doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection
}
doc.addEventListener('mousemove', returnOverlay)
doc.addEventListener('mouseclick', returnOverlay)
})
}
getParentElement(): HTMLElement | null {
@ -131,7 +115,7 @@ export default class Screen {
private getBoundingClientRect(): DOMRect {
if (this.boundingRect === null) {
// TODO: use this.screen instead in order to separate overlay functionality
return this.boundingRect = this.overlay.getBoundingClientRect() // expensive operation?
return this.boundingRect = this.screen.getBoundingClientRect() // expensive operation?
}
return this.boundingRect
}
@ -139,7 +123,7 @@ export default class Screen {
getInternalViewportCoordinates({ x, y }: Point): Point {
const { x: overlayX, y: overlayY, width } = this.getBoundingClientRect();
const screenWidth = this.overlay.offsetWidth;
const screenWidth = this.screen.offsetWidth;
const scale = screenWidth / width;
const screenX = (x - overlayX) * scale;
@ -240,7 +224,7 @@ export default class Screen {
width: width + 'px',
})
this.boundingRect = this.overlay.getBoundingClientRect();
this.boundingRect = this.screen.getBoundingClientRect();
}
}

View file

@ -0,0 +1 @@
// separate Player from File & WebLive player

View file

@ -15,6 +15,10 @@ export default class WebLivePlayer extends WebPlayer {
...WebPlayer.INITIAL_STATE,
...AssistManager.INITIAL_STATE,
liveTimeTravel: false,
assistLoading: false,
// get ready() { // TODO TODO TODO how to extend state here?
// return this.assistLoading && super.ready
// }
}
assistManager: AssistManager // public so far
@ -23,12 +27,12 @@ export default class WebLivePlayer extends WebPlayer {
private lastMessageInFileTime = 0
private lastMessageInFileIndex = 0
constructor(wpState: Store<typeof WebLivePlayer.INITIAL_STATE>, private session:any, config: RTCIceServer[]) {
super(wpState, session, true)
constructor(wpStore: Store<typeof WebLivePlayer.INITIAL_STATE>, private session:any, config: RTCIceServer[]) {
super(wpStore, session, true)
this.assistManager = new AssistManager(
session,
f => this.messageManager.setMessagesLoading(f),
assistLoading => wpStore.update({ assistLoading }),
(msg, idx) => {
this.incomingMessages.push(msg)
if (!this.historyFileIsLoading) {
@ -38,31 +42,27 @@ export default class WebLivePlayer extends WebPlayer {
},
this.screen,
config,
wpState,
wpStore,
)
this.assistManager.connect(session.agentToken)
}
toggleTimetravel = async () => {
if (this.wpState.get().liveTimeTravel) {
// TODO: implement via jump() API rewritten instead
if (this.wpStore.get().liveTimeTravel) {
return
}
let result = false;
this.historyFileIsLoading = true
this.messageManager.setMessagesLoading(true) // do it in one place. update unique loading states each time instead
this.messageManager.resetMessageManagers()
try {
const bytes = await requestEFSDom(this.session.sessionId)
const fileReader = new MFileReader(bytes, this.session.startedAt)
for (let msg = fileReader.readNext();msg !== null;msg = fileReader.readNext()) {
this.messageManager.distributeMessage(msg, msg._index)
}
this.wpState.update({
await this.messageLoader.requestFallbackDOM()
this.wpStore.update({
liveTimeTravel: true,
})
this.messageManager.onMessagesLoaded()
result = true
// here we need to update also lists state, if we gonna use them this.messageManager.onFileReadSuccess
} catch(e) {
toast.error('Error requesting a session file')
console.error("EFS file download error:", e)
@ -75,16 +75,14 @@ export default class WebLivePlayer extends WebPlayer {
this.incomingMessages.length = 0
this.historyFileIsLoading = false
this.messageManager.setMessagesLoading(false)
return result;
}
jumpToLive = () => {
this.wpState.update({
live: true,
this.wpStore.update({
livePlay: true,
})
this.jump(this.wpState.get().lastMessageTime)
this.jump(this.wpStore.get().lastMessageTime)
}
clean = () => {

View file

@ -1,31 +1,38 @@
import { Log, LogLevel } from './types/log'
import { toast } from 'react-toastify';
import logger from 'App/logger';
import type { Store } from 'App/player'
import type { Store } from '../common/types'
import StorSubscriber from '../common/StoreSubscriber'
import Player from '../player/Player'
import MessageManager from './MessageManager'
import InspectorController from './addons/InspectorController'
import TargetMarker from './addons/TargetMarker'
import Screen, { ScaleMode } from './Screen/Screen'
import MessageLoader from './MessageLoader'
// export type State = typeof WebPlayer.INITIAL_STATE
export default class WebPlayer extends Player {
static readonly INITIAL_STATE = {
...Player.INITIAL_STATE,
...TargetMarker.INITIAL_STATE,
...MessageManager.INITIAL_STATE,
...MessageLoader.INITIAL_STATE,
ready: true,
inspectorMode: false,
}
private readonly inspectorController: InspectorController
protected readonly screen: Screen
protected readonly messageLoader: MessageLoader
protected readonly messageManager: MessageManager
private targetMarker: TargetMarker
constructor(protected wpState: Store<typeof WebPlayer.INITIAL_STATE>, session: any, live: boolean, isClickMap = false) {
let initialLists = live ? {} : {
event: session.events || [],
@ -39,13 +46,32 @@ export default class WebPlayer extends Player {
) || [],
}
const store = new StorSubscriber(wpState)
const screen = new Screen(session.isMobile, isClickMap ? ScaleMode.AdjustParentHeight : ScaleMode.Embed)
const messageManager = new MessageManager(session, wpState, screen, initialLists)
super(wpState, messageManager)
const messageManager = new MessageManager(session, store, screen, initialLists)
//TODO: same for scaling
store.subscribe(state => state.cssLoading, cssLoading => this.screen.displayFrame(!cssLoading))
store.subscribe(state => state.domLoading, domLoading => this.screen.display(!domLoading))
store.subscribe(state => {
const notReady = state.cssLoading || (state.domLoading && state.time >= state.lastMessageTime)
return !notReady
}, ready => store.update({ ready }))
super(store, messageManager)
this.screen = screen
this.messageManager = messageManager
this.messageLoader = new MessageLoader(session, wpState, messageManager.distributeMessage)
if (!live) { // hack. TODO: split OfflinePlayer class
void messageManager.loadMessages(isClickMap)
this.messageLoader.loadDOM()
.then(() => messageManager.onMessagesLoaded())
.catch((e: any) => {
logger.error(e)
toast.error('Error requesting a session file') // TODO: outside of the player lib
})
this.messageLoader.loadDevtools()
.then(() => messageManager.onMessagesLoaded())
.catch(e => logger.error("Can not download the devtools file", e))
}
this.targetMarker = new TargetMarker(this.screen, wpState)
@ -127,6 +153,7 @@ export default class WebPlayer extends Player {
clean = () => {
super.clean()
this.messageManager.clean()
window.removeEventListener('resize', this.scale)
}
}

View file

@ -8,7 +8,20 @@ export default class InspectorController {
private substitutor: Screen | null = null
private inspector: Inspector | null = null
marker: Marker | null = null
constructor(private screen: Screen) {}
constructor(private screen: Screen) {
screen.overlay.addEventListener('contextmenu', () => {
screen.overlay.style.display = 'none'
const doc = screen.document
if (!doc) { return }
const returnOverlay = () => {
screen.overlay.style.display = 'block'
doc.removeEventListener('mousemove', returnOverlay)
doc.removeEventListener('mouseclick', returnOverlay) // TODO: prevent default in case of input selection
}
doc.addEventListener('mousemove', returnOverlay)
doc.addEventListener('mouseclick', returnOverlay)
})
}
scale(dims: Dimensions) {
if (this.substitutor) {

View file

@ -0,0 +1,3 @@
export interface MessageManager {
handleMessage(msg: Message): void
}

View file

@ -0,0 +1,163 @@
import { toast } from 'react-toastify';
import logger from 'App/logger';
import { decryptSessionBytes } from '../network/crypto';
import MFileReader from '../messages/MFileReader';
import { loadFiles, requestEFSDom, requestEFSDevtools } from './loadFiles';
import type {
Message,
} from '../messages';
export default class MessageLoader {
private lastMessageInFileTime: number = 0;
constructor(
private readonly session: any /*Session*/,
private setMessagesLoading: (flag: boolean) => void,
private distributeMessage: (msg: Message, index: number) => void,
) {}
requestEFSFile() {
this.setMessagesLoading(true)
this.waitingForFiles = true
const onData = (byteArray: Uint8Array) => {
const onMessage = (msg: Message) => { this.lastMessageInFileTime = msg.time }
this.parseAndDistributeMessages(new MFileReader(byteArray, this.session.startedAt), onMessage)
}
// assist will pause and skip messages to prevent timestamp related errors
// ----> this.reloadMessageManagers()
// ---> this.windowNodeCounter.reset()
return requestEFSDom(this.session.sessionId)
.then(onData)
// --->.then(this.onFileReadSuccess)
// --->.catch(this.onFileReadFailed)
.finally(this.onFileReadFinally)
}
private parseAndDistributeMessages(fileReader: MFileReader, onMessage?: (msg: Message) => void) {
const msgs: Array<Message> = []
let next: ReturnType<MFileReader['next']>
while (next = fileReader.next()) {
const [msg, index] = next
this.distributeMessage(msg, index)
msgs.push(msg)
onMessage?.(msg)
}
logger.info("Messages loaded: ", msgs.length, msgs)
// @ts-ignore Hack for upet (TODO: fix ordering in one mutation in tracker(removes first))
//--->// const headChildrenIds = msgs.filter(m => m.parentID === 1).map(m => m.id);
// this.pagesManager.sortPages((m1, m2) => {
// if (m1.time === m2.time) {
// if (m1.tp === MType.RemoveNode && m2.tp !== MType.RemoveNode) {
// if (headChildrenIds.includes(m1.id)) {
// return -1;
// }
// } else if (m2.tp === MType.RemoveNode && m1.tp !== MType.RemoveNode) {
// if (headChildrenIds.includes(m2.id)) {
// return 1;
// }
// } else if (m2.tp === MType.RemoveNode && m1.tp === MType.RemoveNode) {
// const m1FromHead = headChildrenIds.includes(m1.id);
// const m2FromHead = headChildrenIds.includes(m2.id);
// if (m1FromHead && !m2FromHead) {
// return -1;
// } else if (m2FromHead && !m1FromHead) {
// return 1;
// }
// }
// }
// return 0;
// })
}
private waitingForFiles: boolean = false
//--->// private onFileReadSuccess = () => {
// const stateToUpdate : Partial<State>= {
// performanceChartData: this.performanceTrackManager.chartData,
// performanceAvaliability: this.performanceTrackManager.avaliability,
// ...this.lists.getFullListsState(),
// }
// if (this.activityManager) {
// this.activityManager.end()
// stateToUpdate.skipIntervals = this.activityManager.list
// }
// this.state.update(stateToUpdate)
// }
//---> private onFileReadFailed = (e: any) => {
// this.state.update({ error: true })
// logger.error(e)
// toast.error('Error requesting a session file')
// }
private onFileReadFinally = () => {
//--->// this.incomingMessages
// .filter(msg => msg.time >= this.lastMessageInFileTime)
// .forEach(msg => this.distributeMessage(msg, 0))
this.waitingForFiles = false
this.setMessagesLoading(false)
}
private createNewParser(shouldDecrypt=true) {
const decrypt = shouldDecrypt && this.session.fileKey
? (b: Uint8Array) => decryptSessionBytes(b, this.session.fileKey)
: (b: Uint8Array) => Promise.resolve(b)
// Each time called - new fileReader created. TODO: reuseable decryptor instance
const fileReader = new MFileReader(new Uint8Array(), this.session.startedAt)
return (b: Uint8Array) => decrypt(b).then(b => {
fileReader.append(b)
this.parseAndDistributeMessages(fileReader)
this.setMessagesLoading(false)
})
}
loadDOM() {
this.setMessagesLoading(true)
this.waitingForFiles = true
let fileReadPromise = this.session.domURL && this.session.domURL.length > 0
? loadFiles(this.session.domURL, this.createNewParser())
: Promise.reject()
return fileReadPromise
// EFS fallback
.catch(() => requestEFSDom(this.session.sessionId).then(this.createNewParser(false)))
// old url fallback
.catch(e => {
logger.error('Can not get normal session replay file:', e)
// back compat fallback to an old mobsUrl
return loadFiles(this.session.mobsUrl, this.createNewParser(false))
})
// --->.then(this.onFileReadSuccess)
// --->.catch(this.onFileReadFailed)
.finally(this.onFileReadFinally)
}
loadDevtools() {
// load devtools
if (this.session.devtoolsURL.length) {
// ---> this.state.update({ devtoolsLoading: true })
return loadFiles(this.session.devtoolsURL, this.createNewParser())
.catch(() =>
requestEFSDevtools(this.session.sessionId)
.then(this.createNewParser(false))
)
//--->// .then(() => {
// this.state.update(this.lists.getFullListsState())
// })
// --->.catch(e => logger.error("Can not download the devtools file", e))
// --->//.finally(() => this.state.update({ devtoolsLoading: false }))
}
return Promise.resolve()
}
}

View file

@ -0,0 +1,60 @@
import APIClient from 'App/api_client';
const NO_NTH_FILE = "nnf"
const NO_UNPROCESSED_FILES = "nuf"
export async function* loadFiles(
urls: string[],
){
const firstFileURL = urls[0]
urls = urls.slice(1)
if (!firstFileURL) {
throw "No urls provided"
}
try {
yield await window.fetch(firstFileURL)
.then(r => processAPIStreamResponse(r, true))
for(const url in urls) {
yield await window.fetch(url)
.then(r => processAPIStreamResponse(r, false))
}
} catch(e) {
if (e === NO_NTH_FILE) {
return
}
throw e
}
}
export async function requestEFSDom(sessionId: string) {
return await requestEFSMobFile(sessionId + "/dom.mob")
}
export async function requestEFSDevtools(sessionId: string) {
return await requestEFSMobFile(sessionId + "/devtools.mob")
}
async function requestEFSMobFile(filename: string) {
const api = new APIClient()
const res = await api.fetch('/unprocessed/' + filename)
if (res.status >= 400) {
throw NO_UNPROCESSED_FILES
}
return await processAPIStreamResponse(res, false)
}
const processAPIStreamResponse = (response: Response, isFirstFile: boolean) => {
return new Promise<ArrayBuffer>((res, rej) => {
if (response.status === 404 && !isFirstFile) {
return rej(NO_NTH_FILE)
}
if (response.status >= 400) {
return rej(
isFirstFile ? `no start file. status code ${ response.status }`
: `Bad endfile status code ${response.status}`
)
}
res(response.arrayBuffer())
}).then(buffer => new Uint8Array(buffer))
}

View file

@ -16,6 +16,10 @@ export default class MFileReader extends RawMessageReader {
super(data)
}
getPosition() {
return this.p
}
private needSkipMessage(): boolean {
if (this.p === 0) return false
for (let i = 7; i >= 0; i--) {

173
read-file.ts Normal file
View file

@ -0,0 +1,173 @@
import MFileReader from './frontend/app/player/web/messages/MFileReader';
import {
MType,
} from './frontend/app/player/web/messages/raw.gen';
import fs from 'fs'
// silent logger
// const logger = {
// log(){},
// error(){},
// warn(){},
// group(){},
// }
/**
* For reading big files by chunks
*/
function readBytes(fd, sharedBuffer) {
return new Promise((resolve, reject) => {
fs.read(
fd,
sharedBuffer,
0,
sharedBuffer.length,
null,
(err) => {
if(err) { return reject(err); }
resolve(void)
}
)
})
}
async function* readByChunks(filePath, size) {
const sharedBuffer = Buffer.alloc(size);
const stats = fs.statSync(filePath); // file details
const fd = fs.openSync(filePath); // file descriptor
let bytesRead = 0; // how many bytes were read
let end = size;
for(let i = 0; i < Math.ceil(stats.size / size); i++) {
await readBytes(fd, sharedBuffer);
bytesRead = (i + 1) * size;
if(bytesRead > stats.size) {
// When we reach the end of file,
// we have to calculate how many bytes were actually read
end = size - (bytesRead - stats.size);
}
yield sharedBuffer.slice(0, end);
}
}
/* ==== end chunk-reader === */
/*== Message generators ==*/
async function* readBigFileMessages(file: string, chunkSize: number) {
let i = 0
const fileReader = new MFileReader(new Uint8Array(), 0)
for await(const chunk of readByChunks(file, chunkSize)) {
i++
fileReader.append(chunk)
let msg
while (msg = fileReader.readNext()) {
yield msg
}
console.log("Read chunk: ", i)
}
}
function* readMessagesTwoFiles(filename1: string, filename2: string) {
const file1 = fs.readFileSync(filename1)
const file2 = fs.readFileSync(filename2)
console.log("First file: ", file1.length, " bytes")
const fileReader = new MFileReader(file1, 0 )
fileReader.append(file2)
let msg
while (msg = fileReader.readNext()) {
yield [ msg, fileReader.getPosition() ]
}
}
/*== end message generators ==*/
function addToMap(map, key, n=1) {
map[key] = map[key] ? map[key] + n : n
}
function estimateSize(msg) {
return Object.values(msg).reduce((prevSum: number, val: any) => {
if (typeof val === "string") {
return prevSum + val.length + 1
}
if (typeof val ==="number") {
return prevSum + 2
}
return prevSum
}, 0)
}
const mapByTp = {}
const mapBySize = {}
const stringRepeatMapAttrs = {}
const stringRepeatMapAttrsNodes = {}
const stringRepeatMapOthers = {}
function updateStringsMap(map, msg){
Object.values(msg).forEach(val => {
if (typeof val === "string") {
addToMap(map, val)
}
})
}
const CHUNK_SIZE = 100000000; // 100MB
const FILE = "../decrypted0.mob"
let lastI = 0
let currentPageUrl = ""
async function main() {
for (const msg of readBigFileMessages(FILE, CHUNK_SIZE)) {
console.log(msg)
// const index = msg._index
// lastI = isNaN(index) ? lastI : index
// addToMap(mapByTp, msg.tp)
// addToMap(mapBySize, msg.tp, estimateSize(msg))
// if (msg.tp === 4) {
// currentPageUrl = msg.url
// }
// if (msg.tp === 12) {
// if (!stringRepeatMapAttrsNodes[msg.name]) {
// stringRepeatMapAttrsNodes[msg.name] = {}
// }
// if (!stringRepeatMapAttrsNodes[msg.value]) {
// stringRepeatMapAttrsNodes[msg.value] = {}
// }
// addToMap(stringRepeatMapAttrsNodes[msg.name], msg.id)
// addToMap(stringRepeatMapAttrsNodes[msg.value], msg.id)
// updateStringsMap(stringRepeatMapAttrs, msg)
// } else { updateStringsMap(stringRepeatMapOthers, msg)}
}
}
function calcStrMapStats(strMap){
const topStringMap = {}
const stringEntries = Object.entries(strMap)
stringEntries
.sort(([k1, v1], [k2, v2]) => v1*k1.length - v2*k2.length)
.slice(-10)
.forEach(([key, val]) => topStringMap[key]=val)
const keySize = Math.log10(stringEntries.length)
const strSum = stringEntries
.reduce((s, [k, v]) => s+k.length*v, 0)
const redSum = stringEntries
.reduce((s, [k, v]) => s+v*keySize, 0)
const dictSize = stringEntries
.reduce((s, [k, v]) => s+k.length +keySize, 0)
return [ strSum, redSum, dictSize, keySize, topStringMap ]
}
const statsAttr = calcStrMapStats(stringRepeatMapAttrs)
const statsOthers = calcStrMapStats(stringRepeatMapOthers)
// main()

View file

@ -0,0 +1,56 @@
import type Message from './app/messages.gen.js'
import type App from './app/index.js'
//import { PluginPayload } from './app/messages.gen.js'
// TODO: sendState(name, action, state) for state plugins;
export interface PluginDescription {
name: string
version: string
requiredTrackerVersion: string
onStart: () => void
onStop: () => void
onNode: (node: Node, id: number) => void
onContext: (context: typeof globalThis) => void
}
type PluginWrapper = (app: AppForPlugins) => Partial<PluginDescription>
interface AppForPlugins {
sendMessage(m: Message): void
send(name: string, payload: Object): void
// ...
}
export function applyPlugin<T>(app: App, pluginWrapper: PluginWrapper) {
function send(name: string, second?: Object) {
const paload = app.safe(() => JSON.stringify(second))() || ''
// app.send(PluginPayload(name, payload)) // send PluginPayload message
return
}
function sendMessage(msg: Message) {
app.send(msg)
}
const plugin = pluginWrapper({
send,
sendMessage,
//...
})
if (plugin.onStart) {
app.attachStartCallback(plugin.onStart)
}
if (plugin.onStop) {
app.attachStopCallback(plugin.onStop)
}
if (plugin.onNode) {
app.nodes.attachNodeCallback((node) => {
const id = app.nodes.getID(node)
if (id !== undefined) {
plugin.onNode(node, id)
}
})
}
return plugin
}