newrelic
Version:
New Relic agent
144 lines (128 loc) • 5.22 kB
JavaScript
/*
* Copyright 2020 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/
class ExclusiveCalculator {
constructor(root, trace) {
const node = trace.getNode(root.id)
// could not find segment in trace, typically because context propagation is broken
// add some defensive and essentially set the exclusive time to the segment duration
this.node = node ?? { children: [], segment: root }
// use a second stack to do a post-order traversal
this.parentStack = []
}
/**
* Kicks off the exclusive duration calculation. This is performed
* using a depth first, postorder traversal over the tree recursively
*
* @param {Node} node to process duration and children
*/
process(node = this.node) {
const { children, segment } = node
if (children.length === 0) {
segment._exclusiveDuration = segment.getDurationInMillis()
if (this.parentStack.length) {
this.finishLeaf(segment.timer.toRange())
}
} else {
this.parentStack.push({
childrenLeft: children.length,
segment,
childPairs: []
})
for (let i = children.length - 1; i >= 0; --i) {
this.process(children[i])
}
}
}
/**
* Updates the immediate parent in the parent stack that a leaf node has
* been processed. If the parent isn't expecting any more children to
* be processed, it pops the stack and propagates the processing to
* more distant predecessors.
*
* @param {Array} childRange An array of start and end time for the finished leaf node
*/
finishLeaf(childRange) {
let parent = this.parentStack[this.parentStack.length - 1]
// push the current segment's range pair up to the parent's child pairs
parent.childPairs = merge(parent.childPairs, [childRange])
// decrement the number of children expected for the current parent; process the
// parent if it is not expecting any further children to finish (i.e. the number
// of children left to process is 0).
while (--parent.childrenLeft === 0) {
// pull off the finished parent and assign the exclusive duration
const { segment: finishedParent, childPairs } = this.parentStack.pop()
const timer = finishedParent.timer
const finishedEnd = timer.getDurationInMillis() + timer.start
let duration = finishedParent.getDurationInMillis()
for (let i = 0; i < childPairs.length; ++i) {
const pair = childPairs[i]
// since these are non-overlapping and ordered by start time, the first one
// to start after the parent's end marks the end of the segments we care
// about.
if (pair[0] >= finishedEnd) {
break
}
duration -= Math.min(pair[1], finishedEnd) - pair[0]
}
finishedParent._exclusiveDuration = duration
parent = this.parentStack[this.parentStack.length - 1]
// since the parent was potentially a child of another segment, we need to
// rerun this for the parent's parent till we hit a parent with children yet
// to be processed.
if (parent) {
// merge the current child segments in with the finished parent's range
const inserted = merge(childPairs, [finishedParent.timer.toRange()])
// merge the finished parent's merged range into its parent's range
parent.childPairs = merge(parent.childPairs, inserted)
} else {
// in the case where the parent doesn't exist, we are done and can break out.
break
}
}
}
}
function merge(first, second) {
if (!first.length) {
return second
}
if (!second.length) {
return first
}
const res = []
let resIdx = 0
let firstIdx = 0
let secondIdx = 0
// N.B. this is destructive, it will be updating the end times for range arrays in
// the input arrays. If we need to reuse these arrays for anything, this behavior
// must be changed.
let currInterval =
first[firstIdx][0] < second[secondIdx][0] ? first[firstIdx++] : second[secondIdx++]
while (firstIdx < first.length && secondIdx < second.length) {
const next = first[firstIdx][0] < second[secondIdx][0] ? first[firstIdx++] : second[secondIdx++]
if (next[0] <= currInterval[1]) {
// if the segment overlaps, update the end of the current merged segment
currInterval[1] = Math.max(next[1], currInterval[1])
} else {
// if there is no overlap, start a new merging segment and push the old one
res[resIdx++] = currInterval
currInterval = next
}
}
const firstIsRemainder = firstIdx !== first.length
const remainder = firstIsRemainder ? first : second
let remainderIdx = firstIsRemainder ? firstIdx : secondIdx
// merge the segments overlapping with the current interval
while (remainder[remainderIdx] && remainder[remainderIdx][0] <= currInterval[1]) {
currInterval[1] = Math.max(remainder[remainderIdx++][1], currInterval[1])
}
res[resIdx++] = currInterval
// append the remaining non-overlapping ranges
for (; remainderIdx < remainder.length; ++remainderIdx) {
res[resIdx++] = remainder[remainderIdx]
}
return res
}
module.exports = ExclusiveCalculator