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:
parent
5009491f63
commit
b5e681ff00
12 changed files with 142 additions and 46 deletions
BIN
tracker/tracker-assist/.yarn/install-state.gz
Normal file
BIN
tracker/tracker-assist/.yarn/install-state.gz
Normal file
Binary file not shown.
1
tracker/tracker-assist/.yarnrc.yml
Normal file
1
tracker/tracker-assist/.yarnrc.yml
Normal file
|
|
@ -0,0 +1 @@
|
|||
nodeLinker: node-modules
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export const pkgVersion = "9.0.2-beta.3";
|
||||
export const pkgVersion = "9.0.2-beta.11";
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
82
tracker/tracker/src/main/app/nodes/maintainer.ts
Normal file
82
tracker/tracker/src/main/app/nodes/maintainer.ts
Normal 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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue