Skip to content

Commit 35425f7

Browse files
authored
bench(dsm): add Data Streams Monitoring pathway benchmark (#8202)
DSM observes every traced Kafka, SQS, SNS, Kinesis, Pub/Sub, and AMQP message when enabled; the per-message hot path has no sirun coverage. The bench drives the real `DataStreamsProcessor` with five variants: * `produce` / `consume` -- the steady-state cache-hit path. * `produce-with-message-size` -- walks `getMessageSize` per iteration to exercise the size-accounting helpers. * `produce-manual-checkpoint` -- appends `manual_checkpoint:true` the way the public `DataStreamsCheckpointer` always sets it. * `produce-high-cardinality` -- 200 unique edge-tag combos modelling a 20-topic x 10-partition customer.
1 parent b47fabe commit 35425f7

4 files changed

Lines changed: 175 additions & 0 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,7 @@
153153
/packages/dd-trace/test/plugins/util/llm.spec.js @DataDog/ml-observability
154154

155155
# Data Streams Monitoring
156+
/benchmark/sirun/datastreams/ @DataDog/data-streams-monitoring
156157
/packages/dd-trace/src/datastreams/ @DataDog/data-streams-monitoring
157158
/packages/dd-trace/test/datastreams/ @DataDog/data-streams-monitoring
158159
/packages/**/dsm.spec.js @DataDog/data-streams-monitoring
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
This benchmark measures the per-message DSM pathway hot path that fires on every
2+
traced Kafka, SQS, SNS, Kinesis, Pub/Sub, and AMQP message when Data Streams
3+
Monitoring is enabled. Each iteration covers `setCheckpoint` (sort edge tags,
4+
build the LRU cache key, sha-hash on miss, accumulate the sketches in the
5+
bucket), the pathway codec, and the size accounting.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
'use strict'
2+
3+
const assert = require('node:assert/strict')
4+
5+
// `DataStreamsProcessor` registers a `beforeExit` handler on the dd-trace global,
6+
// which the tracer normally provides at init. Stub the minimal shape so the bench
7+
// exercises the processor without a full tracer.
8+
globalThis[Symbol.for('dd-trace')] ??= { beforeExitHandlers: new Set() }
9+
10+
const { DataStreamsProcessor } = require('../../../packages/dd-trace/src/datastreams/processor')
11+
const { DsmPathwayCodec } = require('../../../packages/dd-trace/src/datastreams/pathway')
12+
const { getMessageSize } = require('../../../packages/dd-trace/src/datastreams/size')
13+
14+
const { VARIANT } = process.env
15+
16+
const ITERATIONS = 1_200_000
17+
18+
const processor = new DataStreamsProcessor({
19+
dsmEnabled: true,
20+
service: 'bench-svc',
21+
env: 'bench-env',
22+
flushInterval: 2_147_483_647,
23+
})
24+
processor.writer.flush = () => {}
25+
clearInterval(processor.timer)
26+
27+
const span = { setTag () {} }
28+
29+
const PRODUCER_TAGS = [
30+
['direction:out', 'topic:orders', 'type:kafka'],
31+
['direction:out', 'topic:payments', 'type:kafka'],
32+
['direction:out', 'topic:notifications', 'type:kafka'],
33+
['direction:out', 'topic:audit', 'type:kafka'],
34+
['direction:out', 'topic:metrics', 'type:kafka'],
35+
]
36+
37+
const CONSUMER_TAGS = [
38+
['direction:in', 'topic:orders', 'type:kafka', 'group:fraud-svc'],
39+
['direction:in', 'topic:payments', 'type:kafka', 'group:ledger-svc'],
40+
['direction:in', 'topic:notifications', 'type:kafka', 'group:email-svc'],
41+
['direction:in', 'topic:audit', 'type:kafka', 'group:storage-svc'],
42+
['direction:in', 'topic:metrics', 'type:kafka', 'group:dashboard-svc'],
43+
]
44+
45+
// `manual_checkpoint:true` is what `DataStreamsCheckpointer.setProduceCheckpoint` /
46+
// `setConsumeCheckpoint` always set on the public manual-DSM API.
47+
const MANUAL_PRODUCER_TAGS = PRODUCER_TAGS.map(tags => [...tags, 'manual_checkpoint:true'])
48+
49+
// Models a realistic mid-fanout customer (20 topics x 10 partitions per service); 200
50+
// combos fit inside the LRU's 500-entry ceiling so steady-state every call is a hit.
51+
const HIGH_CARDINALITY_PRODUCER_TAGS = []
52+
for (let topicIndex = 0; topicIndex < 20; topicIndex++) {
53+
for (let partitionIndex = 0; partitionIndex < 10; partitionIndex++) {
54+
HIGH_CARDINALITY_PRODUCER_TAGS.push([
55+
'direction:out',
56+
`topic:orders-${topicIndex}`,
57+
`partition:${partitionIndex}`,
58+
'type:kafka',
59+
])
60+
}
61+
}
62+
63+
const MESSAGE = {
64+
key: 'order-1234567890',
65+
value: JSON.stringify({
66+
id: 'order-1234567890',
67+
customerId: 'cust-987654321',
68+
items: 5,
69+
total: 1234.56,
70+
currency: 'USD',
71+
placedAt: '2026-04-30T13:45:00Z',
72+
}),
73+
headers: {
74+
'x-traceparent': '00-1234567890abcdef1234567890abcdef-1234567890abcdef-01',
75+
'x-request-id': 'req-deadbeefcafef00d',
76+
'x-tenant-id': 'tenant-a1b2c3d4e5f6',
77+
},
78+
}
79+
const MESSAGE_SIZE = getMessageSize(MESSAGE)
80+
81+
// Pre-warmed parent contexts so the LRU pathway cache reaches a realistic steady state.
82+
const PARENT_CTXS = []
83+
for (let parentIndex = 0; parentIndex < 10; parentIndex++) {
84+
PARENT_CTXS.push(processor.setCheckpoint(['direction:in', 'topic:warmup', `idx:${parentIndex}`], span, null, 0))
85+
}
86+
87+
// Carriers cycle through the consume loop so the parent hash varies as it would when
88+
// consuming from a producer that fans out across topics.
89+
const CONSUME_CARRIERS = []
90+
for (let carrierIndex = 0; carrierIndex < 50; carrierIndex++) {
91+
const ctx = processor.setCheckpoint(
92+
PRODUCER_TAGS[carrierIndex % PRODUCER_TAGS.length],
93+
span,
94+
PARENT_CTXS[carrierIndex % PARENT_CTXS.length],
95+
MESSAGE_SIZE
96+
)
97+
const carrier = {}
98+
DsmPathwayCodec.encode(ctx, carrier)
99+
CONSUME_CARRIERS.push(carrier)
100+
}
101+
102+
// Pre-flight: confirm checkpoint + codec actually populate state; catches a silent
103+
// breakage where the processor stayed disabled or the codec wrote no header.
104+
assert.ok(processor.buckets.size > 0, 'no DSM bucket created')
105+
assert.ok(CONSUME_CARRIERS[0]['dd-pathway-ctx-base64'], 'codec did not inject pathway header')
106+
107+
if (VARIANT === 'consume') {
108+
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
109+
const carrier = CONSUME_CARRIERS[iteration % CONSUME_CARRIERS.length]
110+
const ctx = DsmPathwayCodec.decode(carrier)
111+
processor.setCheckpoint(CONSUMER_TAGS[iteration % CONSUMER_TAGS.length], span, ctx, MESSAGE_SIZE)
112+
}
113+
} else if (VARIANT === 'produce-with-message-size') {
114+
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
115+
const payloadSize = getMessageSize(MESSAGE)
116+
const ctx = processor.setCheckpoint(
117+
PRODUCER_TAGS[iteration % PRODUCER_TAGS.length],
118+
span,
119+
PARENT_CTXS[iteration % PARENT_CTXS.length],
120+
payloadSize
121+
)
122+
DsmPathwayCodec.encode(ctx, {})
123+
}
124+
} else if (VARIANT === 'produce-manual-checkpoint') {
125+
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
126+
const ctx = processor.setCheckpoint(
127+
MANUAL_PRODUCER_TAGS[iteration % MANUAL_PRODUCER_TAGS.length],
128+
span,
129+
PARENT_CTXS[iteration % PARENT_CTXS.length],
130+
MESSAGE_SIZE
131+
)
132+
DsmPathwayCodec.encode(ctx, {})
133+
}
134+
} else if (VARIANT === 'produce-high-cardinality') {
135+
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
136+
const ctx = processor.setCheckpoint(
137+
HIGH_CARDINALITY_PRODUCER_TAGS[iteration % HIGH_CARDINALITY_PRODUCER_TAGS.length],
138+
span,
139+
PARENT_CTXS[iteration % PARENT_CTXS.length],
140+
MESSAGE_SIZE
141+
)
142+
DsmPathwayCodec.encode(ctx, {})
143+
}
144+
} else {
145+
for (let iteration = 0; iteration < ITERATIONS; iteration++) {
146+
const ctx = processor.setCheckpoint(
147+
PRODUCER_TAGS[iteration % PRODUCER_TAGS.length],
148+
span,
149+
PARENT_CTXS[iteration % PARENT_CTXS.length],
150+
MESSAGE_SIZE
151+
)
152+
DsmPathwayCodec.encode(ctx, {})
153+
}
154+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "datastreams",
3+
"run": "node index.js",
4+
"run_with_affinity": "bash -c \"taskset -c $CPU_AFFINITY node index.js\"",
5+
"cachegrind": false,
6+
"iterations": 30,
7+
"instructions": true,
8+
"variants": {
9+
"produce": { "env": { "VARIANT": "produce" } },
10+
"consume": { "env": { "VARIANT": "consume" } },
11+
"produce-with-message-size": { "baseline": "produce", "env": { "VARIANT": "produce-with-message-size" } },
12+
"produce-manual-checkpoint": { "baseline": "produce", "env": { "VARIANT": "produce-manual-checkpoint" } },
13+
"produce-high-cardinality": { "baseline": "produce", "env": { "VARIANT": "produce-high-cardinality" } }
14+
}
15+
}

0 commit comments

Comments
 (0)