3
3
const { performance, constants, PerformanceObserver } = require ( 'perf_hooks' )
4
4
const { END_TIMESTAMP_LABEL , SPAN_ID_LABEL , LOCAL_ROOT_SPAN_ID_LABEL , encodeProfileAsync } = require ( './shared' )
5
5
const { Function, Label, Line, Location, Profile, Sample, StringTable, ValueType } = require ( 'pprof-format' )
6
+ const { availableParallelism, effectiveLibuvThreadCount } = require ( '../libuv-size' )
6
7
7
8
// perf_hooks uses millis, with fractional part representing nanos. We emit nanos into the pprof file.
8
9
const MS_TO_NS = 1_000_000
@@ -38,6 +39,20 @@ function labelFromStrStr (stringTable, keyStr, valStr) {
38
39
return labelFromStr ( stringTable , stringTable . dedup ( keyStr ) , valStr )
39
40
}
40
41
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 ( 100_000 , Math . floor ( maxCpuSamples * factor ) )
54
+ }
55
+
41
56
class GCDecorator {
42
57
constructor ( stringTable ) {
43
58
this . stringTable = stringTable
@@ -181,12 +196,13 @@ const decoratorTypes = {
181
196
182
197
// Translates performance entries into pprof samples.
183
198
class EventSerializer {
184
- constructor ( ) {
199
+ constructor ( maxSamples ) {
185
200
this . stringTable = new StringTable ( )
186
201
this . samples = [ ]
187
202
this . locations = [ ]
188
203
this . functions = [ ]
189
204
this . decorators = { }
205
+ this . maxSamples = maxSamples
190
206
191
207
// A synthetic single-frame location to serve as the location for timeline
192
208
// samples. We need these as the profiling backend (mimicking official pprof
@@ -204,6 +220,29 @@ class EventSerializer {
204
220
}
205
221
206
222
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 ) {
207
246
const { entryType, startTime, duration, _ddSpanId, _ddRootSpanId } = item
208
247
let decorator = this . decorators [ entryType ]
209
248
if ( ! decorator ) {
@@ -236,7 +275,7 @@ class EventSerializer {
236
275
label
237
276
}
238
277
decorator . decorateSample ( sampleInput , item )
239
- this . samples . push ( new Sample ( sampleInput ) )
278
+ return new Sample ( sampleInput )
240
279
}
241
280
242
281
createProfile ( startDate , endDate ) {
@@ -338,7 +377,7 @@ class CompositeEventSource {
338
377
}
339
378
}
340
379
341
- function createPossionProcessSamplingFilter ( samplingIntervalMillis ) {
380
+ function createPoissonProcessSamplingFilter ( samplingIntervalMillis ) {
342
381
let nextSamplingInstant = performance . now ( )
343
382
let currentSamplingInstant = 0
344
383
setNextSamplingInstant ( )
@@ -377,12 +416,13 @@ function createPossionProcessSamplingFilter (samplingIntervalMillis) {
377
416
class EventsProfiler {
378
417
constructor ( options = { } ) {
379
418
this . type = 'events'
380
- this . eventSerializer = new EventSerializer ( )
419
+ this . maxSamples = getMaxSamples ( options )
420
+ this . eventSerializer = new EventSerializer ( this . maxSamples )
381
421
382
422
const eventHandler = event => this . eventSerializer . addEvent ( event )
383
423
const eventFilter = options . timelineSamplingEnabled
384
424
// options.samplingInterval comes in microseconds, we need millis
385
- ? createPossionProcessSamplingFilter ( ( options . samplingInterval ?? 1e6 / 99 ) / 1000 )
425
+ ? createPoissonProcessSamplingFilter ( ( options . samplingInterval ?? 1e6 / 99 ) / 1000 )
386
426
: _ => true
387
427
const filteringEventHandler = event => {
388
428
if ( eventFilter ( event ) ) {
@@ -414,7 +454,7 @@ class EventsProfiler {
414
454
this . stop ( )
415
455
}
416
456
const thatEventSerializer = this . eventSerializer
417
- this . eventSerializer = new EventSerializer ( )
457
+ this . eventSerializer = new EventSerializer ( this . maxSamples )
418
458
return ( ) => thatEventSerializer . createProfile ( startDate , endDate )
419
459
}
420
460
0 commit comments