Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf: speed up otel trace generation #26854

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
207 changes: 127 additions & 80 deletions runtime/js/telemetry.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { core, primordials } from "ext:core/mod.js";
import {
op_crypto_get_random_values,
op_otel_log,
op_otel_span_attribute,
op_otel_span_attribute2,
Expand All @@ -15,13 +16,14 @@ import { performance } from "ext:deno_web/15_performance.js";

const {
SymbolDispose,
MathRandom,
Array,
ObjectEntries,
SafeMap,
ReflectApply,
SymbolFor,
Error,
Uint8Array,
TypedArrayPrototypeSubarray,
} = primordials;
const { AsyncVariable, setAsyncContext } = core;

Expand All @@ -45,36 +47,43 @@ const hexSliceLookupTable = (function () {
return table;
})();

function generateId(bytes) {
function bytesToHex(bytes) {
let out = "";
for (let i = 0; i < bytes / 4; i += 1) {
const r32 = (MathRandom() * 2 ** 32) >>> 0;
out += hexSliceLookupTable[(r32 >> 24) & 0xff];
out += hexSliceLookupTable[(r32 >> 16) & 0xff];
out += hexSliceLookupTable[(r32 >> 8) & 0xff];
out += hexSliceLookupTable[r32 & 0xff];
for (let i = 0; i < bytes.length; i += 1) {
out += hexSliceLookupTable[bytes[i]];
}
return out;
}

function submit(span) {
if (!(span.traceFlags & TRACE_FLAG_SAMPLED)) return;
function submit(
spanId,
traceId,
traceFlags,
name,
kind,
parentSpanId,
startTime,
endTime,
status,
attributes,
) {
if (!(traceFlags & TRACE_FLAG_SAMPLED)) return;

op_otel_span_start(
span.traceId,
span.spanId,
span.parentSpanId ?? "",
span.kind,
span.name,
span.startTime,
span.endTime,
traceId,
spanId,
parentSpanId,
kind,
name,
startTime,
endTime,
);

if (span.status !== null && span.status.code !== 0) {
op_otel_span_continue(span.code, span.message ?? "");
if (status !== null && status.code !== 0) {
op_otel_span_continue(code, status.message ?? "");
}

const attributes = ObjectEntries(span.attributes);
attributes = ObjectEntries(attributes);
let i = 0;
while (i < attributes.length) {
if (i + 2 < attributes.length) {
Expand Down Expand Up @@ -112,14 +121,25 @@ function submit(span) {

const now = () => (performance.timeOrigin + performance.now()) / 1000;

const INVALID_SPAN_ID = "0000000000000000";
const INVALID_TRACE_ID = "00000000000000000000000000000000";
const INVALID_SPAN_ID = "0000000000000000";
const NO_ASYNC_CONTEXT = {};
let otelLog;

class Span {
traceId;
spanId;
parentSpanId;
/** @type {string | Uint8Array} */
#traceId;
/** @type {Uint8Array} */
#spanId;
/** @type {string | Uint8Array | null} */
#parentSpanId = null;
/** @type {string} */
#traceIdString;
/** @type {string} */
#spanIdString;
/** @type {string | null} */
#parentSpanIdString = null;

kind;
name;
startTime;
Expand All @@ -131,40 +151,59 @@ class Span {
enabled = TRACING_ENABLED;
#asyncContext = NO_ASYNC_CONTEXT;

static {
otelLog = function otelLog(message, level) {
let traceId = null;
let spanId = null;
let traceFlags = 0;
const span = Span.current();
if (span) {
// The lint is wrong, we can not use anything but `in` here because this
// is a private field.
// deno-lint-ignore prefer-primordials
if (#traceId in span) {
traceId = span.#traceId;
spanId = span.#spanId;
traceFlags = span.traceFlags;
} else {
const context = span.spanContext();
traceId = context.traceId;
spanId = context.spanId;
traceFlags = context.traceFlags;
}
}
return op_otel_log(message, level, traceId, spanId, traceFlags);
};
}

constructor(name, kind = "internal") {
if (!this.enabled) {
this.traceId = INVALID_TRACE_ID;
this.spanId = INVALID_SPAN_ID;
this.parentSpanId = INVALID_SPAN_ID;
this.#traceId = INVALID_TRACE_ID;
this.#spanId = INVALID_SPAN_ID;
return;
}

this.startTime = now();

this.spanId = generateId(SPAN_ID_BYTES);

let traceId;
let parentSpanId;
const parent = Span.current();
if (parent) {
if (parent.spanId !== undefined) {
parentSpanId = parent.spanId;
traceId = parent.traceId;
// The lint is wrong, we can not use anything but `in` here because this
// is a private field.
// deno-lint-ignore prefer-primordials
if (#traceId in parent) {
this.#traceId = parent.#traceId;
this.#parentSpanId = parent.#spanId;
} else {
const context = parent.spanContext();
parentSpanId = context.spanId;
traceId = context.traceId;
this.#traceId = parent.traceId;
this.#parentSpanId = parent.spanId;
}
}
if (
traceId && traceId !== INVALID_TRACE_ID && parentSpanId &&
parentSpanId !== INVALID_SPAN_ID
) {
this.traceId = traceId;
this.parentSpanId = parentSpanId;
this.#spanId = new Uint8Array(SPAN_ID_BYTES);
op_crypto_get_random_values(this.#spanId);
} else {
this.traceId = generateId(TRACE_ID_BYTES);
this.parentSpanId = INVALID_SPAN_ID;
const buffer = new Uint8Array(TRACE_ID_BYTES + SPAN_ID_BYTES);
op_crypto_get_random_values(buffer);
this.#traceId = TypedArrayPrototypeSubarray(buffer, 0, TRACE_ID_BYTES);
this.#spanId = TypedArrayPrototypeSubarray(buffer, TRACE_ID_BYTES);
}

this.name = name;
Expand Down Expand Up @@ -192,12 +231,29 @@ class Span {
this.enter();
}

get traceId() {
Copy link
Member

Choose a reason for hiding this comment

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

these properties don't exist in otel. if buffers are fastest we could just do this.traceId = buffer without using a private. if/when we do perf work in upstream otel, we could do similar, with strings only being created in places where the api expects it.

Copy link
Member Author

Choose a reason for hiding this comment

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

We could also remove these fields entirely then and only have the spanContext() API?

Copy link
Member

Choose a reason for hiding this comment

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

well, the original idea was to avoid needing to use spanContext, since the extra object allocation is theoretically unneeded when flushing a span.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, but if you look at how Deno.tracing.Span spans are flushed now, they don't access either spanContext or traceId (only the internal #traceId).

if (!this.#traceIdString) this.#traceIdString = bytesToHex(this.#traceId);
return this.#traceIdString;
}

get spanId() {
if (!this.#spanIdString) this.#spanIdString = bytesToHex(this.#spanId);
return this.#spanIdString;
}

get parentSpanId() {
if (!this.#parentSpanIdString && this.#parentSpanId) {
this.#parentSpanIdString = bytesToHex(this.#parentSpanId);
}
return this.#parentSpanIdString;
}

// helper function to match otel js api
spanContext() {
return {
traceId: this.traceId,
spanId: this.spanId,
traceFlags: this.traceFlags,
parentSpanId: this.parentSpanId,
};
}

Expand All @@ -222,7 +278,18 @@ class Span {
if (!this.enabled || this.endTime !== undefined) return;
this.exit();
this.endTime = now();
submit(this);
submit(
this.#spanId,
this.#traceId,
this.traceFlags,
this.name,
this.kind,
this.#parentSpanId,
this.startTime,
this.endTime,
this.status,
this.attributes,
);
}

[SymbolDispose]() {
Expand All @@ -245,18 +312,18 @@ class SpanExporter {
for (let i = 0; i < spans.length; i += 1) {
const span = spans[i];
const context = span.spanContext();
submit({
spanId: context.spanId,
traceId: context.traceId,
traceFlags: context.traceFlags,
name: span.name,
kind: span.kind,
parentSpanId: span.parentSpanId,
startTime: hrToSecs(span.startTime),
endTime: hrToSecs(span.endTime),
status: span.status,
attributes: span.attributes,
});
submit(
context.spanId,
context.traceId,
context.traceFlags,
span.name,
span.kind,
span.parentSpanId,
hrToSecs(span.startTime),
hrToSecs(span.endTime),
span.status,
span.attributes,
);
}
resultCallback({ code: 0 });
} catch (error) {
Expand Down Expand Up @@ -334,26 +401,6 @@ class ContextManager {
}
}

function otelLog(message, level) {
let traceId = "";
let spanId = "";
let traceFlags = 0;
const span = Span.current();
if (span) {
if (span.spanId !== undefined) {
spanId = span.spanId;
traceId = span.traceId;
traceFlags = span.traceFlags;
} else {
const context = span.spanContext();
spanId = context.spanId;
traceId = context.traceId;
traceFlags = context.traceFlags;
}
}
return op_otel_log(message, level, traceId, spanId, traceFlags);
}

const otelConsoleConfig = {
ignore: 0,
capture: 1,
Expand Down
Loading
Loading