Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
29 changes: 16 additions & 13 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down
16 changes: 4 additions & 12 deletions lib/statsFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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;
};
}

Expand Down
2 changes: 1 addition & 1 deletion lib/statsd.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
70 changes: 51 additions & 19 deletions perfTest/test.js
Original file line number Diff line number Diff line change
@@ -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();