Skip to content

Commit

Permalink
feat: added segment tree to transaction trace
Browse files Browse the repository at this point in the history
  • Loading branch information
bizob2828 committed Nov 7, 2024
1 parent 22bca8d commit cd3f053
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 89 deletions.
46 changes: 19 additions & 27 deletions lib/transaction/trace/exclusive-time-calculator.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,40 +7,32 @@

class ExclusiveCalculator {
constructor(root, trace) {
this.trace = trace
this.id = root.id
this.toProcess = [root]
this.node = trace.getNode(root.id)
// 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.
* using a depth first, postorder traversal over the tree recursively
*
* @param {Node} node to process duration and children
*/
process() {
while (this.toProcess.length) {
const segment = this.toProcess.pop()
const children = this.trace.getChildren(segment.id)
// when we hit a leaf, calc the exclusive time and report the time
// range to the parent
if (children.length === 0) {
segment._exclusiveDuration = segment.getDurationInMillis()
if (this.parentStack.length) {
this.finishLeaf(segment.timer.toRange())
}
} else {
// in the case we are processing an internal node, we just push it on the stack
// and push its children to be processed. all processing will be done after its
// children are all done (i.e. postorder)
this.parentStack.push({
childrenLeft: children.length,
segment: segment,
childPairs: []
})
for (let i = children.length - 1; i >= 0; --i) {
this.toProcess.push(children[i])
}
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])
}
}
}
Expand Down
105 changes: 61 additions & 44 deletions lib/transaction/trace/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ const logger = require('../../logger').child({ component: 'trace' })
const { DESTINATIONS } = require('../../config/attribute-filter')
const FROM_MILLIS = 1e-3
const ATTRIBUTE_SCOPE = 'transaction'

const REQUEST_URI_KEY = 'request.uri'
const UNKNOWN_URI_PLACEHOLDER = '/Unknown'
const SegmentTree = require('./segment-tree')

/**
* A Trace holds the root of the Segment graph and produces the final
Expand All @@ -30,18 +30,18 @@ function Trace(transaction) {

this.transaction = transaction

transaction.incrementCounters()

this.root = new TraceSegment({
const root = new TraceSegment({
config: transaction.agent.config,
name: 'ROOT',
collect: transaction.collect,
isRoot: true
})
this.root.start()
root.start()
transaction.incrementCounters()

this.intrinsics = Object.create(null)
this.segments = []
this.segments = new SegmentTree(root)
this.root = this.segments.root.segment
this.totalTimeCache = null

this.custom = new Attributes(ATTRIBUTE_SCOPE, MAXIMUM_CUSTOM_ATTRIBUTES)
Expand All @@ -61,32 +61,33 @@ function Trace(transaction) {
/**
* End and close the current trace. Triggers metric recording for trace
* segments that support recording.
* @param {Node} [node] the node to process the segment and its children
*/
Trace.prototype.end = function end() {
this.root.finalize(this)
const segments = this.segments
Trace.prototype.end = function end(node = this.segments.root) {
const { children, segment } = node
segment.finalize(this)

for (let i = 0; i < segments.length; i++) {
segments[i].finalize(this)
for (let i = 0; i < children.length; ++i) {
this.end(children[i])
}
}

/**
* Iterates over the trace tree and generates a span event for each segment.
* @param {Node} [node] the node to process the segment and its children
*/
Trace.prototype.generateSpanEvents = function generateSpanEvents() {
Trace.prototype.generateSpanEvents = function generateSpanEvents(node = this.segments.root) {
const config = this.transaction.agent.config

if (!shouldGenerateSpanEvents(config, this.transaction)) {
return
}

const { children, segment } = node

// Root segment does not become a span, so we need to process it separately.
const spanAggregator = this.transaction.agent.spanEventAggregator

const segments = this.segments

if (segments.length > 0) {
if (children.length && segment.name === 'ROOT') {
// At the point where these attributes are available, we only have a
// root span. Adding attributes to first non-root span here.
const attributeMap = {
Expand All @@ -100,13 +101,12 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents() {

for (const [key, value] of Object.entries(attributeMap)) {
if (value !== null) {
segments[0].addSpanAttribute(key, value)
children[0].segment.addSpanAttribute(key, value)
}
}
}

for (let i = 0; i < segments.length; ++i) {
const segment = segments[i]
if (segment.id !== this.root.id) {
const isRoot = segment.parentId === this.root.id
const parentId = isRoot ? this.transaction.parentSpanId : segment.parentId
// Even though at some point we might want to stop adding events because all the priorities
Expand All @@ -118,6 +118,10 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents() {
isRoot
})
}

for (let i = 0; i < children.length; ++i) {
this.generateSpanEvents(children[i])
}
}

function shouldGenerateSpanEvents(config, txn) {
Expand Down Expand Up @@ -209,14 +213,22 @@ Trace.prototype.getTotalTimeDurationInMillis = function getTotalTimeDurationInMi
if (this.totalTimeCache !== null) {
return this.totalTimeCache
}
const segments = this.segments
if (segments.length === 0) {

const rootNode = this.segments.root
const children = []
children.push(...rootNode.children)

if (!children.length) {
return 0
}

let totalTimeInMillis = 0
for (let i = 0; i < segments.length; i++) {
totalTimeInMillis += segments[i].getExclusiveDurationInMillis(this)

while (children.length !== 0) {
const node = children.pop()
const { segment, children: childChildren } = node
totalTimeInMillis += segment.getExclusiveDurationInMillis(this)
childChildren.forEach((child) => children.push(child))
}

if (!this.transaction.isActive()) {
Expand Down Expand Up @@ -339,37 +351,41 @@ Trace.prototype._getRequestUri = function _getRequestUri() {
return requestUri
}

/**
* Gets all children of a segment.
*
* @param {number} id of segment
* @returns {Array.<TraceSegment>} list of all segments that have the parentId of the segment
*/
Trace.prototype.getChildren = function getChildren(id) {
return this.segments.filter((segment) => segment.parentId === id)
Trace.prototype.getNode = function getNode(id) {
return this.segments.find(id)
}

/**
* Gets all children of a segment that should be collected and not ignored.
*
* @param {number} id of segment
* @returns {Array.<TraceSegment>} list of all segments that have the parentId of the segment
* @param {Array.<Node>} children filters children that are not ignored or `_collect` is false
* @returns {Array.<Node>} list of all segments and their children
*/
Trace.prototype.getCollectedChildren = function getCollectedChildren(id) {
return this.segments.filter(
(segment) => segment.parentId === id && segment._collect && !segment.ignore
)
Trace.prototype.getCollectedChildren = function getCollectedChildren(children) {
return children.filter((child) => child.segment._collect && !child.segment.ignore)
}

/**
* Gets the parent segment from list of segments on trace by passing in the `parentId`
* and matching on the `segment.id`
* and matching on the `segment.id`. Only used in testing
*
* @param {number} parentId id of parent segment you want to retrieve
* @returns {TraceSegment} parent segment
*/
Trace.prototype.getParent = function getParent(parentId) {
return this.segments.filter((segment) => segment.id === parentId)[0]
const node = this.segments.find(parentId)
return node?.segment
}

/**
* Gets all children of a segment. This is only used in testing
*
* @param {number} id of segment
* @returns {Array.<TraceSegment>} list of all segments that have the parentId of the segment
*/
Trace.prototype.getChildren = function getChildren(id) {
const node = this.segments.find(id)
return node?.children.map((child) => child.segment)
}

/**
Expand Down Expand Up @@ -404,17 +420,18 @@ Trace.prototype.toJSON = function toJSON() {
// serialized data.
const segmentsToProcess = [
{
segment: this.root,
node: this.segments.root,
destination: resultDest
}
]

while (segmentsToProcess.length !== 0) {
const { segment, destination } = segmentsToProcess.pop()
const { node, destination } = segmentsToProcess.pop()
const { segment, children } = node
const start = segment.timer.startedRelativeTo(this.root.timer)
const duration = segment.getDurationInMillis()

const segmentChildren = this.getCollectedChildren(segment.id)
const segmentChildren = this.getCollectedChildren(children)
const childArray = []

// push serialized data into the specified destination
Expand All @@ -426,7 +443,7 @@ Trace.prototype.toJSON = function toJSON() {
// onto the stack backwards (so the first one created is on top).
for (let i = segmentChildren.length - 1; i >= 0; --i) {
segmentsToProcess.push({
segment: segmentChildren[i],
node: segmentChildren[i],
destination: childArray
})
}
Expand Down Expand Up @@ -463,7 +480,7 @@ Trace.prototype._serializeTrace = function _serializeTrace() {
]

// clear out segments
this.segments = []
this.segments = null
return trace
}

Expand Down
49 changes: 49 additions & 0 deletions lib/transaction/trace/segment-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

'use strict'
const logger = require('../../logger').child({ component: 'segment-tree' })

class Node {
constructor(segment) {
this.segment = segment
this.children = []
}
}

class SegmentTree {
constructor(root) {
this.root = new Node(root)
}

find(parentId, node = this.root) {
if (parentId === node.segment.id) {
return node
}

for (const child of node.children) {
const result = this.find(parentId, child)
if (result) {
return result
}
}

return null
}

add(segment) {
const node = new Node(segment)
const parent = this.find(segment.parentId)

if (!parent) {
logger.debug('Cannot find parent %s in tree', segment.parentId)
return
}

parent.children.push(node)
}
}

module.exports = SegmentTree
6 changes: 3 additions & 3 deletions lib/transaction/tracer/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ function createSegment({ name, recorder, parent, transaction }) {
logger.trace('Adding segment %s to %s in %s', name, parent.name, transaction.id)

let collect = true
if (transaction.trace.segments.length >= this.agent.config.max_trace_segments) {

if (transaction.numSegments >= this.agent.config.max_trace_segments) {
collect = false
}

transaction.incrementCounters()

const segment = new TraceSegment({
Expand All @@ -119,7 +119,7 @@ function createSegment({ name, recorder, parent, transaction }) {
if (recorder) {
transaction.addRecorder(recorder.bind(null, segment))
}
transaction.trace.segments.push(segment)
transaction.trace.segments.add(segment)

return segment
}
Expand Down
Loading

0 comments on commit cd3f053

Please sign in to comment.