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: added segment tree to transaction trace #2717

Merged
merged 2 commits into from
Nov 12, 2024
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
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
50 changes: 50 additions & 0 deletions lib/transaction/trace/segment-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright 2024 New Relic Corporation. All rights reserved.
* SPDX-License-Identifier: Apache-2.0
*/

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

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

class SegmentTree {
constructor(root, { logger = defaultLogger } = {}) {
this.logger = logger
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) {
this.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
Loading