@clinic/heap-profiler
Version:
Programmable interface to Clinic.js Heap Profiler
235 lines (186 loc) • 6.55 kB
JavaScript
const d3 = require('./d3.js')
const HtmlContent = require('./html-content.js')
class StackBar extends HtmlContent {
constructor (parentContent, contentProperties = {}) {
super(parentContent, contentProperties)
this.highlightedNode = null
this.highlightedNodeTimeoutHandler = null
this.frameTooltipHandler = null
this.tooltip = contentProperties.tooltip
this.tooltipHtmlContent = contentProperties.tooltipHtmlContent
this.tooltipHtmlContent
.getTooltipD3()
.on('mouseenter', () => {
clearTimeout(this.highlightedNodeTimeoutHandler)
// this.ui.highlightNode(this.ui.selectedNode)
})
.on('mouseleave', () => {
this.ui.highlightNode(this.ui.selectedNode)
})
this.ui.on('highlightNode', node => {
this.pointToNode(node || this.ui.selectedNode)
})
this.ui.on('selectNode', node => {
this.pointToNode(node)
})
this.ui.on('zoomNode', () => {
this.draw()
})
}
initializeElements () {
super.initializeElements()
const ui = this.ui
this.d3StacksWrapper = this.d3Element
.append('div')
.classed('stacks-wrapper', true)
.on('mousemove', () => {
clearTimeout(this.highlightedNodeTimeoutHandler)
const nodeElem = this.getNodeAtX(d3.event.offsetX)
if (!nodeElem) return
const nodeData = nodeElem.d
ui.highlightNode(nodeElem.d)
const wrapperRect = this.d3StacksWrapper.node().getBoundingClientRect()
if (!nodeData) {
this.tooltip.hide()
return
}
this.tooltipHtmlContent.setNodeData(nodeData)
this.tooltip.show({
msg: this.tooltipHtmlContent.getTooltipD3().node(),
pointerCoords: { x: d3.event.offsetX, y: d3.event.offsetY },
targetRect: wrapperRect,
wrapperNode: this.d3StacksWrapper.node()
})
})
.on('mouseout', () => {
clearTimeout(this.highlightedNodeTimeoutHandler)
this.highlightedNodeTimeoutHandler = setTimeout(() => {
ui.highlightNode(this.ui.selectedNode)
this.tooltip.hide(200)
}, 200)
})
.on('click', () => {
const nodeElem = this.getNodeAtX(d3.event.offsetX)
if (!nodeElem) return
const nodeData = nodeElem.d
if (nodeData) {
this.ui.highlightNode(nodeData)
this.ui.selectNode(nodeData)
}
})
.on('dblclick', () => {
const nodeElem = this.getNodeAtX(d3.event.offsetX)
if (!nodeElem) return
const nodeData = nodeElem.d
if (nodeData) {
this.ui.zoomNode(nodeData)
}
})
this.d3Pointer = this.d3Element.append('div').classed('pointer', true)
}
pointToNode (node) {
this.highlightedNode = node
this.draw()
}
getNodeAtX (x) {
const totalWidth = this.d3StacksWrapper.node().getBoundingClientRect().width
let left = 0
return this.frames.find(frame => {
left += totalWidth * frame.width + frame.margin
return left > x
})
}
getNodePosition (node) {
let found = false
let isInRemaining = false
let left = 0
let margin = 0
let i = 0
const frames = this.frames
const totalWidth = this.d3StacksWrapper.node().getBoundingClientRect().width
if (!frames || !node) return '0px'
while (!found && i < frames.length - 1) {
const frame = frames[i]
found = node.id === frame.d.id
margin += found ? 0 : frame.margin
left += found ? frame.width / 2 : frame.width
i++
}
if (!found) {
const lastFrame = frames[frames.length - 1]
// This may not be an aggregate `remaining` frame if all frames fit on the stack bar,
// which can happen on small profiles or with aggressive filters.
isInRemaining =
Array.isArray(lastFrame.remaining) && lastFrame.remaining.some(smallFrame => smallFrame.id === node.id)
}
return found || isInRemaining ? `${left * totalWidth + margin}px` : '-20px'
}
prepareFrames () {
if (process.env.DEBUG_MODE) {
console.time('StackBar.prepareFrames')
}
const { dataTree } = this.ui
// flattening the children array and sorting the frames
dataTree.sortAllocations(this.ui.zoomedNode)
const availableWidth = this.d3Element.node().getBoundingClientRect().width
const onePxPercent = 1 / availableWidth
const frames = []
let usedWidth = 0.0
for (let i = 0; i < dataTree.sortedAllocations.length; i++) {
const d = dataTree.sortedAllocations[i]
const value = d.selfValue
const totalFraction = Math.max(onePxPercent, value / dataTree.total)
const width = totalFraction
const margin = totalFraction > 0.02 ? 2 : 1
frames.push({ d, width, margin })
usedWidth += width + margin / availableWidth
if (usedWidth >= 0.98) {
const remaining = dataTree.sortedAllocations.slice(i + 1)
frames.push({ remaining, width: 1 - usedWidth, margin: 0 })
break
}
}
if (process.env.DEBUG_MODE) {
console.timeEnd('StackBar.prepareFrames')
}
return frames
}
draw () {
super.draw()
const { dataTree } = this.ui
if (dataTree.sortedAllocations === null) {
return
}
if (process.env.DEBUG_MODE) {
console.time('StackBar.draw')
}
this.frames = this.prepareFrames()
const update = this.d3StacksWrapper.selectAll('div').data(this.frames)
update.exit().remove()
const self = this
update
.enter()
.append('div')
.classed('stack-frame', true)
.merge(update)
.each(function (data) {
const { width, margin } = data
const isHighlighted = data.d && self.highlightedNode && self.highlightedNode.id === data.d.id
const isSelected = data.d && self.ui.selectedNode && self.ui.selectedNode.id === data.d.id
d3.select(this)
.classed('highlighted', isHighlighted)
.classed('selected', isSelected)
.style('background-color', self.ui.dataTree.getHeatColor(data.d))
.style('width', `${(width * 100).toFixed(3)}%`)
.style('margin-right', `${margin}px`)
})
// moving the selector over the bar
const left = this.getNodePosition(self.highlightedNode)
this.d3Pointer.style('transform', `translateX(${left})`)
this.d3Pointer.classed('hidden', left === null)
if (process.env.DEBUG_MODE) {
console.timeEnd('StackBar.draw')
}
}
}
module.exports = StackBar