Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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: pre-join global tags, eliminate Buffer round-trip for TCP/stream, cache byteLength, use hrtime.bigint, optimize overrideTags

## 14.0.0 (2026-2-15)

Expand Down
63 changes: 50 additions & 13 deletions lib/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,38 +96,75 @@ function overrideTags (parent, child, telegraf) {
return parent;
}

const childCopy = {};
const formattedChild = formatTags(child, telegraf);

// Fast path: if no child tag key appears in the parent, just concatenate.
// This is the common case when per-metric tags don't overlap with global tags.
const parentKeys = new Set();
for (const tag of parent) {
const idx = typeof tag === 'string' ? tag.indexOf(':') : -1;
if (idx > 0) {
parentKeys.add(tag.substring(0, idx));
}
}

let hasOverlap = false;
const kvTags = [];
const valueTags = [];
for (const tag of formattedChild) {
const idx = typeof tag === 'string' ? tag.indexOf(':') : -1;
if (idx < 1) {
valueTags.push(tag);
} else {
kvTags.push(tag);
if (parentKeys.has(tag.substring(0, idx))) {
hasOverlap = true;
break;
}
}
}

if (!hasOverlap) {
// Fast path: key:value child tags first, then value-only child tags (matching slow-path ordering)
const result = parent.concat(kvTags);
result.push(...valueTags);
return result;
}

// Slow path: rebuild parent, replacing tags whose keys appear in child.
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
15 changes: 13 additions & 2 deletions lib/statsd.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ const Client = function (host, port, prefix, suffix, globalize, cacheDns, mock,
concat(availableDDEnvs.map(env => `${DD_ENV_GLOBAL_TAGS_MAPPING[env]}:${helpers.sanitizeTags(process.env[env])}`));
}
}
this._globalTagStr = this.globalTags.length > 0 ?
`|${this.tagPrefix}${this.globalTags.join(this.tagSeparator)}` : '';
this.telegraf = options.telegraf || false;
if (options.maxBufferSize !== undefined) {
this.maxBufferSize = options.maxBufferSize;
Expand Down Expand Up @@ -359,7 +361,11 @@ Client.prototype.send = function (message, tags, callback) {
return tag.substring(0, idx) + '=' + tag.substring(idx + 1);
}).join(',');
message = `${message[0]},${tagStr}:${message.slice(1).join(':')}`;
} else if (mergedTags === this.globalTags) {
// Fast path: no per-metric tags, use pre-joined string
message += this._globalTagStr;
} else {
// Slow path: per-metric tags were merged, must rejoin
message += `|${this.tagPrefix}${mergedTags.join(this.tagSeparator)}`;
}
}
Expand Down Expand Up @@ -505,7 +511,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 All @@ -528,7 +534,10 @@ Client.prototype.sendMessage = function (message, callback, isTelemetry) {
this.messagesInFlight++;
debug('hot-shots sendMessage: sending %d bytes via %s transport (messagesInFlight=%d)',
Buffer.byteLength(message), this.protocol, this.messagesInFlight);
this.socket.send(Buffer.from(message), handleCallback);
const payload = (this.protocol === PROTOCOL.TCP || this.protocol === PROTOCOL.STREAM) ?
message :
Buffer.from(message);
this.socket.send(payload, handleCallback);
} catch (err) {
debug('hot-shots sendMessage: exception during send - %s', err.message);
handleCallback(err);
Expand Down Expand Up @@ -671,6 +680,8 @@ const ChildClient = function (parent, options) {
// Child inherits telemetry from parent (for metric tracking)
telemetry : parent.telemetry
});
this._globalTagStr = this.globalTags.length > 0 ?
`|${this.tagPrefix}${this.globalTags.join(this.tagSeparator)}` : '';
};
util.inherits(ChildClient, Client);

Expand Down
10 changes: 5 additions & 5 deletions lib/transport.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ let unixDgram;
const UDS_PATH_DEFAULT = '/var/run/datadog/dsd.socket';

/**
* Ensures a buffer ends with a newline character for line-based protocols.
* @param {Buffer} buf - The buffer to check and modify
* @returns {string} The buffer content as a string with newline appended if needed
* Ensures a message ends with a newline character for line-based protocols.
* @param {Buffer|string} buf - The buffer or string to check and modify
* @returns {string} The message as a string with newline appended if needed
*/
const addEol = (buf) => {
let msg = buf.toString();
const msg = typeof buf === 'string' ? buf : buf.toString();
if (msg.length > 0 && msg[msg.length - 1] !== '\n') {
msg += '\n';
return msg + '\n';
}
return msg;
};
Expand Down