Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: show detail on leaking dom nodes #20

Merged
merged 2 commits into from
Dec 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/analyzeDomNodes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
function createDescriptionToCounts (nodes) {
const map = new Map()
for (const { description } of nodes) {
const count = map.get(description) || 0
map.set(description, count + 1)
}
return map
}

export function analyzeDomNodes (nodesBefore, nodesAfter, numIterations) {
const result = []

const descriptionToCountsBefore = createDescriptionToCounts(nodesBefore)
const descriptionToCountsAfter = createDescriptionToCounts(nodesAfter)

descriptionToCountsAfter.forEach((countAfter, description) => {
const countBefore = descriptionToCountsBefore.get(description) || 0
const delta = countAfter - countBefore
if (delta > 0) {
result.push({
description,
before: countBefore,
after: countAfter,
delta,
deltaPerIteration: delta / numIterations
})
}
})
return result
}
14 changes: 14 additions & 0 deletions src/analyzeEventListeners.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,17 @@ export function analyzeEventListeners (startListenersSummary, endListenersSummar

return sortBy(result, ['type'])
}

export function calculateEventListenersSummary (eventListenersStart, eventListenersEnd, numIterations) {
const before = sum(eventListenersStart.map(({ listeners }) => listeners.length))
const after = sum(eventListenersEnd.map(({ listeners }) => listeners.length))
const delta = after - before
const deltaPerIteration = delta / numIterations

return {
before,
after,
delta,
deltaPerIteration
}
}
10 changes: 0 additions & 10 deletions src/domNodes.js

This file was deleted.

79 changes: 36 additions & 43 deletions src/eventListeners.js
Original file line number Diff line number Diff line change
@@ -1,59 +1,52 @@
import { v4 as uuidV4 } from 'uuid'
import { omit, pick } from './util.js'
import { getDescriptors } from './getDescriptors.js'
import { getAllDomNodes } from './browser/getAllDomNodes.js'

// via https://stackoverflow.com/a/67030384
export async function getEventListeners (page) {
export async function getDomNodesAndListeners (page, cdpSession) {
const objectGroup = uuidV4()
const cdpSession = await page.target().createCDPSession()
try {
const { result: { objectId } } = await cdpSession.send('Runtime.evaluate', {
expression: `
const { result: { objectId } } = await cdpSession.send('Runtime.evaluate', {
expression: `
(function () {
${getAllDomNodes}
return [...getAllDomNodes(), window, document]
})()
`,
objectGroup
})
// Using the returned remote object ID, actually get the list of descriptors
const { result } = await cdpSession.send('Runtime.getProperties', { objectId })

const arrayProps = Object.fromEntries(result.map(_ => ([_.name, _.value])))

const length = arrayProps.length.value

const nodes = []

for (let i = 0; i < length; i++) {
nodes.push(arrayProps[i])
objectGroup
})
const nodeDescriptors = await getDescriptors(cdpSession, objectId)

const listenersWithNodes = []

// scrub the objects for external consumption, remove unnecessary stuff like objectId
const cleanNode = node => pick(node, ['className', 'description'])
const cleanListener = listener => ({
// originalHandler seems to contain the same information as handler
...omit(listener, ['backendNodeId', 'originalHandler']),
handler: omit(listener.handler, ['objectId'])
})

for (const node of nodeDescriptors) {
const { objectId } = node

const { listeners } = await cdpSession.send('DOMDebugger.getEventListeners', { objectId })

if (listeners.length) {
listenersWithNodes.push({
node: cleanNode(node),
listeners: listeners.map(cleanListener)
})
}
}

const nodesWithListeners = []

for (const node of nodes) {
const { objectId } = node

const { listeners } = await cdpSession.send('DOMDebugger.getEventListeners', { objectId })

if (listeners.length) {
nodesWithListeners.push({
node: pick(node, ['className', 'description']),
listeners: listeners.map(listener => {
return {
// originalHandler seems to contain the same information as handler
// as for objectId, these are useless since we will just release the object group anyway
...omit(listener, ['backendNodeId', 'originalHandler']),
handler: omit(listener.handler, ['objectId'])
}
})
})
}
}
await cdpSession.send('Runtime.releaseObjectGroup', { objectGroup })

// don't include the window/document objects in the list of dom nodes
const returnNodes = nodeDescriptors.slice(0, nodeDescriptors.length - 2).map(cleanNode)

await cdpSession.send('Runtime.releaseObjectGroup', { objectGroup })
return nodesWithListeners
} finally {
await cdpSession.detach()
return {
nodes: returnNodes,
listeners: listenersWithNodes
}
}
23 changes: 17 additions & 6 deletions src/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ${markdownTable(tableData)}
`.trim() + '\n\n'
}

function formatLeakingEventListeners (listenerSummaries) {
function formatLeakingEventListeners (listenerSummaries, eventListenersSummary) {
const tableData = [[
'Event',
'# added',
Expand All @@ -41,18 +41,29 @@ function formatLeakingEventListeners (listenerSummaries) {
])
}
return `
Leaking event listeners:
Leaking event listeners (+${eventListenersSummary.deltaPerIteration} total):

${markdownTable(tableData)}
`.trim() + '\n\n'
}

function formatLeakingDomNodes (domNodes) {
const tableData = [[
'Description',
'# added'
]]

for (const { description, deltaPerIteration } of domNodes.nodes) {
tableData.push([
description,
deltaPerIteration
])
}
return `
Leaking DOM nodes:
Leaking DOM nodes (+${domNodes.deltaPerIteration} total):

DOM size grew by ${domNodes.deltaPerIteration} node(s)
`.trim() + '\n\n'
${markdownTable(tableData)}
`.trim() + '\n\n'
}

function formatLeakingCollections (leakingCollections) {
Expand Down Expand Up @@ -91,7 +102,7 @@ export function formatResult ({ test, result }) {
leakTables += formatLeakingObjects(result.leaks.objects)
}
if (result.leaks.eventListeners.length) {
leakTables += formatLeakingEventListeners(result.leaks.eventListeners)
leakTables += formatLeakingEventListeners(result.leaks.eventListeners, result.leaks.eventListenersSummary)
}
if (result.leaks.domNodes.delta > 0) {
leakTables += formatLeakingDomNodes(result.leaks.domNodes)
Expand Down
14 changes: 14 additions & 0 deletions src/getDescriptors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Given an array of objects, get their descriptors
export async function getDescriptors (cdpSession, objectId) {
// via https://stackoverflow.com/a/67030384
const { result } = await cdpSession.send('Runtime.getProperties', { objectId })

const arrayProps = Object.fromEntries(result.map(_ => ([_.name, _.value])))
const length = arrayProps.length.value
const descriptors = []

for (let i = 0; i < length; i++) {
descriptors.push(arrayProps[i])
}
return descriptors
}
23 changes: 1 addition & 22 deletions src/heapsnapshots.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@ import cryptoRandomString from 'crypto-random-string'
import { createReadStream, createWriteStream } from 'fs'
import * as HeapSnapshotWorker from './thirdparty/devtools/heap_snapshot_worker/heap_snapshot_worker.js'

export async function takeHeapSnapshot (page) {
export async function takeHeapSnapshot (page, cdpSession) {
const filename = path.join(tempDir, `fuite-${cryptoRandomString({ length: 8, type: 'alphanumeric' })}.heapsnapshot`)
const cdpSession = await page.target().createCDPSession()
let writeStream
const writeStreamPromise = new Promise((resolve, reject) => {
writeStream = createWriteStream(filename, { encoding: 'utf8' })
Expand All @@ -30,7 +29,6 @@ export async function takeHeapSnapshot (page) {
})

await heapProfilerPromise
await cdpSession.detach()
writeStream.close()
await writeStreamPromise
return filename
Expand Down Expand Up @@ -64,22 +62,3 @@ export async function createHeapSnapshotModel (filename) {

return (await loader.buildSnapshot())
}

export async function takeThrowawayHeapSnapshot (page) {
const cdpSession = await page.target().createCDPSession()
const heapProfilerPromise = new Promise(resolve => {
cdpSession.on('HeapProfiler.reportHeapSnapshotProgress', ({ finished }) => {
if (finished) {
resolve()
}
})
})
await cdpSession.send('HeapProfiler.enable')
await cdpSession.send('HeapProfiler.collectGarbage')
await cdpSession.send('HeapProfiler.takeHeapSnapshot', {
reportProgress: true
})

await heapProfilerPromise
await cdpSession.detach()
}
Loading