Skip to content

Commit 40bcc3d

Browse files
authored
fix(core): Improve uuid performance (#17938)
I did some benchmarking: - #15862 (comment) ``` ┌─────────┬─────────────────────────────────────────┬──────────────────┬──────────────────┬────────────────────────┬────────────────────────┬─────────┐ │ (index) │ Task name │ Latency avg (ns) │ Latency med (ns) │ Throughput avg (ops/s) │ Throughput med (ops/s) │ Samples │ ├─────────┼─────────────────────────────────────────┼──────────────────┼──────────────────┼────────────────────────┼────────────────────────┼─────────┤ │ 0 │ 'crypto.randomUUID()' │ '269.64 ± 0.24%' │ '250.00 ± 0.00' │ '3879372 ± 0.01%' │ '4000000 ± 0' │ 3708647 │ │ 1 │ 'crypto.randomUUID().replace(/-/g, "")' │ '445.09 ± 6.08%' │ '417.00 ± 0.00' │ '2377301 ± 0.01%' │ '2398082 ± 0' │ 2246729 │ │ 2 │ 'crypto.getRandomValues()' │ '32130 ± 9.11%' │ '28083 ± 834.00' │ '35202 ± 0.07%' │ '35609 ± 1076' │ 31124 │ │ 3 │ 'Math.random()' │ '1929.0 ± 1.02%' │ '1916.0 ± 42.00' │ '525124 ± 0.01%' │ '521921 ± 11413' │ 518396 │ │ 4 │ '@lukeed/uuid' │ '273.79 ± 0.07%' │ '250.00 ± 0.00' │ '3770742 ± 0.01%' │ '4000000 ± 0' │ 3652395 │ │ 5 │ '@lukeed/uuid (custom no hyphens)' │ '262.20 ± 5.68%' │ '250.00 ± 0.00' │ '4089440 ± 0.01%' │ '4000000 ± 0' │ 3813889 │ └─────────┴─────────────────────────────────────────┴──────────────────┴──────────────────┴────────────────────────┴────────────────────────┴─────────┘ ``` I found that in Node.js at least, getting a single byte via `crypto.getRandomValues()` is 10x slower than the `Math.random()` version so we should drop `getRandomValues` usage entirely. I also found that for the `Math.random()` fallback code, we generated the base starting string (`10000000100040008000100000000000`) on every call to `uuid4()`. If we cache this value we get a ~20% improvement in this path. In the browser `crypto.randomUUID()` is only available in secure contexts so our fallback code should have good performance too!
1 parent 75f68c7 commit 40bcc3d

File tree

3 files changed

+30
-56
lines changed
  • dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling
  • packages/core

3 files changed

+30
-56
lines changed

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/startNewTraceSampling/init.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,15 @@ window.Sentry = Sentry;
55
// Force this so that the initial sampleRand is consistent
66
Math.random = () => 0.45;
77

8+
// Polyfill crypto.randomUUID
9+
crypto.randomUUID = function randomUUID() {
10+
return ([1e7] + 1e3 + 4e3 + 8e3 + 1e11).replace(
11+
/[018]/g,
12+
// eslint-disable-next-line no-bitwise
13+
c => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16),
14+
);
15+
};
16+
817
Sentry.init({
918
dsn: 'https://[email protected]/1337',
1019
integrations: [Sentry.browserTracingIntegration()],

packages/core/src/utils/misc.ts

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { snipLine } from './string';
77
import { GLOBAL_OBJ } from './worldwide';
88

99
interface CryptoInternal {
10-
getRandomValues(array: Uint8Array): Uint8Array;
1110
randomUUID?(): string;
1211
}
1312

@@ -22,37 +21,34 @@ function getCrypto(): CryptoInternal | undefined {
2221
return gbl.crypto || gbl.msCrypto;
2322
}
2423

24+
let emptyUuid: string | undefined;
25+
26+
function getRandomByte(): number {
27+
return Math.random() * 16;
28+
}
29+
2530
/**
2631
* UUID4 generator
2732
* @param crypto Object that provides the crypto API.
2833
* @returns string Generated UUID4.
2934
*/
3035
export function uuid4(crypto = getCrypto()): string {
31-
let getRandomByte = (): number => Math.random() * 16;
3236
try {
3337
if (crypto?.randomUUID) {
3438
return crypto.randomUUID().replace(/-/g, '');
3539
}
36-
if (crypto?.getRandomValues) {
37-
getRandomByte = () => {
38-
// crypto.getRandomValues might return undefined instead of the typed array
39-
// in old Chromium versions (e.g. 23.0.1235.0 (151422))
40-
// However, `typedArray` is still filled in-place.
41-
// @see https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues#typedarray
42-
const typedArray = new Uint8Array(1);
43-
crypto.getRandomValues(typedArray);
44-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
45-
return typedArray[0]!;
46-
};
47-
}
4840
} catch {
4941
// some runtimes can crash invoking crypto
5042
// https://github.com/getsentry/sentry-javascript/issues/8935
5143
}
5244

53-
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
54-
// Concatenating the following numbers as strings results in '10000000100040008000100000000000'
55-
return (([1e7] as unknown as string) + 1e3 + 4e3 + 8e3 + 1e11).replace(/[018]/g, c =>
45+
if (!emptyUuid) {
46+
// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
47+
// Concatenating the following numbers as strings results in '10000000100040008000100000000000'
48+
emptyUuid = ([1e7] as unknown as string) + 1e3 + 4e3 + 8e3 + 1e11;
49+
}
50+
51+
return emptyUuid.replace(/[018]/g, c =>
5652
// eslint-disable-next-line no-bitwise
5753
((c as unknown as number) ^ ((getRandomByte() & 15) >> ((c as unknown as number) / 4))).toString(16),
5854
);

packages/core/test/lib/utils/misc.test.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -292,36 +292,29 @@ describe('checkOrSetAlreadyCaught()', () => {
292292

293293
describe('uuid4 generation', () => {
294294
const uuid4Regex = /^[0-9A-F]{12}[4][0-9A-F]{3}[89AB][0-9A-F]{15}$/i;
295-
it('returns valid uuid v4 ids via Math.random', () => {
295+
it('returns valid and unique uuid v4 ids via Math.random', () => {
296+
const uuids = new Set<string>();
296297
for (let index = 0; index < 1_000; index++) {
297-
expect(uuid4()).toMatch(uuid4Regex);
298-
}
299-
});
300-
301-
it('returns valid uuid v4 ids via crypto.getRandomValues', () => {
302-
// eslint-disable-next-line @typescript-eslint/no-var-requires
303-
const cryptoMod = require('crypto');
304-
305-
const crypto = { getRandomValues: cryptoMod.getRandomValues };
306-
307-
for (let index = 0; index < 1_000; index++) {
308-
expect(uuid4(crypto)).toMatch(uuid4Regex);
298+
const id = uuid4();
299+
expect(id).toMatch(uuid4Regex);
300+
uuids.add(id);
309301
}
302+
expect(uuids.size).toBe(1_000);
310303
});
311304

312305
it('returns valid uuid v4 ids via crypto.randomUUID', () => {
313306
// eslint-disable-next-line @typescript-eslint/no-var-requires
314307
const cryptoMod = require('crypto');
315308

316-
const crypto = { getRandomValues: cryptoMod.getRandomValues, randomUUID: cryptoMod.randomUUID };
309+
const crypto = { randomUUID: cryptoMod.randomUUID };
317310

318311
for (let index = 0; index < 1_000; index++) {
319312
expect(uuid4(crypto)).toMatch(uuid4Regex);
320313
}
321314
});
322315

323316
it("return valid uuid v4 even if crypto doesn't exists", () => {
324-
const crypto = { getRandomValues: undefined, randomUUID: undefined };
317+
const crypto = { randomUUID: undefined };
325318

326319
for (let index = 0; index < 1_000; index++) {
327320
expect(uuid4(crypto)).toMatch(uuid4Regex);
@@ -330,9 +323,6 @@ describe('uuid4 generation', () => {
330323

331324
it('return valid uuid v4 even if crypto invoked causes an error', () => {
332325
const crypto = {
333-
getRandomValues: () => {
334-
throw new Error('yo');
335-
},
336326
randomUUID: () => {
337327
throw new Error('yo');
338328
},
@@ -342,25 +332,4 @@ describe('uuid4 generation', () => {
342332
expect(uuid4(crypto)).toMatch(uuid4Regex);
343333
}
344334
});
345-
346-
// Corner case related to crypto.getRandomValues being only
347-
// semi-implemented (e.g. Chromium 23.0.1235.0 (151422))
348-
it('returns valid uuid v4 even if crypto.getRandomValues does not return a typed array', () => {
349-
// eslint-disable-next-line @typescript-eslint/no-var-requires
350-
const cryptoMod = require('crypto');
351-
352-
const getRandomValues = (typedArray: Uint8Array) => {
353-
if (cryptoMod.getRandomValues) {
354-
cryptoMod.getRandomValues(typedArray);
355-
}
356-
};
357-
358-
const crypto = { getRandomValues };
359-
360-
for (let index = 0; index < 1_000; index++) {
361-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
362-
// @ts-ignore - we are testing a corner case
363-
expect(uuid4(crypto)).toMatch(uuid4Regex);
364-
}
365-
});
366335
});

0 commit comments

Comments
 (0)