From f7e3cf3e60b516f997c5a7a081fdf2c1bfc9f078 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 12 Jan 2026 17:30:12 -0500 Subject: [PATCH 1/3] perf(sdk-trace-base): use counter for attribute limit check in Span MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace O(n) Object.keys().length call with O(1) counter lookup when checking attribute count limits in setAttribute(). This eliminates O(n²) complexity when adding many attributes to a span. Benchmark results show up to 22x improvement for spans with 128 attributes. --- .../opentelemetry-sdk-trace-base/src/Span.ts | 13 ++- .../performance/benchmark/setAttributes.js | 99 +++++++++++++++++++ 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 packages/opentelemetry-sdk-trace-base/test/performance/benchmark/setAttributes.js diff --git a/packages/opentelemetry-sdk-trace-base/src/Span.ts b/packages/opentelemetry-sdk-trace-base/src/Span.ts index 508557f6a34..c9b1cd9cabe 100644 --- a/packages/opentelemetry-sdk-trace-base/src/Span.ts +++ b/packages/opentelemetry-sdk-trace-base/src/Span.ts @@ -93,6 +93,7 @@ export class SpanImpl implements Span { private _droppedAttributesCount = 0; private _droppedEventsCount: number = 0; private _droppedLinksCount: number = 0; + private _attributesCount: number = 0; name: string; status: SpanStatus = { @@ -157,16 +158,24 @@ export class SpanImpl implements Span { } const { attributeCountLimit } = this._spanLimits; + const isNewKey = !Object.prototype.hasOwnProperty.call( + this.attributes, + key + ); if ( attributeCountLimit !== undefined && - Object.keys(this.attributes).length >= attributeCountLimit && - !Object.prototype.hasOwnProperty.call(this.attributes, key) + this._attributesCount >= attributeCountLimit && + isNewKey ) { this._droppedAttributesCount++; return this; } + this.attributes[key] = this._truncateToSize(value); + if (isNewKey) { + this._attributesCount++; + } return this; } diff --git a/packages/opentelemetry-sdk-trace-base/test/performance/benchmark/setAttributes.js b/packages/opentelemetry-sdk-trace-base/test/performance/benchmark/setAttributes.js new file mode 100644 index 00000000000..ef0d96c4bc1 --- /dev/null +++ b/packages/opentelemetry-sdk-trace-base/test/performance/benchmark/setAttributes.js @@ -0,0 +1,99 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const Benchmark = require('benchmark'); +const { BasicTracerProvider } = require('../../../build/src'); + +const tracerProvider = new BasicTracerProvider(); +const tracer = tracerProvider.getTracer('test'); + +const suite = new Benchmark.Suite('setAttribute performance'); + +suite.on('cycle', event => { + console.log(String(event.target)); +}); + +// Generate attribute keys for different test sizes +function generateAttributeKeys(count) { + const keys = []; + for (let i = 0; i < count; i++) { + keys.push(`attribute_key_${i.toString().padStart(4, '0')}`); + } + return keys; +} + +const keys10 = generateAttributeKeys(10); +const keys50 = generateAttributeKeys(50); +const keys100 = generateAttributeKeys(100); +const keys128 = generateAttributeKeys(128); + +// Test with 10 attributes +suite.add('setAttribute (10 attributes)', function () { + const span = tracer.startSpan('span'); + for (let i = 0; i < keys10.length; i++) { + span.setAttribute(keys10[i], 'test_value'); + } + span.end(); +}); + +// Test with 50 attributes +suite.add('setAttribute (50 attributes)', function () { + const span = tracer.startSpan('span'); + for (let i = 0; i < keys50.length; i++) { + span.setAttribute(keys50[i], 'test_value'); + } + span.end(); +}); + +// Test with 100 attributes +suite.add('setAttribute (100 attributes)', function () { + const span = tracer.startSpan('span'); + for (let i = 0; i < keys100.length; i++) { + span.setAttribute(keys100[i], 'test_value'); + } + span.end(); +}); + +// Test with 128 attributes (default limit) +suite.add('setAttribute (128 attributes - at limit)', function () { + const span = tracer.startSpan('span'); + for (let i = 0; i < keys128.length; i++) { + span.setAttribute(keys128[i], 'test_value'); + } + span.end(); +}); + +// Test setAttribute with overwriting existing keys (no Object.keys growth) +suite.add('setAttribute (100 overwrites of same key)', function () { + const span = tracer.startSpan('span'); + for (let i = 0; i < 100; i++) { + span.setAttribute('same_key', 'test_value_' + i); + } + span.end(); +}); + +// Test setAttributes in bulk vs individual setAttribute +suite.add('setAttributes bulk (100 attributes)', function () { + const span = tracer.startSpan('span'); + const attrs = {}; + for (let i = 0; i < 100; i++) { + attrs[keys100[i]] = 'test_value'; + } + span.setAttributes(attrs); + span.end(); +}); + +suite.run({ async: false }); From da51619e3cf1d3594d10f816e717c6da54fb5138 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 12 Jan 2026 17:39:53 -0500 Subject: [PATCH 2/3] changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c946fbbf104..343b827cfab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * chore(browser): fix CODEOWNERS paths for browser-related packages * refactor(sdk-metrics): remove Promise.allSettled() ponyfill [#6277](https://github.com/open-telemetry/opentelemetry-js/pull/6277) @cjihrig +* perf(sdk-trace-base): optimize setAttribute performance [#6283](https://github.com/open-telemetry/opentelemetry-js/pull/6283) @AbhiPrasad ## 2.3.0 From ff7c343c7970a0c07e862811dcbdbdaa53a028c2 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Thu, 29 Jan 2026 10:59:42 -0500 Subject: [PATCH 3/3] move around changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 343b827cfab..cbc3b5290b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * perf(sdk-trace-base): use Uint8Array for browser RandomIdGenerator [#6209](https://github.com/open-telemetry/opentelemetry-js/pull/6209) @overbalance * test(sdk-trace-base): remove obsolete TypeScript and platform workarounds [#6327](https://github.com/open-telemetry/opentelemetry-js/pull/6327) @overbalance * fix(example-web): update Docker config and deps for collector [#6342](https://github.com/open-telemetry/opentelemetry-js/pull/6342) @overbalance +* perf(sdk-trace-base): optimize setAttribute performance [#6283](https://github.com/open-telemetry/opentelemetry-js/pull/6283) @AbhiPrasad ## 2.5.0 @@ -50,7 +51,6 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 * chore(browser): fix CODEOWNERS paths for browser-related packages * refactor(sdk-metrics): remove Promise.allSettled() ponyfill [#6277](https://github.com/open-telemetry/opentelemetry-js/pull/6277) @cjihrig -* perf(sdk-trace-base): optimize setAttribute performance [#6283](https://github.com/open-telemetry/opentelemetry-js/pull/6283) @AbhiPrasad ## 2.3.0