feat(tracker): cut orphan child nodes to prevent memory leaks

This commit is contained in:
sylenien 2022-07-25 17:26:09 +02:00 committed by Delirium
parent 2a43c04864
commit b65f45bf03
8 changed files with 55 additions and 23 deletions

View file

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

View file

@ -216,4 +216,4 @@ export default function(opts: Partial<Options> = {}) {
return Promise.reject(error);
});
}
}
}

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker",
"description": "The OpenReplay tracker main package",
"version": "3.5.15",
"version": "3.5.16",
"keywords": [
"logging",
"replay"

View file

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

View file

@ -2,14 +2,14 @@ type NodeCallback = (node: Node, isStart: boolean) => void;
type ElementListener = [string, EventListener];
export default class Nodes {
private readonly nodes: Array<Node | undefined>;
private readonly nodeCallbacks: Array<NodeCallback>;
private readonly elementListeners: Map<number, Array<ElementListener>>;
constructor(private readonly node_id: string) {
this.nodes = [];
this.nodeCallbacks = [];
this.elementListeners = new Map();
}
private nodes: Array<Node | void> = [];
private readonly nodeCallbacks: Array<NodeCallback> = [];
private readonly elementListeners: Map<number, Array<ElementListener>> = 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];
}

View file

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

View file

@ -149,6 +149,7 @@ export default function (app: App, opts: Partial<Options>): 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<Options>): void {
});
checkableValues.forEach((checked, id) => {
const node = app.nodes.getNode(id);
if (!node) return;
if (!isCheckable(node)) {
checkableValues.delete(id);
return;

View file

@ -102,6 +102,3 @@ export default class QueueSender {
}
}