feat(tracker): cut orphan child nodes to prevent memory leaks
This commit is contained in:
parent
2a43c04864
commit
b65f45bf03
8 changed files with 55 additions and 23 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -216,4 +216,4 @@ export default function(opts: Partial<Options> = {}) {
|
|||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "@openreplay/tracker",
|
||||
"description": "The OpenReplay tracker main package",
|
||||
"version": "3.5.15",
|
||||
"version": "3.5.16",
|
||||
"keywords": [
|
||||
"logging",
|
||||
"replay"
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -102,6 +102,3 @@ export default class QueueSender {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue