Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b3791ae
perf(sdk-trace-base): use Uint8Array for browser RandomIdGenerator
overbalance Dec 10, 2025
ee2d94d
chore: add changelog entry for browser RandomIdGenerator perf
overbalance Dec 10, 2025
d7fe798
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Dec 17, 2025
be94956
test(sdk-trace-base): add benchmark for browser RandomIdGenerator
overbalance Dec 17, 2025
462afae
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Dec 17, 2025
a0b0a12
Merge branch 'main' into overbalance/perf-browser-random-id
overbalance Dec 18, 2025
b8a4740
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Dec 19, 2025
ee3f9ea
refactor(sdk-trace-base): reorganize test structure and add benchmarks
overbalance Dec 19, 2025
e04cec4
test(sdk-trace-base): add browser benchmark for RandomIdGenerator
overbalance Dec 19, 2025
16ca8c7
Update karma.webpack.js
overbalance Dec 19, 2025
57e44ed
chore: dont include config in coverage
overbalance Dec 19, 2025
359171b
Merge branch 'main' into overbalance/perf-browser-random-id
overbalance Dec 19, 2025
8f9588e
Merge branch 'main' into overbalance/perf-browser-random-id
overbalance Dec 29, 2025
b3ad1c7
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Jan 6, 2026
57076cf
Merge branch 'main' into overbalance/perf-browser-random-id
overbalance Jan 7, 2026
d7cadc9
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Jan 8, 2026
65d7875
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Jan 8, 2026
449ec34
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Jan 9, 2026
1b2ff44
Update CHANGELOG.md
overbalance Jan 9, 2026
06e9418
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Jan 13, 2026
c2d34a4
Merge branch 'main' into overbalance/perf-browser-random-id
overbalance Jan 20, 2026
6f0629b
don't write browser benchmarks to log
overbalance Jan 20, 2026
4c703e2
refactor(sdk-trace-base): split benchmarks into node and browser
overbalance Jan 20, 2026
46a2879
whitespace
overbalance Jan 20, 2026
e0649b2
Merge remote-tracking branch 'upstream/main' into overbalance/perf-br…
overbalance Jan 21, 2026
6cb1e9b
dont write browser benchmarks to disk
overbalance Jan 21, 2026
ebc8bbe
add global command
overbalance Jan 21, 2026
3499c8f
Update CHANGELOG.md
overbalance Jan 21, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2

### :house: Internal

* perf(sdk-trace-base): use Uint8Array for browser RandomIdGenerator [#6209](https://github.com/open-telemetry/opentelemetry-js/pull/6209) @overbalance

## 2.5.0

### :bug: Bug Fixes
Expand Down
6 changes: 6 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
codecov:
notify:
require_ci_to_pass: no
ignore:
- "*.js"
- "**/karma.*.js"
- "**/.eslintrc.js"
- "**/*.config.*"
- "scripts/"
comment:
layout: "header, changes, diff, files"
behavior: default
Expand Down
1 change: 1 addition & 0 deletions eslint.base.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ module.exports = {
"projectService": true
},
rules: {
"no-console": "warn",
"no-empty": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/no-unsafe-function-type": ["warn"],
Expand Down
6 changes: 6 additions & 0 deletions karma.webpack.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ module.exports = {
// Thanks to https://stackoverflow.com/a/65018686/14239942
// NOTE: I wish there was a better way as this pollutes the tests with a defined 'process' global.
process: 'process/browser.js'
}),
// Benchmark.js checks for AMD's define function which doesn't exist in webpack.
// NOTE: This pollutes tests with a defined 'self.define' global.
new webpack.BannerPlugin({
banner: 'self.define = self.define || Object.assign(function(){}, {amd: false});',
raw: true
})
],
module: {
Expand Down
8 changes: 8 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"test:browser": "nx run-many -t test:browser",
"test:webworker": "nx run-many -t test:webworker",
"test:bench": "nx run-many -t test:bench",
"test:bench:browser": "nx run-many -t test:bench:browser -- --verbose",
"predocs-test": "npm run docs",
"docs": "typedoc --readme none && touch docs/.nojekyll",
"docs-deploy": "gh-pages --dotfiles --dist docs",
Expand Down
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we keep this file? This is a benchmark running node using browser code. I think quoting some benchmark code (or perhaps a https://jsben.ch or similar link) showing browser numbers in the PR discussion would be good, but not sure of the value of having the benchmark file persisting in the repo.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great point. Let me try to add a browser bench test.

Copy link
Copy Markdown
Contributor Author

@overbalance overbalance Dec 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trentm I moved some files around so the platform chosen in package.json is used for common tests, including the new benchmark test for the randomIdGenerator. It expanded the scope quite a bit but now reports numbers for node and browser. I updated the description above with node, browser (main), and browser (this branch) results.

If this is too many changes for this PR I can back them out and make a new branch for the test folder.

Copy link
Copy Markdown
Contributor Author

@overbalance overbalance Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@trentm I added browser benchmark tests that do not write to disk. I just saw @pichlermarc's note about being judicious with the amount of benchmarks published.

I defer to you whether or not they should be published (in a follow-up)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, thanks. Sounds good to me. My main concern had been about the (lack of) value of having those metrics included in the published benchmarks.

Marc had mentioned that he has a PR coming at some point to reduce the set of metrics that will be included in the published set (by just changing the benchmark to not output results to the benchmark.txt file that is slurped up by the benchmark.yml CI, I believe).

Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,18 @@
* limitations under the License.
*/

require('./span');
require('./BatchSpanProcessor');
const karmaWebpackConfig = require('../../karma.webpack');
const karmaBaseConfig = require('../../karma.base');

module.exports = config => {
config.set(
Object.assign({}, karmaBaseConfig, {
webpack: karmaWebpackConfig,
files: ['test/browser/**/*.bench.ts'],
preprocessors: {
'test/browser/**/*.bench.ts': ['webpack'],
},
browserNoActivityTimeout: 120000,
})
);
};
4 changes: 3 additions & 1 deletion packages/opentelemetry-sdk-trace-base/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"test": "nyc mocha 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'",
"test:browser": "karma start --single-run",
"test:webworker": "karma start karma.worker.js --single-run",
"test:bench": "node test/performance/benchmark/index.js | tee .benchmark-results.txt",
"test:bench": "mocha 'test/node/**/*.bench.ts' | grep 'ops/sec' | tee test/node/.benchmark-results.txt",
"test:bench:browser": "karma start karma.bench.js --single-run",
"tdd": "npm run tdd:node",
"tdd:node": "npm run test -- --watch-extensions ts --watch",
"tdd:browser": "karma start",
Expand Down Expand Up @@ -64,6 +65,7 @@
},
"devDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0",
"@types/benchmark": "2.1.5",
"@types/mocha": "10.0.10",
"@types/node": "18.19.130",
"@types/sinon": "17.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,36 +16,56 @@

import { IdGenerator } from '../../IdGenerator';

const SPAN_ID_BYTES = 8;
const TRACE_ID_BYTES = 16;
const SPAN_ID_BYTES = 8;

const TRACE_BUFFER = new Uint8Array(TRACE_ID_BYTES);
const SPAN_BUFFER = new Uint8Array(SPAN_ID_BYTES);

// Byte-to-hex lookup is faster than toString(16) in browsers
const HEX: string[] = Array.from({ length: 256 }, (_, i) =>
i.toString(16).padStart(2, '0')
);

/**
* Fills buffer with random bytes, ensuring at least one is non-zero
* per W3C Trace Context spec.
*/
function randomFill(buf: Uint8Array): void {
for (let i = 0; i < buf.length; i++) {
buf[i] = (Math.random() * 256) >>> 0;
}
// Ensure non-zero
for (let i = 0; i < buf.length; i++) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: as a fallback a '1' is written in the last position to ensure the buf does not contain all zeroes. Maybe accessing a random position of the array and setting it to '1' if not already set would allow is to skip a second iteration.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe accessing a random position of the array and setting it to '1' if not already set would allow is to skip a second iteration.

I'm not a mathematician but I'd worry a bit that that biases the distribution away from values with 0s.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Me neither but what you said makes a lot of sense. The I guess a flag in the loop that fills the buf would be enough

Something like

let allZero = true;
for (let i = 0; i < buf.length; i++) {
    buf[i] = (Math.random() * 256) >>> 0;
    allZero = allZero && buf[i] === 0;
}

if (allZero) {
  buf[buf.length - 1] = 1;
}

Copy link
Copy Markdown
Contributor Author

@overbalance overbalance Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@david-luna the allZero check in the loop is about 20% slower than the proposed function.

if (buf[i] > 0) return;
}
buf[buf.length - 1] = 1;
Comment on lines +35 to +42
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: did you consider using crypto.getRandomValues?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Without running a test i can't say for sure but I'd say its very unlikely the crypto module is fast enough. The "cryptographically strong" requirement of that module trades off speed and its pretty significant in a lot of cases. We only have need for statistical randomness (uniform distribution) here. The distinction is that it might be possible to guess which trace id is generated in some cases (through things like timing attacks), but that's fine in this case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I did try crypto: 10x slower than the original 🫠

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, apparently it's slower than your solution... this was unexpected 😄 https://jsben.ch/ZR73M

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We were actually using crypto module in the past and removed it for performance reasons https://github.com/open-telemetry/opentelemetry-js/pull/1349/changes

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just added a benchmark test.

}

function toHex(buf: Uint8Array): string {
let hex = '';
for (let i = 0; i < buf.length; i++) {
hex += HEX[buf[i]];
}
return hex;
}

export class RandomIdGenerator implements IdGenerator {
/**
* Returns a random 16-byte trace ID formatted/encoded as a 32 lowercase hex
* characters corresponding to 128 bits.
*/
generateTraceId = getIdGenerator(TRACE_ID_BYTES);
generateTraceId(): string {
randomFill(TRACE_BUFFER);
return toHex(TRACE_BUFFER);
}

/**
* Returns a random 8-byte span ID formatted/encoded as a 16 lowercase hex
* characters corresponding to 64 bits.
*/
generateSpanId = getIdGenerator(SPAN_ID_BYTES);
}

const SHARED_CHAR_CODES_ARRAY = Array(32);
function getIdGenerator(bytes: number): () => string {
return function generateId() {
for (let i = 0; i < bytes * 2; i++) {
SHARED_CHAR_CODES_ARRAY[i] = Math.floor(Math.random() * 16) + 48;
// valid hex characters in the range 48-57 and 97-102
if (SHARED_CHAR_CODES_ARRAY[i] >= 58) {
SHARED_CHAR_CODES_ARRAY[i] += 39;
}
}
return String.fromCharCode.apply(
null,
SHARED_CHAR_CODES_ARRAY.slice(0, bytes * 2)
);
};
generateSpanId(): string {
randomFill(SPAN_BUFFER);
return toHex(SPAN_BUFFER);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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.
*/

import * as Benchmark from 'benchmark';
import { RandomIdGenerator } from '../../src/platform';

const idGenerator = new RandomIdGenerator();

describe('RandomIdGenerator benchmark (browser)', function () {
this.timeout(60000);

it('generateTraceId (browser)', done => {
const suite = new Benchmark.Suite();
suite
.add('generateTraceId (browser)', () => idGenerator.generateTraceId())
.on('cycle', (event: Benchmark.Event) =>
console.log(String(event.target))
)
.on('complete', () => done())
.run({ async: true });
});

it('generateSpanId (browser)', done => {
const suite = new Benchmark.Suite();
suite
.add('generateSpanId (browser)', () => idGenerator.generateSpanId())
.on('cycle', (event: Benchmark.Event) =>
console.log(String(event.target))
)
.on('complete', () => done())
.run({ async: true });
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.
*/

import * as Benchmark from 'benchmark';
import { BasicTracerProvider } from '../../src';

const tracerProvider = new BasicTracerProvider();
const tracer = tracerProvider.getTracer('test');

describe('Span benchmark (browser)', function () {
this.timeout(60000);

it('create spans (10 attributes) (browser)', done => {
const suite = new Benchmark.Suite();
suite
.add('create spans (10 attributes) (browser)', () => {
const span = tracer.startSpan('span');
span.setAttribute('aaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('bbbbbbbbbbbbbbbbbbbb', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('cccccccccccccccccccc', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('dddddddddddddddddddd', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('eeeeeeeeeeeeeeeeeeee', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('ffffffffffffffffffff', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('gggggggggggggggggggg', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('hhhhhhhhhhhhhhhhhhhh', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('iiiiiiiiiiiiiiiiiiii', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('jjjjjjjjjjjjjjjjjjjj', 'aaaaaaaaaaaaaaaaaaaa');
span.end();
})
.on('cycle', (event: Benchmark.Event) =>
console.log(String(event.target))
)
.on('complete', () => done())
.run({ async: true });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,38 @@
* limitations under the License.
*/

const Benchmark = require('benchmark');
const { BasicTracerProvider } = require('../../../build/src');
import * as Benchmark from 'benchmark';
import {
BasicTracerProvider,
BatchSpanProcessor,
SpanExporter,
ReadableSpan,
} from '../../../src';
import { ExportResultCode, ExportResult } from '@opentelemetry/core';

const tracerProvider = new BasicTracerProvider();
const tracer = tracerProvider.getTracer('test')
class NoopExporter implements SpanExporter {
export(
spans: ReadableSpan[],
resultCallback: (result: ExportResult) => void
): void {
setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0);
}

const suite = new Benchmark.Suite();
shutdown(): Promise<void> {
return this.forceFlush();
}

suite.on('cycle', event => {
console.log(String(event.target));
forceFlush(): Promise<void> {
return Promise.resolve();
}
}

const tracerProvider = new BasicTracerProvider({
spanProcessors: [new BatchSpanProcessor(new NoopExporter())],
});
const tracer = tracerProvider.getTracer('test');

suite.add('create spans (10 attributes)', function() {
function createSpan(): void {
const span = tracer.startSpan('span');
span.setAttribute('aaaaaaaaaaaaaaaaaaaa', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('bbbbbbbbbbbbbbbbbbbb', 'aaaaaaaaaaaaaaaaaaaa');
Expand All @@ -39,6 +58,21 @@ suite.add('create spans (10 attributes)', function() {
span.setAttribute('iiiiiiiiiiiiiiiiiiii', 'aaaaaaaaaaaaaaaaaaaa');
span.setAttribute('jjjjjjjjjjjjjjjjjjjj', 'aaaaaaaaaaaaaaaaaaaa');
span.end();
});
}

suite.run();
describe('BatchSpanProcessor benchmark (browser)', function () {
this.timeout(60000);

it('process span (browser)', done => {
const suite = new Benchmark.Suite();
suite
.add('BatchSpanProcessor process span (browser)', () => {
createSpan();
})
.on('cycle', (event: Benchmark.Event) =>
console.log(String(event.target))
)
.on('complete', () => done())
.run({ async: true });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/
import * as assert from 'assert';
import { RandomIdGenerator } from '../../../src/platform';
import { RandomIdGenerator } from '../../src/platform';

const idGenerator = new RandomIdGenerator();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {
SamplingDecision,
SamplingResult,
TraceIdRatioBasedSampler,
} from '../../src';
import { assertAssignable } from './util';
} from '../../../src';
import { assertAssignable } from '../util';

describe('Sampler', () => {
const samplers = [
Expand Down
Loading