Skip to content

Commit

Permalink
fix(performance): Use bound instruments, to prevent serialization of …
Browse files Browse the repository at this point in the history
…labels on every update
  • Loading branch information
marcbachmann committed Dec 28, 2020
1 parent 3adc39c commit 3342ad6
Show file tree
Hide file tree
Showing 10 changed files with 84 additions and 91 deletions.
52 changes: 28 additions & 24 deletions metrics/eventLoopLag.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,51 +20,55 @@ module.exports = (meter, {prefix, labels, eventLoopMonitoringPrecision}) => {

histogram.enable()

const lag = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG, {
description: 'Lag of event loop in seconds.'
}).bind(labels)

const lagMin = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_MIN, {
description: 'The minimum recorded event loop delay.'
})
}).bind(labels)

const lagMax = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_MAX, {
description: 'The maximum recorded event loop delay.'
})
}).bind(labels)

const lagMean = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_MEAN, {
description: 'The mean of the recorded event loop delays.'
})
}).bind(labels)

const lagStddev = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_STDDEV, {
description: 'The standard deviation of the recorded event loop delays.'
})
}).bind(labels)

const lagP50 = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_P50, {
description: 'The 50th percentile of the recorded event loop delays.'
})
}).bind(labels)

const lagP90 = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_P90, {
description: 'The 90th percentile of the recorded event loop delays.'
})
}).bind(labels)

const lagP99 = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG_P99, {
description: 'The 99th percentile of the recorded event loop delays.'
})
}).bind(labels)

const lag = meter.createValueObserver(prefix + NODEJS_EVENTLOOP_LAG, {
description: 'Lag of event loop in seconds.'
})

function reportEventloopLag (start, observerBatchResult) {
function reportEventloopLag (start) {
const delta = process.hrtime(start)
const nanosec = (delta[0] * 1e9) + delta[1]
const seconds = nanosec / 1e9
observerBatchResult.observe(labels, [lag.observation(seconds)])
lag.update(seconds)
}

meter.createBatchObserver((observerBatchResult) => {
setImmediate(reportEventloopLag, process.hrtime(), observerBatchResult)

observerBatchResult.observe(labels, [
lagMin.observation(histogram.min / 1e9),
lagMax.observation(histogram.max / 1e9),
lagMean.observation(histogram.mean / 1e9),
lagStddev.observation(histogram.stddev / 1e9),
lagP50.observation(histogram.percentile(50) / 1e9),
lagP90.observation(histogram.percentile(90) / 1e9),
lagP99.observation(histogram.percentile(99) / 1e9)
])
setImmediate(reportEventloopLag, process.hrtime())

lagMin.update(histogram.min / 1e9)
lagMax.update(histogram.max / 1e9)
lagMean.update(histogram.mean / 1e9)
lagStddev.update(histogram.stddev / 1e9)
lagP50.update(histogram.percentile(50) / 1e9)
lagP90.update(histogram.percentile(90) / 1e9)
lagP99.update(histogram.percentile(99) / 1e9)
})

}
Expand Down
25 changes: 11 additions & 14 deletions metrics/gc.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
'use strict'
const perfHooks = require('perf_hooks')
const {PerformanceObserver, constants} = require('perf_hooks')

const NODEJS_GC_DURATION_SECONDS = 'nodejs_gc_duration_seconds'
const DEFAULT_GC_DURATION_BUCKETS = [0.001, 0.01, 0.1, 1, 2, 5]

const kinds = []
kinds[perfHooks.constants.NODE_PERFORMANCE_GC_MAJOR] = 'major'
kinds[perfHooks.constants.NODE_PERFORMANCE_GC_MINOR] = 'minor'
kinds[perfHooks.constants.NODE_PERFORMANCE_GC_INCREMENTAL] = 'incremental'
kinds[perfHooks.constants.NODE_PERFORMANCE_GC_WEAKCB] = 'weakcb'

module.exports = (meter, {prefix, labels, gcDurationBuckets}) => {
const boundaries = gcDurationBuckets || DEFAULT_GC_DURATION_BUCKETS

const gcHistogram = meter.createValueRecorder(prefix + NODEJS_GC_DURATION_SECONDS, {
const histogram = meter.createValueRecorder(prefix + NODEJS_GC_DURATION_SECONDS, {
description: 'Garbage collection duration by kind, one of major, minor, incremental or weakcb.',
boundaries
})

const obs = new perfHooks.PerformanceObserver(list => {
const entry = list.getEntries()[0]
const kinds = {}
kinds[constants.NODE_PERFORMANCE_GC_MAJOR] = histogram.bind({...labels, kind: 'major'})
kinds[constants.NODE_PERFORMANCE_GC_MINOR] = histogram.bind({...labels, kind: 'minor'})
kinds[constants.NODE_PERFORMANCE_GC_INCREMENTAL] = histogram.bind({...labels, kind: 'incremental'}) // eslint-disable-line max-len
kinds[constants.NODE_PERFORMANCE_GC_WEAKCB] = histogram.bind({...labels, kind: 'weakcb'})

gcHistogram
.bind({...labels, kind: kinds[entry.kind]})
// Convert duration from milliseconds to seconds
.record(entry.duration / 1000)
const obs = new PerformanceObserver(list => {
const entry = list.getEntries()[0]
// Convert duration from milliseconds to seconds
kinds[entry.kind].record(entry.duration / 1000)
})

// We do not expect too many gc events per second, so we do not use buffering
Expand Down
16 changes: 7 additions & 9 deletions metrics/heapSizeAndUsed.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,22 @@ const NODEJS_EXTERNAL_MEMORY = 'nodejs_external_memory_bytes'
module.exports = (meter, {labels, prefix}) => {
const heapSizeTotal = meter.createValueObserver(prefix + NODEJS_HEAP_SIZE_TOTAL, {
description: 'Process heap size from Node.js in bytes.'
})
}).bind(labels)

const heapSizeUsed = meter.createValueObserver(prefix + NODEJS_HEAP_SIZE_USED, {
description: 'Process heap size used from Node.js in bytes.'
})
}).bind(labels)

const externalMemUsed = meter.createValueObserver(prefix + NODEJS_EXTERNAL_MEMORY, {
description: 'Node.js external memory size in bytes.'
})
}).bind(labels)

meter.createBatchObserver((observerBatchResult) => {
meter.createBatchObserver(() => {
const memUsage = safeMemoryUsage()
if (!memUsage) return
observerBatchResult.observe(labels, [
heapSizeTotal.observation(memUsage.heapTotal),
heapSizeUsed.observation(memUsage.heapUsed),
memUsage.external !== undefined ? externalMemUsed.observation(memUsage.external) : undefined
])
heapSizeTotal.update(memUsage.heapTotal)
heapSizeUsed.update(memUsage.heapUsed)
if (memUsage.external !== undefined) externalMemUsed.update(memUsage.external)
})
}

Expand Down
11 changes: 7 additions & 4 deletions metrics/helpers/processMetricsHelpers.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
function createAggregatorByObjectName () {
const all = new Map()
return function aggregateByObjectName (list) {
return function aggregateByObjectName (metric, labels, list) {
const current = new Map()
for (const key of all.keys()) current.set(key, 0)

for (let i = 0; i < list.length; i++) {
const listElementConstructor = list[i] && list[i].constructor
if (typeof listElementConstructor === 'undefined') continue
current.set(listElementConstructor.name, (current.get(listElementConstructor.name) || 0) + 1)
}

for (const key of all.keys()) all.set(key, 0)
for (const [key, value] of current) all.set(key, value)
return all
for (const [key, value] of current) {
const instrument = all.get(key) || metric.bind({...labels, type: key})
instrument.update(value)
all.set(key, instrument)
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions metrics/osMemoryHeap.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ const safeMemoryUsage = require('./helpers/safeMemoryUsage')
const PROCESS_RESIDENT_MEMORY = 'process_resident_memory_bytes'

function notLinuxVariant (meter, {prefix, labels}) {
meter.createValueObserver(prefix + PROCESS_RESIDENT_MEMORY, {
const boundMeter = meter.createValueObserver(prefix + PROCESS_RESIDENT_MEMORY, {
description: 'Resident memory size in bytes.'
}, (observerResult) => {
}, () => {
const memUsage = safeMemoryUsage()
// I don't think the other things returned from
// `process.memoryUsage()` is relevant to a standard export
if (memUsage) observerResult.observe(memUsage.rss, labels)
})
if (memUsage) boundMeter.update(memUsage.rss)
}).bind(labels)
}

module.exports = process.platform === 'linux'
Expand Down
16 changes: 7 additions & 9 deletions metrics/osMemoryHeapLinux.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,17 @@ function structureOutput (input) {
module.exports = (meter, {prefix, labels}) => {
const residentMemGauge = meter.createValueObserver(prefix + PROCESS_RESIDENT_MEMORY, {
description: 'Resident memory size in bytes.'
})
}).bind(labels)

const virtualMemGauge = meter.createValueObserver(prefix + PROCESS_VIRTUAL_MEMORY, {
description: 'Virtual memory size in bytes.'
})
}).bind(labels)

const heapSizeMemGauge = meter.createValueObserver(prefix + PROCESS_HEAP, {
description: 'Process heap size in bytes.'
})
}).bind(labels)

meter.createBatchObserver((observerBatchResult) => {
meter.createBatchObserver(() => {
try {
// Sync I/O is often problematic, but /proc isn't really I/O, it
// a virtual filesystem that maps directly to in-kernel data
Expand All @@ -52,11 +52,9 @@ module.exports = (meter, {prefix, labels}) => {
const stat = fs.readFileSync('/proc/self/status', 'utf8')
const structuredOutput = structureOutput(stat)

observerBatchResult.observe(labels, [
residentMemGauge.observation(structuredOutput.VmRSS),
virtualMemGauge.observation(structuredOutput.VmSize),
heapSizeMemGauge.observation(structuredOutput.VmData)
])
residentMemGauge.update(structuredOutput.VmRSS)
virtualMemGauge.update(structuredOutput.VmSize)
heapSizeMemGauge.update(structuredOutput.VmData)
} catch {
// noop
}
Expand Down
18 changes: 7 additions & 11 deletions metrics/processHandles.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,18 @@ module.exports = (meter, {prefix, labels}) => {
if (typeof process._getActiveHandles !== 'function') return

const aggregateByObjectName = createAggregatorByObjectName()
meter.createValueObserver(prefix + NODEJS_ACTIVE_HANDLES, {
const activeHandlesMetric = meter.createValueObserver(prefix + NODEJS_ACTIVE_HANDLES, {
description: 'Number of active libuv handles grouped by handle type. Every handle type is C++ class name.' // eslint-disable-line max-len
}, (observerResult) => {
const handles = process._getActiveHandles()
const data = aggregateByObjectName(handles)
for (const [key, count] of data.entries()) {
observerResult.observe(count, {...labels, type: key})
}
}, () => {
aggregateByObjectName(activeHandlesMetric, labels, process._getActiveHandles())
})

meter.createValueObserver(prefix + NODEJS_ACTIVE_HANDLES_TOTAL, {
const boundTotalMetric = meter.createValueObserver(prefix + NODEJS_ACTIVE_HANDLES_TOTAL, {
description: 'Total number of active handles.'
}, (observerResult) => {
}, () => {
const handles = process._getActiveHandles()
observerResult.observe(handles.length, labels)
})
boundTotalMetric.update(handles.length)
}).bind(labels)
}

module.exports.metricNames = [
Expand Down
8 changes: 4 additions & 4 deletions metrics/processOpenFileDescriptors.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ const PROCESS_OPEN_FDS = 'process_open_fds'
module.exports = (meter, {prefix, labels}) => {
if (process.platform !== 'linux') return

meter.createValueObserver(prefix + PROCESS_OPEN_FDS, {
const boundInstrument = meter.createValueObserver(prefix + PROCESS_OPEN_FDS, {
description: 'Number of open file descriptors.'
}, (observerResult) => {
}, () => {
try {
const fds = fs.readdirSync('/proc/self/fd')
// Minus 1 to not count the fd that was used by readdirSync(),
// it's now closed.
observerResult.observe(fds.length - 1, labels)
boundInstrument.update(fds.length - 1)
} catch {
// noop
}
})
}).bind(labels)
}

module.exports.metricNames = [PROCESS_OPEN_FDS]
18 changes: 7 additions & 11 deletions metrics/processRequests.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,17 @@ module.exports = (meter, {prefix, labels}) => {
if (typeof process._getActiveRequests !== 'function') return

const aggregateByObjectName = createAggregatorByObjectName()
meter.createValueObserver(prefix + NODEJS_ACTIVE_REQUESTS, {
const activeRequestsMetric = meter.createValueObserver(prefix + NODEJS_ACTIVE_REQUESTS, {
description: 'Number of active libuv requests grouped by request type. Every request type is C++ class name.' // eslint-disable-line max-len
}, (observerResult) => {
const requests = process._getActiveRequests()
const data = aggregateByObjectName(requests)
for (const [key, count] of data.entries()) {
observerResult.observe(count, {...labels, type: key})
}
}, () => {
aggregateByObjectName(activeRequestsMetric, labels, process._getActiveRequests())
})

meter.createValueObserver(prefix + NODEJS_ACTIVE_REQUESTS_TOTAL, {
const boundTotalRequests = meter.createValueObserver(prefix + NODEJS_ACTIVE_REQUESTS_TOTAL, {
description: 'Total number of active requests.'
}, (observerResult) => {
observerResult.observe(process._getActiveRequests().length, labels)
})
}, () => {
boundTotalRequests.update(process._getActiveRequests().length)
}).bind(labels)
}

module.exports.metricNames = [
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
"license": "Apache-2.0",
"release": {
"extends": "@livingdocs/semantic-release-presets/npm-github-verify"
}
},
"dependencies": {}
}

0 comments on commit 3342ad6

Please sign in to comment.