diff --git a/CHANGES.md b/CHANGES.md index 4f94355b..f44db2a9 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ CHANGELOG ## Unreleased * @bdeitte Fill in some missing areas for automated tests +* @bdeitte CPU performance improvements: cache byteLength in sendMessage, use hrtime.bigint in timer functions, use Map in overrideTags ## 14.0.0 (2026-2-15) diff --git a/lib/helpers.js b/lib/helpers.js index db5d1508..4f5f2020 100644 --- a/lib/helpers.js +++ b/lib/helpers.js @@ -96,38 +96,41 @@ function overrideTags (parent, child, telegraf) { return parent; } - const childCopy = {}; + const formattedChild = formatTags(child, telegraf); + + const childCopy = new Map(); const toAppend = []; - formatTags(child, telegraf).forEach(tag => { + formattedChild.forEach(tag => { const idx = typeof tag === 'string' ? tag.indexOf(':') : -1; - if (idx < 1) { // Not found or first character + if (idx < 1) { toAppend.push(tag); } else { const key = tag.substring(0, idx); const value = tag.substring(idx + 1); - childCopy[key] = childCopy[key] || []; - childCopy[key].push(value); + if (!childCopy.has(key)) { + childCopy.set(key, []); + } + childCopy.get(key).push(value); } }); const result = parent.filter(tag => { const idx = typeof tag === 'string' ? tag.indexOf(':') : -1; - if (idx < 1) { // Not found or first character + if (idx < 1) { return true; } - const key = tag.substring(0, idx); - - return !childCopy.hasOwnProperty(key); + return !childCopy.has(key); }); - Object.keys(childCopy).forEach(key => { - for (const value of childCopy[key]) { + for (const [key, values] of childCopy) { + for (const value of values) { result.push(`${key}:${value}`); } - }); - return result.concat(toAppend); + } + result.push(...toAppend); + return result; } /** diff --git a/lib/statsFunctions.js b/lib/statsFunctions.js index 4d9dee78..86435fba 100644 --- a/lib/statsFunctions.js +++ b/lib/statsFunctions.js @@ -36,15 +36,11 @@ function applyStatsFns (Client) { return (...args) => { const ctx = createTimerContext(); - const start = process.hrtime(); + const start = process.hrtime.bigint(); try { return func(...args, ctx); } finally { - // get duration in milliseconds - const durationComponents = process.hrtime(start); - const seconds = durationComponents[0]; - const nanoseconds = durationComponents[1]; - const duration = (seconds * 1000) + (nanoseconds / 1E6); + const duration = Number(process.hrtime.bigint() - start) / 1e6; const finalTags = mergeTags(tags, ctx.getTags(), _this.telegraf); _this.timing( @@ -118,14 +114,10 @@ function applyStatsFns (Client) { * High-resolution timer */ function hrtimer() { - const start = process.hrtime(); + const start = process.hrtime.bigint(); return () => { - const durationComponents = process.hrtime(start); - const seconds = durationComponents[0]; - const nanoseconds = durationComponents[1]; - const duration = (seconds * 1000) + (nanoseconds / 1E6); - return duration; + return Number(process.hrtime.bigint() - start) / 1e6; }; } diff --git a/lib/statsd.js b/lib/statsd.js index a46d6107..43e6b8df 100644 --- a/lib/statsd.js +++ b/lib/statsd.js @@ -505,7 +505,7 @@ Client.prototype.sendMessage = function (message, callback, isTelemetry) { protocolErrorHandler(this, this.protocol, err); } } else { - debug('hot-shots sendMessage: successfully sent %d bytes', Buffer.byteLength(message)); + debug('hot-shots sendMessage: successfully sent %d bytes', messageBytes); // Track bytes sent successfully (only for non-telemetry messages) if (this.telemetry && !isTelemetry) { this.telemetry.recordBytesSent(messageBytes); diff --git a/package.json b/package.json index b88c17a0..f549db36 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "coverage": "nyc --reporter=lcov --reporter=text npm test", "test": "mocha -R spec --timeout 5000 test/*.js", "lint": "eslint \"./lib/**/*.js\" \"./test/**/*.js\"", + "perf": "node perfTest/test.js", "pretest": "npm run lint" }, "optionalDependencies": { diff --git a/perfTest/test.js b/perfTest/test.js index 09ff3c6e..553b33b4 100644 --- a/perfTest/test.js +++ b/perfTest/test.js @@ -1,21 +1,53 @@ -const statsD = require('../lib/statsd'); -let count = 0; -const options = { - maxBufferSize: process.argv[2] -}; -const statsd = new statsD(options); - -let start = new Date(); - -function sendPacket() { - count++; - statsd.increment('abc.cde.efg.ghk.klm', 1); - if(count %100000 === 0) { - const stop = new Date(); - console.log(stop - start); - start = stop; - } - setImmediate(sendPacket); +'use strict'; + +const StatsD = require('../lib/statsd'); + +const WARMUP = process.env.WARMUP ? parseInt(process.env.WARMUP) : 20000; +const ITERS = process.env.ITERS ? parseInt(process.env.ITERS) : 300000; + +const noTagClient = new StatsD({ mock: true }); +const globalTagClient = new StatsD({ + mock: true, + globalTags: { env: 'prod', region: 'us-east-1', service: 'api' } +}); + +const timerWrapped = noTagClient.timer(function noop() {}, 'hot.shots.perf.timer'); + +function bench(label, fn) { + noTagClient.mockBuffer = []; + globalTagClient.mockBuffer = []; + + for (let i = 0; i < WARMUP; i++) { fn(); } + + noTagClient.mockBuffer = []; + globalTagClient.mockBuffer = []; + + const start = process.hrtime.bigint(); + for (let i = 0; i < ITERS; i++) { fn(); } + const ns = Number(process.hrtime.bigint() - start); + + const opsPerSec = Math.round(ITERS / (ns / 1e9)); + console.log(` ${label.padEnd(45)} ${opsPerSec.toLocaleString().padStart(14)} ops/sec`); } -sendPacket(); +console.log(`\nhot-shots performance (${ITERS.toLocaleString()} iters, ${WARMUP.toLocaleString()} warmup, mock mode):\n`); + +bench('increment, no tags', + () => noTagClient.increment('hot.shots.perf.metric', 1)); + +bench('increment, global tags only', + () => globalTagClient.increment('hot.shots.perf.metric', 1)); + +bench('increment, per-metric tags (no overlap)', + () => noTagClient.increment('hot.shots.perf.metric', 1, { status: 'ok', host: 'web-01' })); + +bench('increment, per-metric + global tags (overlap)', + () => globalTagClient.increment('hot.shots.perf.metric', 1, { env: 'staging', version: 'v2' })); + +bench('timing', + () => noTagClient.timing('hot.shots.perf.metric', 250)); + +bench('timer wrapper', + () => timerWrapped()); + +console.log();