Merge pull request #563 from openreplay/tracker-pf-fix

Tracker 3.5.13: performance improvements for a case of extensive dom
* use of `.nodeType` property instead of `instanceof` when possible
* use of `.nodeName` property instead of `instanceof` for Elements 
* 2 previous also solve issue of non-function `instanceof` under different iframe contexts
* use map instead of array for storing recent nodes
* check node scrolls on start only
This commit is contained in:
Alex K 2022-06-30 17:05:25 +02:00 committed by GitHub
commit 99e71ba04b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 41 additions and 40 deletions

View file

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

View file

@ -163,7 +163,7 @@ export default class App {
}
// TODO: keep better tactics, discard others (look https://developer.mozilla.org/en-US/docs/Web/API/Navigator/sendBeacon)
this.attachEventListener(window, 'beforeunload', alertWorker, false);
this.attachEventListener(document, 'mouseleave', alertWorker, false, false);
this.attachEventListener(document.body, 'mouseleave', alertWorker, false, false);
this.attachEventListener(document, 'visibilitychange', alertWorker, false);
} catch (e) {
this._debug("worker_start", e);

View file

@ -1,4 +1,4 @@
type NodeCallback = (node: Node) => void;
type NodeCallback = (node: Node, isStart: boolean) => void;
type ElementListener = [string, EventListener];
export default class Nodes {
@ -57,8 +57,8 @@ export default class Nodes {
}
return id;
}
callNodeCallbacks(node: Node): void {
this.nodeCallbacks.forEach((cb) => cb(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];

View file

@ -45,16 +45,21 @@ function isObservable(node: Node): boolean {
/*
TODO:
- fix unbinding logic + send all removals first (ensure sequence is correct)
- use document as a 0-node in the upper context
- use document as a 0-node in the upper context (should be updated in player at first)
*/
enum RecentsType {
New,
Removed,
Changed,
}
export default abstract class Observer {
private readonly observer: MutationObserver;
private readonly commited: Array<boolean | undefined> = [];
private readonly recents: Array<boolean | undefined> = [];
private readonly myNodes: Array<boolean | undefined> = [];
private readonly recents: Map<number, RecentsType> = new Map()
private readonly indexes: Array<number> = [];
private readonly attributesList: Array<Set<string> | undefined> = [];
private readonly attributesMap: Map<number, Set<string>> = new Map();
private readonly textSet: Set<number> = new Set();
constructor(protected readonly app: App, protected readonly isTopContext = false) {
this.observer = new MutationObserver(
@ -79,17 +84,17 @@ export default abstract class Observer {
if (id === undefined) {
continue;
}
if (id >= this.recents.length) { // TODO: something more convinient
this.recents[id] = undefined;
if (!this.recents.has(id)) {
this.recents.set(id, RecentsType.Changed) // TODO only when altered
}
if (type === 'attributes') {
const name = mutation.attributeName;
if (name === null) {
continue;
}
let attr = this.attributesList[id];
let attr = this.attributesMap.get(id)
if (attr === undefined) {
this.attributesList[id] = attr = new Set();
this.attributesMap.set(id, attr = new Set())
}
attr.add(name);
continue;
@ -105,9 +110,9 @@ export default abstract class Observer {
}
private clear(): void {
this.commited.length = 0;
this.recents.length = 0;
this.recents.clear()
this.indexes.length = 1;
this.attributesList.length = 0;
this.attributesMap.clear();
this.textSet.clear();
}
@ -176,11 +181,12 @@ export default abstract class Observer {
}
private bindNode(node: Node): void {
const r = this.app.nodes.registerNode(node);
const id = r[0];
this.recents[id] = r[1] || this.recents[id] || false;
this.myNodes[id] = true;
const [ id, isNew ]= this.app.nodes.registerNode(node);
if (isNew){
this.recents.set(id, RecentsType.New)
} else if (!this.recents.has(id)) {
this.recents.set(id, RecentsType.Removed)
}
}
private bindTree(node: Node): void {
@ -207,7 +213,7 @@ export default abstract class Observer {
private unbindNode(node: Node): void {
const id = this.app.nodes.unregisterNode(node);
if (id !== undefined && this.recents[id] === false) {
if (id !== undefined && this.recents.get(id) === RecentsType.Removed) {
this.app.send(new RemoveNode(id));
}
}
@ -256,12 +262,13 @@ export default abstract class Observer {
if (sibling === null) {
this.indexes[id] = 0;
}
const isNew = this.recents[id];
const index = this.indexes[id];
const recentsType = this.recents.get(id)
const isNew = recentsType === RecentsType.New
const index = this.indexes[id]
if (index === undefined) {
throw 'commitNode: missing node index';
}
if (isNew === true) {
if (isNew) {
if (isElementNode(node)) {
let el: Element = node
if (parentID !== undefined) {
@ -294,10 +301,10 @@ export default abstract class Observer {
}
return true;
}
if (isNew === false && parentID !== undefined) {
if (recentsType === RecentsType.Removed && parentID !== undefined) {
this.app.send(new MoveNode(id, parentID, index));
}
const attr = this.attributesList[id];
const attr = this.attributesMap.get(id);
if (attr !== undefined) {
if (!isElementNode(node)) {
throw 'commitNode: node is not an element';
@ -326,19 +333,14 @@ export default abstract class Observer {
}
return (this.commited[id] = this._commitNode(id, node));
}
private commitNodes(): void {
private commitNodes(isStart: boolean = false): void {
let node;
for (let id = 0; id < this.recents.length; id++) {
// TODO: make things/logic nice here.
// commit required in any case if recents[id] true or false (in case of unbinding) or undefined (in case of attr change).
// Possible solution: separate new node commit (recents) and new attribute/move node commit
// Otherwise commitNode is called on each node, which might be a lot
if (!this.myNodes[id]) { continue }
this.recents.forEach((type, id) => {
this.commitNode(id);
if (this.recents[id] === true && (node = this.app.nodes.getNode(id))) {
this.app.nodes.callNodeCallbacks(node);
if (type === RecentsType.New && (node = this.app.nodes.getNode(id))) {
this.app.nodes.callNodeCallbacks(node, isStart)
}
}
})
this.clear();
}
@ -354,12 +356,11 @@ export default abstract class Observer {
});
this.bindTree(nodeToBind);
beforeCommit(this.app.nodes.getID(node))
this.commitNodes();
this.commitNodes(true)
}
disconnect(): void {
this.observer.disconnect();
this.clear();
this.myNodes.length = 0;
}
}

View file

@ -35,8 +35,8 @@ export default function (app: App): void {
nodeScroll.clear();
});
app.nodes.attachNodeCallback(node => {
if (isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
app.nodes.attachNodeCallback((node, isStart) => {
if (isStart && isElementNode(node) && node.scrollLeft + node.scrollTop > 0) {
nodeScroll.set(node, [node.scrollLeft, node.scrollTop]);
}
})