Skip to content

Commit a659d71

Browse files
committed
Set an upper bound for the number of timeline events gathered within a single flush period
1 parent 4e1033e commit a659d71

File tree

1 file changed

+46
-6
lines changed
  • packages/dd-trace/src/profiling/profilers

1 file changed

+46
-6
lines changed

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

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const { performance, constants, PerformanceObserver } = require('perf_hooks')
44
const { END_TIMESTAMP_LABEL, SPAN_ID_LABEL, LOCAL_ROOT_SPAN_ID_LABEL, encodeProfileAsync } = require('./shared')
55
const { Function, Label, Line, Location, Profile, Sample, StringTable, ValueType } = require('pprof-format')
6+
const { availableParallelism, effectiveLibuvThreadCount } = require('../libuv-size')
67

78
// perf_hooks uses millis, with fractional part representing nanos. We emit nanos into the pprof file.
89
const MS_TO_NS = 1_000_000
@@ -38,6 +39,20 @@ function labelFromStrStr (stringTable, keyStr, valStr) {
3839
return labelFromStr(stringTable, stringTable.dedup(keyStr), valStr)
3940
}
4041

42+
function getMaxSamples (options) {
43+
const cpuSamplingInterval = (options.samplingInterval || 1e3 / 99) // 99hz
44+
const flushInterval = options.flushInterval || 65 * 1e3 // 60 seconds
45+
const maxCpuSamples = flushInterval / cpuSamplingInterval
46+
47+
// The lesser of max parallelism and libuv thread pool size, plus one so we can detect
48+
// oversubscription on libuv thread pool, plus another one for GC.
49+
const factor = Math.max(1, Math.min(availableParallelism(), effectiveLibuvThreadCount)) + 2
50+
51+
// Let's not go overboard with too large limit and cap it at 100k. With current defaults, the
52+
// value will be 65000/10.1*(4+2) = 38613.
53+
return Math.min(100000, Math.floor(maxCpuSamples * factor))
54+
}
55+
4156
class GCDecorator {
4257
constructor (stringTable) {
4358
this.stringTable = stringTable
@@ -181,12 +196,13 @@ const decoratorTypes = {
181196

182197
// Translates performance entries into pprof samples.
183198
class EventSerializer {
184-
constructor () {
199+
constructor (maxSamples) {
185200
this.stringTable = new StringTable()
186201
this.samples = []
187202
this.locations = []
188203
this.functions = []
189204
this.decorators = {}
205+
this.maxSamples = maxSamples
190206

191207
// A synthetic single-frame location to serve as the location for timeline
192208
// samples. We need these as the profiling backend (mimicking official pprof
@@ -204,6 +220,29 @@ class EventSerializer {
204220
}
205221

206222
addEvent (item) {
223+
if (this.samples.length < this.maxSamples) {
224+
const sample = this.createSample(item)
225+
if (sample !== undefined) {
226+
this.samples.push(sample)
227+
}
228+
} else {
229+
// Choose one sample to be dropped. Using rnd(max + 1) - 1 we allow the
230+
// current sample too to be the dropped one, with the equal likelihood as
231+
// any other sample.
232+
const replacementIndex = Math.floor(Math.random() * (this.maxSamples + 1)) - 1
233+
if (replacementIndex != -1) {
234+
const sample = this.createSample(item)
235+
if (sample !== undefined) {
236+
// This will cause the samples to no longer be sorted in their array
237+
// by their end time. This is fine as the backend has no ordering
238+
// expectations.
239+
this.samples[replacementIndex] = sample
240+
}
241+
}
242+
}
243+
}
244+
245+
createSample (item) {
207246
const { entryType, startTime, duration, _ddSpanId, _ddRootSpanId } = item
208247
let decorator = this.decorators[entryType]
209248
if (!decorator) {
@@ -236,7 +275,7 @@ class EventSerializer {
236275
label
237276
}
238277
decorator.decorateSample(sampleInput, item)
239-
this.samples.push(new Sample(sampleInput))
278+
return new Sample(sampleInput)
240279
}
241280

242281
createProfile (startDate, endDate) {
@@ -338,7 +377,7 @@ class CompositeEventSource {
338377
}
339378
}
340379

341-
function createPossionProcessSamplingFilter (samplingIntervalMillis) {
380+
function createPoissonProcessSamplingFilter (samplingIntervalMillis) {
342381
let nextSamplingInstant = performance.now()
343382
let currentSamplingInstant = 0
344383
setNextSamplingInstant()
@@ -377,12 +416,13 @@ function createPossionProcessSamplingFilter (samplingIntervalMillis) {
377416
class EventsProfiler {
378417
constructor (options = {}) {
379418
this.type = 'events'
380-
this.eventSerializer = new EventSerializer()
419+
this.maxSamples = getMaxSamples(options)
420+
this.eventSerializer = new EventSerializer(this.maxSamples)
381421

382422
const eventHandler = event => this.eventSerializer.addEvent(event)
383423
const eventFilter = options.timelineSamplingEnabled
384424
// options.samplingInterval comes in microseconds, we need millis
385-
? createPossionProcessSamplingFilter((options.samplingInterval ?? 1e6 / 99) / 1000)
425+
? createPoissonProcessSamplingFilter((options.samplingInterval ?? 1e6 / 99) / 1000)
386426
: _ => true
387427
const filteringEventHandler = event => {
388428
if (eventFilter(event)) {
@@ -414,7 +454,7 @@ class EventsProfiler {
414454
this.stop()
415455
}
416456
const thatEventSerializer = this.eventSerializer
417-
this.eventSerializer = new EventSerializer()
457+
this.eventSerializer = new EventSerializer(this.maxSamples)
418458
return () => thatEventSerializer.createProfile(startDate, endDate)
419459
}
420460

0 commit comments

Comments
 (0)