Skip to content

Commit 146969f

Browse files
authoredMar 21, 2024
PROF-9374: Move timeline events serialization from the hot path (#4173)
1 parent 76d76e0 commit 146969f

File tree

1 file changed

+79
-82
lines changed
  • packages/dd-trace/src/profiling/profilers

1 file changed

+79
-82
lines changed
 

‎packages/dd-trace/src/profiling/profilers/events.js

+79-82
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ const MS_TO_NS = 1000000
1515
const pprofValueType = 'timeline'
1616
const pprofValueUnit = 'nanoseconds'
1717

18+
const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
19+
1820
function labelFromStr (stringTable, key, valStr) {
1921
return new Label({ key, str: stringTable.dedup(valStr) })
2022
}
@@ -146,6 +148,76 @@ if (node16) {
146148
decoratorTypes.net = NetDecorator
147149
}
148150

151+
// Translates performance entries into pprof samples.
152+
class EventSerializer {
153+
constructor () {
154+
this.stringTable = new StringTable()
155+
this.samples = []
156+
this.locations = []
157+
this.functions = []
158+
this.decorators = {}
159+
160+
// A synthetic single-frame location to serve as the location for timeline
161+
// samples. We need these as the profiling backend (mimicking official pprof
162+
// tool's behavior) ignores these.
163+
const fn = new Function({ id: this.functions.length + 1, name: this.stringTable.dedup('') })
164+
this.functions.push(fn)
165+
const line = new Line({ functionId: fn.id })
166+
const location = new Location({ id: this.locations.length + 1, line: [line] })
167+
this.locations.push(location)
168+
this.locationId = [location.id]
169+
170+
this.timestampLabelKey = this.stringTable.dedup(END_TIMESTAMP_LABEL)
171+
}
172+
173+
addEvent (item) {
174+
const { entryType, startTime, duration } = item
175+
let decorator = this.decorators[entryType]
176+
if (!decorator) {
177+
const DecoratorCtor = decoratorTypes[entryType]
178+
if (DecoratorCtor) {
179+
decorator = new DecoratorCtor(this.stringTable)
180+
decorator.eventTypeLabel = labelFromStrStr(this.stringTable, 'event', entryType)
181+
this.decorators[entryType] = decorator
182+
} else {
183+
// Shouldn't happen but it's better to not rely on observer only getting
184+
// requested event types.
185+
return
186+
}
187+
}
188+
const endTime = startTime + duration
189+
const sampleInput = {
190+
value: [Math.round(duration * MS_TO_NS)],
191+
locationId: this.locationId,
192+
label: [
193+
decorator.eventTypeLabel,
194+
new Label({ key: this.timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
195+
]
196+
}
197+
decorator.decorateSample(sampleInput, item)
198+
this.samples.push(new Sample(sampleInput))
199+
}
200+
201+
createProfile (startDate, endDate) {
202+
const timeValueType = new ValueType({
203+
type: this.stringTable.dedup(pprofValueType),
204+
unit: this.stringTable.dedup(pprofValueUnit)
205+
})
206+
207+
return new Profile({
208+
sampleType: [timeValueType],
209+
timeNanos: endDate.getTime() * MS_TO_NS,
210+
periodType: timeValueType,
211+
period: 1,
212+
durationNanos: (endDate.getTime() - startDate.getTime()) * MS_TO_NS,
213+
sample: this.samples,
214+
location: this.locations,
215+
function: this.functions,
216+
stringTable: this.stringTable
217+
})
218+
}
219+
}
220+
149221
/**
150222
* This class generates pprof files with timeline events sourced from Node.js
151223
* performance measurement APIs.
@@ -155,15 +227,17 @@ class EventsProfiler {
155227
this.type = 'events'
156228
this._flushIntervalNanos = (options.flushInterval || 60000) * 1e6 // 60 sec
157229
this._observer = undefined
158-
this.entries = []
230+
this.eventSerializer = new EventSerializer()
159231
}
160232

161233
start () {
162234
// if already started, do nothing
163235
if (this._observer) return
164236

165237
function add (items) {
166-
this.entries.push(...items.getEntries())
238+
for (const item of items.getEntries()) {
239+
this.eventSerializer.addEvent(item)
240+
}
167241
}
168242
this._observer = new PerformanceObserver(add.bind(this))
169243
this._observer.observe({ entryTypes: Object.keys(decoratorTypes) })
@@ -177,89 +251,12 @@ class EventsProfiler {
177251
}
178252

179253
profile (restart, startDate, endDate) {
180-
if (this.entries.length === 0) {
181-
// No events in the period; don't produce a profile
182-
return null
183-
}
184-
185-
const stringTable = new StringTable()
186-
const locations = []
187-
const functions = []
188-
189-
// A synthetic single-frame location to serve as the location for timeline
190-
// samples. We need these as the profiling backend (mimicking official pprof
191-
// tool's behavior) ignores these.
192-
const locationId = (() => {
193-
const fn = new Function({ id: functions.length + 1, name: stringTable.dedup('') })
194-
functions.push(fn)
195-
const line = new Line({ functionId: fn.id })
196-
const location = new Location({ id: locations.length + 1, line: [line] })
197-
locations.push(location)
198-
return [location.id]
199-
})()
200-
201-
const decorators = {}
202-
for (const [eventType, DecoratorCtor] of Object.entries(decoratorTypes)) {
203-
const decorator = new DecoratorCtor(stringTable)
204-
decorator.eventTypeLabel = labelFromStrStr(stringTable, 'event', eventType)
205-
decorators[eventType] = decorator
206-
}
207-
const timestampLabelKey = stringTable.dedup(END_TIMESTAMP_LABEL)
208-
209-
const dateOffset = BigInt(Math.round(performance.timeOrigin * MS_TO_NS))
210-
const lateEntries = []
211-
const perfEndDate = endDate.getTime() - performance.timeOrigin
212-
const samples = this.entries.map((item) => {
213-
const decorator = decorators[item.entryType]
214-
if (!decorator) {
215-
// Shouldn't happen but it's better to not rely on observer only getting
216-
// requested event types.
217-
return null
218-
}
219-
const { startTime, duration } = item
220-
if (startTime >= perfEndDate) {
221-
// An event past the current recording end date; save it for the next
222-
// profile. Not supposed to happen as long as there's no async activity
223-
// between capture of the endDate value in profiler.js _collect() and
224-
// here, but better be safe than sorry.
225-
lateEntries.push(item)
226-
return null
227-
}
228-
const endTime = startTime + duration
229-
const sampleInput = {
230-
value: [Math.round(duration * MS_TO_NS)],
231-
locationId,
232-
label: [
233-
decorator.eventTypeLabel,
234-
new Label({ key: timestampLabelKey, num: dateOffset + BigInt(Math.round(endTime * MS_TO_NS)) })
235-
]
236-
}
237-
decorator.decorateSample(sampleInput, item)
238-
return new Sample(sampleInput)
239-
}).filter(v => v)
240-
241-
this.entries = lateEntries
242-
243-
const timeValueType = new ValueType({
244-
type: stringTable.dedup(pprofValueType),
245-
unit: stringTable.dedup(pprofValueUnit)
246-
})
247-
248254
if (!restart) {
249255
this.stop()
250256
}
251-
252-
return new Profile({
253-
sampleType: [timeValueType],
254-
timeNanos: endDate.getTime() * MS_TO_NS,
255-
periodType: timeValueType,
256-
period: 1,
257-
durationNanos: (endDate.getTime() - startDate.getTime()) * MS_TO_NS,
258-
sample: samples,
259-
location: locations,
260-
function: functions,
261-
stringTable
262-
})
257+
const profile = this.eventSerializer.createProfile(startDate, endDate)
258+
this.eventSerializer = new EventSerializer()
259+
return profile
263260
}
264261

265262
encode (profile) {

0 commit comments

Comments
 (0)
Please sign in to comment.