Skip to content
Closed
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
29 changes: 25 additions & 4 deletions benchmarks/proto/nested.proto
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,34 @@ package bench.v1;

// nested.proto intentionally mirrors the shape of the OTLP trace export
// request (opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest),
// simplified to the fields most traffic goes through. This is a realistic
// hotpath workload — see open-telemetry/opentelemetry-js#6221 for the
// production incident that motivates measuring this shape.
// now extended to exercise the full AnyValue oneof and a map<string,string>
// so that the fast-path encoder benchmark matches a realistic OTel payload.
// See open-telemetry/opentelemetry-js#6221 for the production incident that
// motivates measuring this shape.

// AnyValue is the oneof-typed value carried by every KeyValue attribute in
// OTLP. We include the common leaf types; array/kvlist are intentionally
// omitted — they nest through KeyValue again and would dominate the
// benchmark on a distribution we don't observe in practice.
message AnyValue {
oneof value {
string string_value = 1;
bool bool_value = 2;
int64 int_value = 3;
double double_value = 4;
bytes bytes_value = 7;
}
}

message KeyValue {
string key = 1;
string string_value = 2;
AnyValue value = 2;
}

message InstrumentationScope {
string name = 1;
string version = 2;
repeated KeyValue attributes = 3;
}

message Span {
Expand All @@ -47,6 +63,11 @@ message ScopeSpans {

message Resource {
repeated KeyValue attributes = 1;
// `labels` exercises map<string, string> encoding on the fast path.
// Real OTLP payloads don't carry a labels map, but maps show up in
// many other ConnectRPC schemas and we want the benchmark to reflect
// that fast-path cost too.
map<string, string> labels = 2;
}

message ResourceSpans {
Expand Down
52 changes: 48 additions & 4 deletions benchmarks/src/bench-comparison-protobufjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,20 @@ function buildOtelLikePayload(): Record<string, unknown> {
for (let i = 0; i < SPAN_COUNT; i++) {
const attributes: unknown[] = [];
for (let j = 0; j < 10; j++) {
attributes.push({ key: `k${j}`, stringValue: `v${i}-${j}` });
// AnyValue oneof: mostly string, some int, some bool — matches the
// distribution the fast-path benchmark feeds into the reflective
// encoder via the same fixture.
let anyValue: Record<string, unknown>;
if (j === 2 || j === 5) {
anyValue = { value: { case: "intValue", value: BigInt(200 + j) } };
} else if (j === 8) {
anyValue = { value: { case: "boolValue", value: (i + j) % 7 === 0 } };
} else {
anyValue = {
value: { case: "stringValue", value: `v${i}-${j}` },
};
}
attributes.push({ key: `k${j}`, value: anyValue });
}
spans.push({
traceId: new Uint8Array(16),
Expand All @@ -76,7 +89,14 @@ function buildOtelLikePayload(): Record<string, unknown> {
return {
resourceSpans: [
{
resource: { attributes: [] },
resource: {
attributes: [],
labels: {
env: "production",
region: "us-east-1",
cluster: "bench-cluster",
},
},
scopeSpans: [
{
scope: { name: "@example/tracer", version: "1.0.0" },
Expand All @@ -91,15 +111,39 @@ function buildOtelLikePayload(): Record<string, unknown> {
// protobufjs uses Long for 64-bit fields by default. We generate with
// --force-long to get consistent typing; here we still pass BigInt-like
// plain numbers via strings so both paths encode the same timestamp.
// Prepare a payload variant with string timestamps for pbjs, so the
// write paths are comparable (no JSON conversion cost inside the hot loop).
// Prepare a payload variant for pbjs with:
// - string timestamps (no JSON conversion cost inside the hot loop)
// - AnyValue oneof represented as a plain field rather than the
// `{ case, value }` ADT protobuf-es uses, because protobufjs stores
// oneof members directly on the parent message.
function buildOtelLikePayloadForPbjs(): Record<string, unknown> {
const base = buildOtelLikePayload();
// biome-ignore lint/suspicious/noExplicitAny: in-place shape munging
const resourceSpans = (base.resourceSpans as any[])[0];
for (const span of resourceSpans.scopeSpans[0].spans) {
span.startTimeUnixNano = "1700000000000000000";
span.endTimeUnixNano = "1700000000000001000";
// biome-ignore lint/suspicious/noExplicitAny: in-place shape munging
for (const attr of span.attributes as any[]) {
const adt = attr.value?.value as
| { case: string; value: unknown }
| undefined;
if (adt) {
const pbjsKey =
adt.case === "intValue"
? "intValue"
: adt.case === "boolValue"
? "boolValue"
: "stringValue";
// protobufjs also accepts int64 as string without Long.
attr.value = {
[pbjsKey]:
adt.case === "intValue"
? (adt.value as bigint).toString()
: adt.value,
};
}
}
}
return base;
}
Expand Down
9 changes: 7 additions & 2 deletions benchmarks/src/bench-create-toBinary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Bench } from "tinybench";
import { create, toBinary, toBinaryFast } from "@bufbuild/protobuf";
import { SimpleMessageSchema } from "./gen/small_pb.js";
import {
AnyValueSchema,
ExportTraceRequestSchema,
ResourceSpansSchema,
ScopeSpansSchema,
Expand Down Expand Up @@ -62,7 +63,9 @@ export async function runCreateToBinaryBench() {
attrs.push(
create(KeyValueSchema, {
key: `k${j}`,
stringValue: `v${i}-${j}`,
value: create(AnyValueSchema, {
value: { case: "stringValue", value: `v${i}-${j}` },
}),
}),
);
}
Expand Down Expand Up @@ -104,7 +107,9 @@ export async function runCreateToBinaryBench() {
attrs.push(
create(KeyValueSchema, {
key: `k${j}`,
stringValue: `v${i}-${j}`,
value: create(AnyValueSchema, {
value: { case: "stringValue", value: `v${i}-${j}` },
}),
}),
);
}
Expand Down
5 changes: 4 additions & 1 deletion benchmarks/src/bench-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Bench } from "tinybench";
import { create } from "@bufbuild/protobuf";
import { SimpleMessageSchema } from "./gen/small_pb.js";
import {
AnyValueSchema,
ExportTraceRequestSchema,
ResourceSpansSchema,
ScopeSpansSchema,
Expand Down Expand Up @@ -56,7 +57,9 @@ export async function runCreateBench() {
attrs.push(
create(KeyValueSchema, {
key: `k${j}`,
stringValue: `v${i}-${j}`,
value: create(AnyValueSchema, {
value: { case: "stringValue", value: `v${i}-${j}` },
}),
}),
);
}
Expand Down
43 changes: 41 additions & 2 deletions benchmarks/src/bench-memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,19 @@ function buildInit(): Record<string, unknown> {
for (let i = 0; i < SPAN_COUNT; i++) {
const attributes: unknown[] = [];
for (let j = 0; j < 10; j++) {
attributes.push({ key: `k${j}`, stringValue: `v${i}-${j}` });
// Mirror the distribution used in bench-comparison-protobufjs so the
// memory numbers reflect the same workload as the throughput runs.
let anyValue: Record<string, unknown>;
if (j === 2 || j === 5) {
anyValue = { value: { case: "intValue", value: BigInt(200 + j) } };
} else if (j === 8) {
anyValue = { value: { case: "boolValue", value: (i + j) % 7 === 0 } };
} else {
anyValue = {
value: { case: "stringValue", value: `v${i}-${j}` },
};
}
attributes.push({ key: `k${j}`, value: anyValue });
}
spans.push({
traceId: new Uint8Array(16),
Expand All @@ -61,7 +73,14 @@ function buildInit(): Record<string, unknown> {
return {
resourceSpans: [
{
resource: { attributes: [] },
resource: {
attributes: [],
labels: {
env: "production",
region: "us-east-1",
cluster: "bench-cluster",
},
},
scopeSpans: [
{
scope: { name: "@example/tracer", version: "1.0.0" },
Expand All @@ -80,6 +99,26 @@ function buildInitForPbjs(): Record<string, unknown> {
for (const span of resourceSpans.scopeSpans[0].spans) {
span.startTimeUnixNano = "1700000000000000000";
span.endTimeUnixNano = "1700000000000001000";
// biome-ignore lint/suspicious/noExplicitAny: in-place shape munging
for (const attr of span.attributes as any[]) {
const adt = attr.value?.value as
| { case: string; value: unknown }
| undefined;
if (adt) {
const pbjsKey =
adt.case === "intValue"
? "intValue"
: adt.case === "boolValue"
? "boolValue"
: "stringValue";
attr.value = {
[pbjsKey]:
adt.case === "intValue"
? (adt.value as bigint).toString()
: adt.value,
};
}
}
}
return base;
}
Expand Down
Loading
Loading