change map/set to weakmap/set where possible, check canvas observers on time intervals and destroy peers; run node list maintainer every 30 sec (50ms ticks)

This commit is contained in:
nick-delirium 2024-10-17 15:23:52 +02:00
parent 5009491f63
commit b5e681ff00
No known key found for this signature in database
GPG key ID: 93ABD695DF5FDBA0
12 changed files with 142 additions and 46 deletions

Binary file not shown.

View file

@ -0,0 +1 @@
nodeLinker: node-modules

View file

@ -1,7 +1,7 @@
{
"name": "@openreplay/tracker-assist",
"description": "Tracker plugin for screen assistance through the WebRTC",
"version": "9.0.2-beta.3",
"version": "9.0.2-beta.11",
"keywords": [
"WebRTC",
"assistance",
@ -49,11 +49,12 @@
"prettier": "^2.7.1",
"replace-in-files-cli": "^1.0.0",
"ts-jest": "^29.0.3",
"typescript": "^4.6.0-dev.20211126"
"typescript": "^5.6.3"
},
"lint-staged": {
"*.{js,mjs,cjs,jsx,ts,tsx}": [
"eslint --fix --quiet"
]
}
},
"packageManager": "yarn@4.5.0"
}

View file

@ -86,6 +86,7 @@ export default class Assist {
private socket: Socket | null = null
private peer: Peer | null = null
private canvasPeers: Record<number, Peer | null> = {}
private canvasNodeCheckers: Map<number, any> = new Map()
private assistDemandedRestart = false
private callingState: CallingState = CallingState.False
private remoteControl: RemoteControl | null = null;
@ -674,6 +675,20 @@ export default class Assist {
app.debug.error,
)
this.canvasMap.set(id, canvasHandler)
if (this.canvasNodeCheckers.has(id)) {
clearInterval(this.canvasNodeCheckers.get(id))
}
const int = setInterval(() => {
const isPresent = node.ownerDocument.defaultView && node.isConnected
if (!isPresent) {
canvasHandler.stop()
this.canvasMap.delete(id)
this.canvasPeers[id]?.destroy()
this.canvasPeers[id] = null
clearInterval(int)
}
}, 5000)
this.canvasNodeCheckers.set(id, int)
}
})
}
@ -703,6 +718,10 @@ export default class Assist {
this.socket.disconnect()
this.app.debug.log('Socket disconnected')
}
this.canvasMap.clear()
this.canvasPeers = []
this.canvasNodeCheckers.forEach((int) => clearInterval(int))
this.canvasNodeCheckers.clear()
}
}

View file

@ -1 +1 @@
export const pkgVersion = "9.0.2-beta.3";
export const pkgVersion = "9.0.2-beta.11";

View file

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

View file

@ -32,7 +32,7 @@ import Message, {
UserID,
WSChannel,
} from './messages.gen.js'
import Nodes from './nodes.js'
import Nodes from './nodes/index.js'
import type { Options as ObserverOptions } from './observer/top_observer.js'
import Observer from './observer/top_observer.js'
import type { Options as SanitizerOptions } from './sanitizer.js'

View file

@ -1,20 +1,24 @@
import { createEventListener, deleteEventListener } from '../utils.js'
import { createEventListener, deleteEventListener } from '../../utils.js'
import Maintainer from './maintainer.js'
type NodeCallback = (node: Node, isStart: boolean) => void
type ElementListener = [string, EventListener, boolean]
export default class Nodes {
private nodes: Array<Node | void> = []
private readonly nodes: Map<number, Node | void> = new Map()
private totalNodeAmount = 0
private readonly nodeCallbacks: Array<NodeCallback> = []
private readonly elementListeners: Map<number, Array<ElementListener>> = new Map()
private nextNodeId = 0
private readonly node_id: string
private readonly forceNgOff: boolean
private readonly maintainer: Maintainer
constructor(params: { node_id: string; forceNgOff: boolean }) {
this.node_id = params.node_id
this.forceNgOff = params.forceNgOff
this.maintainer = new Maintainer(this.nodes, this.unregisterNode)
this.maintainer.start()
}
syntheticMode(frameOrder: number) {
@ -30,12 +34,12 @@ export default class Nodes {
}
// Attached once per Tracker instance
attachNodeCallback(nodeCallback: NodeCallback): void {
this.nodeCallbacks.push(nodeCallback)
attachNodeCallback(nodeCallback: NodeCallback): number {
return this.nodeCallbacks.push(nodeCallback)
}
scanTree = (cb: (node: Node | void) => void) => {
this.nodes.forEach((node) => cb(node))
this.nodes.forEach((node) => (node ? cb(node) : undefined))
}
attachNodeListener = (
@ -64,18 +68,18 @@ export default class Nodes {
id = this.nextNodeId
this.totalNodeAmount++
this.nextNodeId++
this.nodes[id] = node
this.nodes.set(id, node)
;(node as any)[this.node_id] = id
}
return [id, isNew]
}
unregisterNode(node: Node): number | undefined {
unregisterNode = (node: Node): number | undefined => {
const id = (node as any)[this.node_id]
if (id !== undefined) {
;(node as any)[this.node_id] = undefined
delete (node as any)[this.node_id]
delete this.nodes[id]
this.nodes.delete(id)
const listeners = this.elementListeners.get(id)
if (listeners !== undefined) {
this.elementListeners.delete(id)
@ -93,8 +97,7 @@ export default class Nodes {
// 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]
for (const [_, node] of this.nodes) {
if (node && !document.contains(node)) {
this.unregisterNode(node)
}
@ -111,7 +114,7 @@ export default class Nodes {
}
getNode(id: number) {
return this.nodes[id]
return this.nodes.get(id)
}
getNodeCount() {
@ -119,14 +122,13 @@ export default class Nodes {
}
clear(): void {
for (let id = 0; id < this.nodes.length; id++) {
const node = this.nodes[id]
if (!node) {
continue
for (const [_, node] of this.nodes) {
if (node) {
this.unregisterNode(node)
}
this.unregisterNode(node)
}
this.nextNodeId = 0
this.nodes.length = 0
this.nodes.clear()
}
}

View file

@ -0,0 +1,82 @@
const SECOND = 1000
function processMapInBatches(
map: Map<number, Node | void>,
batchSize: number,
processBatchCallback: (node: Node) => void,
) {
const iterator = map.entries()
function processNextBatch() {
const batch = []
let result = iterator.next()
while (!result.done && batch.length < batchSize) {
batch.push(result.value)
result = iterator.next()
}
if (batch.length > 0) {
batch.forEach(([_, node]) => {
if (node) {
processBatchCallback(node)
}
})
setTimeout(processNextBatch, 50)
}
}
processNextBatch()
}
function isNodeStillActive(node: Node): boolean {
try {
if (!node.isConnected) {
return false
}
const nodeWindow = node.ownerDocument?.defaultView
if (!nodeWindow) {
return false
}
if (nodeWindow.closed) {
return false
}
if (!node.ownerDocument.documentElement.isConnected) {
return false
}
return true
} catch (e) {
console.error('Error checking node activity:', e)
return false
}
}
class Maintainer {
private interval: ReturnType<typeof setInterval>
constructor(
private readonly nodes: Map<number, Node | void>,
private readonly unregisterNode: (node: Node) => void,
) {}
public start = () => {
this.interval = setInterval(() => {
processMapInBatches(this.nodes, SECOND * 2.5, (node) => {
if (!isNodeStillActive(node)) {
this.unregisterNode(node)
}
})
}, SECOND * 30)
}
public stop = () => {
clearInterval(this.interval)
}
}
export default Maintainer

View file

@ -8,7 +8,7 @@ type OffsetState = {
}
export default class IFrameOffsets {
private readonly states: Map<Document, OffsetState> = new Map()
private states: WeakMap<Document, OffsetState> = new WeakMap()
private calcOffset(state: OffsetState): Offset {
let parLeft = 0,
@ -55,12 +55,10 @@ export default class IFrameOffsets {
// anything more reliable? This does not cover all cases (layout changes are ignored, for ex.)
parentDoc.addEventListener('scroll', invalidateOffset)
parentDoc.defaultView?.addEventListener('resize', invalidateOffset)
this.states.set(doc, state)
}
clear() {
this.states.forEach((s) => s.clear())
this.states.clear()
this.states = new WeakMap()
}
}

View file

@ -103,9 +103,6 @@ export default abstract class Observer {
if (name === null) {
continue
}
if (target instanceof HTMLIFrameElement && name === 'src') {
this.handleIframeSrcChange(target)
}
let attr = this.attributesMap.get(id)
if (attr === undefined) {
this.attributesMap.set(id, (attr = new Set()))
@ -132,7 +129,9 @@ export default abstract class Observer {
}
/**
* Unbinds the removed nodes in case of iframe src change.
* EXPERIMENTAL: Unbinds the removed nodes in case of iframe src change.
*
* right now, we're relying on nodes.maintainer
*/
private handleIframeSrcChange(iframe: HTMLIFrameElement): void {
const oldContentDocument = iframe.contentDocument

View file

@ -55,7 +55,7 @@ export default class TopObserver extends Observer {
private readonly contextCallbacks: Array<ContextCallback> = []
// Attached once per Tracker instance
private readonly contextsSet: Set<Window> = new Set()
private readonly contextsSet: WeakSet<Window> = new WeakSet()
attachContextCallback(cb: ContextCallback) {
this.contextCallbacks.push(cb)
}
@ -64,7 +64,7 @@ export default class TopObserver extends Observer {
return this.iframeOffsets.getDocumentOffset(doc)
}
private iframeObservers: IFrameObserver[] = []
private iframeObservers: WeakMap<HTMLIFrameElement | Document, IFrameObserver> = new WeakMap()
private handleIframe(iframe: HTMLIFrameElement): void {
let doc: Document | null = null
// setTimeout is required. Otherwise some event listeners (scroll, mousemove) applied in modules
@ -72,16 +72,12 @@ export default class TopObserver extends Observer {
const handle = this.app.safe(() =>
setTimeout(() => {
const id = this.app.nodes.getID(iframe)
if (id === undefined) {
//log
return
}
if (!canAccessIframe(iframe)) return
if (id === undefined || !canAccessIframe(iframe)) return
const currentWin = iframe.contentWindow
const currentDoc = iframe.contentDocument
if (currentDoc && currentDoc !== doc) {
const observer = new IFrameObserver(this.app)
this.iframeObservers.push(observer)
this.iframeObservers.set(iframe, observer)
observer.observe(iframe) // TODO: call unregisterNode for the previous doc if present (incapsulate: one iframe - one observer)
doc = currentDoc
@ -105,10 +101,10 @@ export default class TopObserver extends Observer {
handle()
}
private shadowRootObservers: ShadowRootObserver[] = []
private shadowRootObservers: WeakMap<ShadowRoot, ShadowRootObserver> = new WeakMap()
private handleShadowRoot(shRoot: ShadowRoot) {
const observer = new ShadowRootObserver(this.app)
this.shadowRootObservers.push(observer)
this.shadowRootObservers.set(shRoot, observer)
observer.observe(shRoot.host)
}
@ -152,17 +148,15 @@ export default class TopObserver extends Observer {
this.app.nodes.clear()
this.app.nodes.syntheticMode(frameOder)
const iframeObserver = new IFrameObserver(this.app)
this.iframeObservers.push(iframeObserver)
this.iframeObservers.set(window.document, iframeObserver)
iframeObserver.syntheticObserve(rootNodeId, window.document)
}
disconnect() {
this.iframeOffsets.clear()
Element.prototype.attachShadow = attachShadowNativeFn
this.iframeObservers.forEach((o) => o.disconnect())
this.iframeObservers = []
this.shadowRootObservers.forEach((o) => o.disconnect())
this.shadowRootObservers = []
this.iframeObservers = new WeakMap()
this.shadowRootObservers = new WeakMap()
super.disconnect()
}
}