diff --git a/tracker/tracker-assist/package.json b/tracker/tracker-assist/package.json index 3d363cffd..d04faef0d 100644 --- a/tracker/tracker-assist/package.json +++ b/tracker/tracker-assist/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker-assist", "description": "Tracker plugin for screen assistance through the WebRTC", - "version": "3.5.15", + "version": "3.5.16", "keywords": [ "WebRTC", "assistance", diff --git a/tracker/tracker-axios/src/index.ts b/tracker/tracker-axios/src/index.ts index e7abce84e..14d3387dc 100644 --- a/tracker/tracker-axios/src/index.ts +++ b/tracker/tracker-axios/src/index.ts @@ -216,4 +216,4 @@ export default function(opts: Partial = {}) { return Promise.reject(error); }); } -} \ No newline at end of file +} diff --git a/tracker/tracker/package.json b/tracker/tracker/package.json index 183006c05..7a6fdbe01 100644 --- a/tracker/tracker/package.json +++ b/tracker/tracker/package.json @@ -1,7 +1,7 @@ { "name": "@openreplay/tracker", "description": "The OpenReplay tracker main package", - "version": "3.5.15", + "version": "3.5.16", "keywords": [ "logging", "replay" diff --git a/tracker/tracker/src/main/app/index.ts b/tracker/tracker/src/main/app/index.ts index be03a968c..16d472c90 100644 --- a/tracker/tracker/src/main/app/index.ts +++ b/tracker/tracker/src/main/app/index.ts @@ -426,6 +426,7 @@ export default class App { this.ticker.start(); this.notify.log("OpenReplay tracking started."); + // get rid of onStart ? if (typeof this.options.onStart === 'function') { this.options.onStart(onStartInfo) @@ -458,7 +459,7 @@ export default class App { }) } } - stop(calledFromAPI = false): void { + stop(calledFromAPI = false, restarting = false): void { if (this.activityState !== ActivityState.NotActive) { try { this.sanitizer.clear() @@ -470,7 +471,7 @@ export default class App { this.session.reset() } this.notify.log("OpenReplay tracking stopped.") - if (this.worker) { + if (this.worker && !restarting) { this.worker.postMessage("stop") } } finally { @@ -478,4 +479,8 @@ export default class App { } } } + restart() { + this.stop(false, true); + this.start({ forceNew: false }); + } } diff --git a/tracker/tracker/src/main/app/nodes.ts b/tracker/tracker/src/main/app/nodes.ts index a1c87b6e2..536ba4efc 100644 --- a/tracker/tracker/src/main/app/nodes.ts +++ b/tracker/tracker/src/main/app/nodes.ts @@ -2,14 +2,14 @@ type NodeCallback = (node: Node, isStart: boolean) => void; type ElementListener = [string, EventListener]; export default class Nodes { - private readonly nodes: Array; - private readonly nodeCallbacks: Array; - private readonly elementListeners: Map>; - constructor(private readonly node_id: string) { - this.nodes = []; - this.nodeCallbacks = []; - this.elementListeners = new Map(); - } + private nodes: Array = []; + private readonly nodeCallbacks: Array = []; + private readonly elementListeners: Map> = new Map(); + + constructor( + private readonly node_id: string, + ) {} + attachNodeCallback(nodeCallback: NodeCallback): void { this.nodeCallbacks.push(nodeCallback); } @@ -46,7 +46,7 @@ export default class Nodes { const id = (node as any)[this.node_id]; if (id !== undefined) { delete (node as any)[this.node_id]; - this.nodes[id] = undefined; + delete this.nodes[id]; const listeners = this.elementListeners.get(id); if (listeners !== undefined) { this.elementListeners.delete(id); @@ -57,13 +57,25 @@ export default class Nodes { } return id; } + cleanTree() { + // sadly we keep empty items in array here resulting in some memory still being used + // but its still better than keeping dead nodes or undef elements + // plus we keep our index positions for new/alive nodes + // performance test: 3ms for 30k nodes with 17k dead ones + for (let i = 0; i < this.nodes.length; i++) { + const node = this.nodes[i]; + if (node && !document.contains(node)) { + this.unregisterNode(node); + } + } + } callNodeCallbacks(node: Node, isStart: boolean): void { this.nodeCallbacks.forEach((cb) => cb(node, isStart)); } getID(node: Node): number | undefined { return (node as any)[this.node_id]; } - getNode(id: number): Node | undefined { + getNode(id: number) { return this.nodes[id]; } diff --git a/tracker/tracker/src/main/app/observer/observer.ts b/tracker/tracker/src/main/app/observer/observer.ts index 940a7eefb..8c59d6029 100644 --- a/tracker/tracker/src/main/app/observer/observer.ts +++ b/tracker/tracker/src/main/app/observer/observer.ts @@ -48,10 +48,17 @@ function isObservable(node: Node): boolean { - use document as a 0-node in the upper context (should be updated in player at first) */ +/* + Nikita: + - rn we only send unbind event for parent (all child nodes will be cut in the live replay anyways) + to prevent sending 1k+ unbinds for child nodes and making replay file bigger than it should be +*/ + enum RecentsType { New, Removed, Changed, + RemovedChild, } export default abstract class Observer { @@ -73,7 +80,7 @@ export default abstract class Observer { } if (type === 'childList') { for (let i = 0; i < mutation.removedNodes.length; i++) { - this.bindTree(mutation.removedNodes[i]); + this.bindTree(mutation.removedNodes[i], true); } for (let i = 0; i < mutation.addedNodes.length; i++) { this.bindTree(mutation.addedNodes[i]); @@ -188,8 +195,12 @@ export default abstract class Observer { this.recents.set(id, RecentsType.Removed) } } + private unbindChildNode(node: Node): void { + const [ id ]= this.app.nodes.registerNode(node); + this.recents.set(id, RecentsType.RemovedChild) + } - private bindTree(node: Node): void { + private bindTree(node: Node, isChildUnbinding: boolean = false): void { if (!isObservable(node)) { return } @@ -199,7 +210,8 @@ export default abstract class Observer { NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT, { acceptNode: (node) => - isIgnored(node) || this.app.nodes.getID(node) !== undefined + isIgnored(node) + || (this.app.nodes.getID(node) !== undefined && !isChildUnbinding) ? NodeFilter.FILTER_REJECT : NodeFilter.FILTER_ACCEPT, }, @@ -207,11 +219,15 @@ export default abstract class Observer { false, ); while (walker.nextNode()) { - this.bindNode(walker.currentNode); + if (isChildUnbinding) { + this.unbindChildNode(walker.currentNode); + } else { + this.bindNode(walker.currentNode); + } } } - private unbindNode(node: Node): void { + private unbindNode(node: Node) { const id = this.app.nodes.unregisterNode(node); if (id !== undefined && this.recents.get(id) === RecentsType.Removed) { this.app.send(new RemoveNode(id)); diff --git a/tracker/tracker/src/main/modules/input.ts b/tracker/tracker/src/main/modules/input.ts index 496d642a2..f4d80ebf0 100644 --- a/tracker/tracker/src/main/modules/input.ts +++ b/tracker/tracker/src/main/modules/input.ts @@ -149,6 +149,7 @@ export default function (app: App, opts: Partial): void { app.ticker.attach((): void => { inputValues.forEach((value, id) => { const node = app.nodes.getNode(id); + if (!node) return; if (!isTextEditable(node)) { inputValues.delete(id); return; @@ -164,6 +165,7 @@ export default function (app: App, opts: Partial): void { }); checkableValues.forEach((checked, id) => { const node = app.nodes.getNode(id); + if (!node) return; if (!isCheckable(node)) { checkableValues.delete(id); return; diff --git a/tracker/tracker/src/webworker/QueueSender.ts b/tracker/tracker/src/webworker/QueueSender.ts index d4686539d..b5fc8c8ee 100644 --- a/tracker/tracker/src/webworker/QueueSender.ts +++ b/tracker/tracker/src/webworker/QueueSender.ts @@ -102,6 +102,3 @@ export default class QueueSender { } } - - -