总跟踪:#121
正式 macro gate 前置:#122
类型:测量/归因 issue。本 issue 允许增加 benchmark、test-only instrumentation 和 throwaway prototype,但不预设 production optimization 一定存在。
实施基线:开始工作时读取 dev 实际 HEAD,并重新核对本文列出的代码事实。
目标
把“CallContext / Telemetry 很贵”拆成三个彼此独立的问题:
A. CallContext snapshot + AsyncLocal push/restore
B. Metrics enabled path
C. Activity/Tracing propagation + recording path
每个问题必须独立得到 Go 或 No-Go。不能因为 tracing 很贵就顺手重写 CallContext,也不能因为 metrics 有 allocation 就默认 Activity 是同一个原因。
只有某一个子问题通过立项门槛后,才为那个具体机制另开 implementation issue。
当前 dev 必须先理解的事实
执行时重新核对,不能盲信 issue 文本。
CallContext
当前公开 API:
SharpLinkCallContext.Current
通过 AsyncLocal<SharpLinkCallContextSnapshot?> 暴露当前 server invocation context。
Push 会:
previous = AsyncLocal.Value
AsyncLocal.Value = snapshot
return Scope(previous)
Scope.Dispose 恢复 previous。
这意味着即使没有 interceptor,业务 service 本身也可能直接读取 SharpLinkCallContext.Current。因此:
“没有 interceptor 就不 Push AsyncLocal”不是合法的透明优化。
除非另行设计明确的 public opt-in/compatibility 变更,否则不能删除这项可观察语义。
当前 ServerConnectionState 已经对最常见场景做了一层缓存:没有 per-call deadline/metadata 时可复用 connection-level SharpLinkCallContextSnapshot。因此不要重复提出“缓存默认 context”作为新优化,先确认现状。
Metrics/Tracing
SharpLinkTelemetry 当前已有 fast guards,例如:
ActivitySource.HasListeners()
instrument.Enabled
当既没有 Activity listener、也没有 call metrics listener 时,call scope 可以直接是 default。
启用 Activity 时当前会创建 Activity 并设置 RPC tags/status/error fields。要特别区分:
ActivitySamplingResult.None
PropagationData
AllData
AllDataAndRecorded
1% recorded 不等于 1% 创建 Activity。如果未采样的 99% 返回 PropagationData,它们仍可能需要 Activity 进行 context propagation,只是不需要完整 recording data。
历史 P2 evidence 曾显示 metrics 有稳定额外 allocation,且某个 client tracing 配置中 1%/100% tracing 都非常昂贵;这些数据只能作为“值得重新测”的方向性证据。正式 Go/No-Go 必须在 #122 的新 formal recorder 下重跑。
第一性原理
1. 诊断功能不是“免费才算正确”
Metrics/Tracing 被启用时,本来就会产生额外工作。目标不是把 enabled path 变成 off path,而是消除不必要的 allocation、tag construction、duplicate work、AsyncLocal/context work。
2. Off-path 最重要的是不付 enabled-path 成本
Telemetry off 的核心要求:
没有 listener/provider -> 不创建 Activity,不构造 tags,不启动 duration measurement,不创建额外对象
如果当前已经满足,就不要为了“优化”增加新的 feature flag/service lookup。
3. Public CallContext 是语义,不是内部缓存
业务代码可以捕获/读取 SharpLinkCallContext.Current;interceptor/logger 也可能观察 context。因此禁止池化或跨调用复用可变 public invocation context。
4. 先归因,再选方案
如果某 benchmark 多 600 B/op,必须回答 allocation stack 来自:
- Activity object;
- tags/TagList;
- Meter listener path;
SharpLinkServerInvocationContext;
- metadata;
- interceptor pipeline;
- AsyncLocal/ExecutionContext;
- logging scope;
- benchmark harness。
不知道来源时不能写 production candidate。
5. Sampling 与 propagation 是不同语义
一个 trace 可能“不记录 span”但仍必须传播 TraceId/SpanId。不能为了 1% tracing 的数字,让 99% 请求失去 distributed context propagation。
6. 每个子问题单独决定
合法结果示例:
A CallContext: No-Go(成本可测但由 public AsyncLocal semantics 决定)
B Metrics: Go(发现每 call 重复 tag object allocation,可无语义变化消除)
C Tracing: Go(PropagationData path 做了 AllData-only work)
这比一个“大 telemetry refactor”更容易验证和回退。
建议分支与 commit
测量分支:
feature/telemetry-callcontext-evidence
推荐 commit:
bench: isolate call context push and snapshot costs
bench: isolate metrics and activity states
bench: add telemetry allocation attribution scenarios
perf-test: add formal telemetry macro matrix
docs: record call-context telemetry go-no-go evidence
throwaway candidate 必须单独 commit,例如:
experiment: skip all-data-only activity enrichment for propagation-only spans
如果不达门槛,revert experiment,benchmark/evidence 保留。
A. CallContext 调查
A1. 先建立纯 microbenchmark
至少保留/新增以下 cases:
NoContextWork
ReuseSnapshot_NoPush
CreateDefaultSnapshot
CreateDeadlineSnapshot
CreateMetadataSnapshot
PushCachedSnapshot
PushAndReadCurrent
PushNestedAndRestore
每个记录:
- ns/op;
- B/op;
- Gen0;
- ExecutionContext/AsyncLocal attribution(可用 profiler/trace)。
NoContextWork 必须存在,否则不知道 Push 成本相对什么。
benchmark harness 注意
不要在 benchmark method 自己创建 Task/closure 来制造异步边界,然后把它的 allocation 归到 AsyncLocal。
先测同步 push/restore;再单独测一个真实 async service continuation 场景。
A2. 真实 server invocation A/B
构造以下 server workloads:
1. no auth / no metadata / no deadline / no interceptor
2. auth only
3. metadata only
4. deadline only
5. interceptor enabled but interceptor does nothing
6. service reads SharpLinkCallContext.Current
7. service does not read Current
注意:6/7 只用于归因读取成本,不是允许 runtime 猜业务会不会读取。
正式 macro 使用 #122 formal recorder。
concurrency:
至少 TCP;如果 SharedMemory 能放大 runtime CPU 占比,再补 SharedMemory。
A3. Allocation stack
如果 default case 有 per-call object allocation,确认是不是:
SharpLinkCallContextSnapshot
SharpLinkServerInvocationContext
AsyncLocalValueChangedArgs / ExecutionContext related
interceptor context
metadata
不要只看类型名;记录 call stack。
A4. A 的 Go/No-Go 门槛
Go 只允许在以下情况
找到一个不改变 SharpLinkCallContext.Current 可观察语义的 candidate,并满足至少一个:
- default server workload CPU/op 降 >=3%;或
- 可归因 per-call allocation 明显下降,且 end-to-end throughput/P99 有稳定收益;或
- AsyncLocal/context 相关成本占目标 server workload总 CPU >=5%,candidate 可无语义变化消掉其中显著部分。
No-Go
如果主要成本就是“每个 invocation 必须建立正确 AsyncLocal scope”,且没有透明低风险替代,则明确记录:
cost is real but semantic
然后关闭 A,不发明 pooling/global static/current-thread hack。
B. Metrics 调查
B1. 固定 provider/listener
测试必须明确启用了哪些 instruments。不能写“metrics on”但不同运行注册不同 MeterListener/OpenTelemetry provider。
至少:
MetricsOff
CallCountersOnly
DurationHistogramOnly
CountersAndDuration
RepresentativeFullMetrics
client/server 分开测,再测 both enabled。
B2. 直接测 SharpLinkTelemetry scope
需要一个低噪声 microbenchmark,隔离:
StartServerCall + Complete success
StartServerCall + Complete SharpLinkException
StartClientCall + Complete
每种在 off/on 状态测 B/op 和 ns/op。
同时测 instrument Enabled guard 本身,不要把 provider export/background worker 成本和 runtime instrumentation 成本混成一个数字。
B3. Provider export 分层
至少区分:
listener consumes synchronously / no exporter
in-memory exporter
realistic exporter if repo already有标准配置
本 issue 首先优化 SharpLink instrumentation cost,不把网络 exporter 性能问题算给 SharpLink。
B4. Metrics correctness
任何 candidate 必须保持:
- calls.started/completed/failed exact;
- active call delta 最终归零;
- duration count 与完成 call 一致;
- status/error tags;
- client/server side tags;
- ResourceExhausted reason;
- retry/attempt 等现有指标定义不漂移。
要新增 deterministic metric listener tests,而不是只人工看 Grafana。
B5. Metrics Go 门槛
正式 #122 macro 下:
- telemetry off 相对完全移除 instrumentation 的 control cost目标 <=1%;
- representative metrics enabled 的 SharpLink instrumentation 增量目标 <=3%,但这个是目标,不是为了通过而改变 metric semantics;
- 如果当前 >3%,只有 profiler 能明确归因到可消除 runtime work 才 Go;
- candidate 至少稳定降低目标成本 >=20% 或使 end-to-end target workload改善 >=3%,同时字段完整。
如果主要成本来自 provider/exporter 而不是 SharpLink instrumentation:No-Go,记录边界。
C. Tracing 调查
C1. Sampling matrix 必须正确
建立一个可控制的 ActivityListener,分别返回:
None
PropagationData
AllData
AllDataAndRecorded
再建立真实采样组合:
1% AllDataAndRecorded + 99% None
1% AllDataAndRecorded + 99% PropagationData
100% AllDataAndRecorded
这两个 1% 场景必须分开,因为语义完全不同。
C2. 记录的指标
- Activity allocations/call;
- tag enrichment allocations;
- CPU/op;
- QPS;
- P99/P99.9;
Activity.Current / trace id propagation correctness;
- exporter off/on 分层。
C3. 优先验证的假设
当前值得优先验证:
PropagationData Activity 是否仍执行只对 IsAllDataRequested 有价值的完整 tag/status enrichment?
throwaway prototype 可以类似:
activity = source.StartActivity(...)
if activity is null -> default
if activity.IsAllDataRequested:
SetTag(method/contract/...)
// propagation 必需的 Activity 生命周期继续存在
但哪些 tag 在 PropagationData 下属于现有 contract,必须先用 tests/文档确认。不能看到 IsAllDataRequested 就机械包住所有代码。
另一个假设:error path 的昂贵 error tags 只在 activity 真正记录时创建;仍需保持 error/status metric semantics。
C4. 禁止的“优化”
禁止:
- sampled=false 时直接不创建 Activity,但当前 listener 要求 PropagationData;
- 使用自定义 ThreadStatic 代替 Activity.Current;
- 复用/池化 Activity;
- 让 1% trace 丢掉 W3C propagation;
- 修改 trace IDs/span parent 关系来换性能;
- 把 exporter batching 改动混进 Runtime candidate。
C5. Tracing Go 门槛
先把目标按语义分开:
None sampling
应接近 telemetry off;如果不接近,说明 guard 有问题。
1% recorded + 99% None
SharpLink tracing overhead 目标 <=5%。
1% recorded + 99% PropagationData
不要硬套 <=5%。PropagationData 本身要求创建/传播 Activity context。这里的 Go 条件是:
- profiler 证明额外 enrichment 是显著部分;
- candidate 保持 propagation;
- candidate 对该场景 end-to-end 至少有稳定 >=5% throughput/CPU 改善,或显著 allocation 减少且 P99 不恶化。
100% recorded
主要作为 correctness/upper-bound control,不要求和 off 接近。
统一的正式 macro matrix
#122 完成后至少跑:
Server
Default
ContextRead
Metadata
Deadline
Metrics
Trace_None
Trace_1pct_None
Trace_1pct_Propagation
Trace_All
Client
Default
Metrics
Trace_None
Trace_1pct_None
Trace_1pct_Propagation
Trace_All
每个至少:
固定 tiny unary workload 优先,因为 payload/serialization 太大时会掩盖 per-call diagnostics overhead。
base/head 同机交替 >=5 轮。
必须新增/强化的 correctness tests
CallContext
- Current 在 service invocation 内可见。
- invocation 后恢复 previous/null。
- nested scope 恢复正确。
- async continuation 后 Current 仍正确。
- 两个并发 call 不串 context。
- auth/default snapshot 不串 connection。
- deadline/metadata per-call 值正确。
- interceptor 与 service 看见同一个语义 context。
Metrics
- success started/completed/active delta。
- failure code/status。
- cancellation。
- ResourceExhausted reason。
- duration exactly once。
- telemetry disabled 时不产生 metric callback。
Tracing
- None -> 无 Activity。
- PropagationData -> Activity context 可传播。
- AllData/Recorded -> required tags 完整。
- parent/child relation。
- retry attempt span 不重复 logical call counters。
- error status/type。
- cancellation status。
- 1% sampler 不破坏 propagation policy。
风险
风险:把公开 context 改成 pool/reuse
业务代码、interceptor、logger、AsyncLocal continuation 可能捕获它。跨调用重用会产生极难复现的数据串扰。禁止。
风险:错误理解 ActivitySamplingResult
PropagationData 不是 None。先写行为测试,再做 tag enrichment candidate。
风险:把 exporter 成本归因给 Runtime
所有实验必须有 no-export/listener-only control。
风险:Telemetry test 使用 process-global listener 导致并行串扰
在 #83 完成前/期间按现有 global-test 约束隔离;不要为了 perf test 随意恢复 assembly-wide serial,也不要让 listener 泄漏到其他测试。
风险:candidate 只改善 microbenchmark
最终 Go 必须有 #122 formal macro evidence。
回退规则
每个 A/B/C candidate 独立 commit。
立即 revert candidate,如果出现:
SharpLinkCallContext.Current 缺失/串扰;
- trace parent/propagation 改变;
- metrics count/tag 丢失;
- error diagnostics 字段缺失;
- default/off workload稳定回退 >1%-2%;
- enabled target 的收益低于预先 Go 门槛;
- 只是把 allocation 移到 exporter/其他层。
benchmark、correctness tests、No-Go evidence 可保留。
本 issue 的完成定义
本 issue 可以在 A/B/C 全部 No-Go 时正常关闭;“没有值得做的优化”是有效结论。
目标
把“CallContext / Telemetry 很贵”拆成三个彼此独立的问题:
每个问题必须独立得到
Go或No-Go。不能因为 tracing 很贵就顺手重写 CallContext,也不能因为 metrics 有 allocation 就默认 Activity 是同一个原因。只有某一个子问题通过立项门槛后,才为那个具体机制另开 implementation issue。
当前
dev必须先理解的事实执行时重新核对,不能盲信 issue 文本。
CallContext
当前公开 API:
通过
AsyncLocal<SharpLinkCallContextSnapshot?>暴露当前 server invocation context。Push会:Scope.Dispose恢复 previous。这意味着即使没有 interceptor,业务 service 本身也可能直接读取
SharpLinkCallContext.Current。因此:除非另行设计明确的 public opt-in/compatibility 变更,否则不能删除这项可观察语义。
当前
ServerConnectionState已经对最常见场景做了一层缓存:没有 per-call deadline/metadata 时可复用 connection-levelSharpLinkCallContextSnapshot。因此不要重复提出“缓存默认 context”作为新优化,先确认现状。Metrics/Tracing
SharpLinkTelemetry当前已有 fast guards,例如:当既没有 Activity listener、也没有 call metrics listener 时,call scope 可以直接是 default。
启用 Activity 时当前会创建 Activity 并设置 RPC tags/status/error fields。要特别区分:
1% recorded不等于1% 创建 Activity。如果未采样的 99% 返回PropagationData,它们仍可能需要 Activity 进行 context propagation,只是不需要完整 recording data。历史 P2 evidence 曾显示 metrics 有稳定额外 allocation,且某个 client tracing 配置中 1%/100% tracing 都非常昂贵;这些数据只能作为“值得重新测”的方向性证据。正式 Go/No-Go 必须在 #122 的新 formal recorder 下重跑。
第一性原理
1. 诊断功能不是“免费才算正确”
Metrics/Tracing 被启用时,本来就会产生额外工作。目标不是把 enabled path 变成 off path,而是消除不必要的 allocation、tag construction、duplicate work、AsyncLocal/context work。
2. Off-path 最重要的是不付 enabled-path 成本
Telemetry off 的核心要求:
如果当前已经满足,就不要为了“优化”增加新的 feature flag/service lookup。
3. Public CallContext 是语义,不是内部缓存
业务代码可以捕获/读取
SharpLinkCallContext.Current;interceptor/logger 也可能观察 context。因此禁止池化或跨调用复用可变 public invocation context。4. 先归因,再选方案
如果某 benchmark 多 600 B/op,必须回答 allocation stack 来自:
SharpLinkServerInvocationContext;不知道来源时不能写 production candidate。
5. Sampling 与 propagation 是不同语义
一个 trace 可能“不记录 span”但仍必须传播 TraceId/SpanId。不能为了 1% tracing 的数字,让 99% 请求失去 distributed context propagation。
6. 每个子问题单独决定
合法结果示例:
这比一个“大 telemetry refactor”更容易验证和回退。
建议分支与 commit
测量分支:
推荐 commit:
bench: isolate call context push and snapshot costsbench: isolate metrics and activity statesbench: add telemetry allocation attribution scenariosperf-test: add formal telemetry macro matrixdocs: record call-context telemetry go-no-go evidencethrowaway candidate 必须单独 commit,例如:
如果不达门槛,revert experiment,benchmark/evidence 保留。
A. CallContext 调查
A1. 先建立纯 microbenchmark
至少保留/新增以下 cases:
每个记录:
NoContextWork必须存在,否则不知道 Push 成本相对什么。benchmark harness 注意
不要在 benchmark method 自己创建 Task/closure 来制造异步边界,然后把它的 allocation 归到 AsyncLocal。
先测同步 push/restore;再单独测一个真实 async service continuation 场景。
A2. 真实 server invocation A/B
构造以下 server workloads:
注意:6/7 只用于归因读取成本,不是允许 runtime 猜业务会不会读取。
正式 macro 使用 #122 formal recorder。
concurrency:
至少 TCP;如果 SharedMemory 能放大 runtime CPU 占比,再补 SharedMemory。
A3. Allocation stack
如果 default case 有 per-call object allocation,确认是不是:
不要只看类型名;记录 call stack。
A4. A 的 Go/No-Go 门槛
Go 只允许在以下情况
找到一个不改变
SharpLinkCallContext.Current可观察语义的 candidate,并满足至少一个:No-Go
如果主要成本就是“每个 invocation 必须建立正确 AsyncLocal scope”,且没有透明低风险替代,则明确记录:
然后关闭 A,不发明 pooling/global static/current-thread hack。
B. Metrics 调查
B1. 固定 provider/listener
测试必须明确启用了哪些 instruments。不能写“metrics on”但不同运行注册不同 MeterListener/OpenTelemetry provider。
至少:
client/server 分开测,再测 both enabled。
B2. 直接测
SharpLinkTelemetryscope需要一个低噪声 microbenchmark,隔离:
每种在 off/on 状态测 B/op 和 ns/op。
同时测 instrument
Enabledguard 本身,不要把 provider export/background worker 成本和 runtime instrumentation 成本混成一个数字。B3. Provider export 分层
至少区分:
本 issue 首先优化 SharpLink instrumentation cost,不把网络 exporter 性能问题算给 SharpLink。
B4. Metrics correctness
任何 candidate 必须保持:
要新增 deterministic metric listener tests,而不是只人工看 Grafana。
B5. Metrics Go 门槛
正式 #122 macro 下:
如果主要成本来自 provider/exporter 而不是 SharpLink instrumentation:No-Go,记录边界。
C. Tracing 调查
C1. Sampling matrix 必须正确
建立一个可控制的
ActivityListener,分别返回:再建立真实采样组合:
这两个 1% 场景必须分开,因为语义完全不同。
C2. 记录的指标
Activity.Current/ trace id propagation correctness;C3. 优先验证的假设
当前值得优先验证:
throwaway prototype 可以类似:
但哪些 tag 在 PropagationData 下属于现有 contract,必须先用 tests/文档确认。不能看到
IsAllDataRequested就机械包住所有代码。另一个假设:error path 的昂贵 error tags 只在 activity 真正记录时创建;仍需保持 error/status metric semantics。
C4. 禁止的“优化”
禁止:
C5. Tracing Go 门槛
先把目标按语义分开:
None sampling
应接近 telemetry off;如果不接近,说明 guard 有问题。
1% recorded + 99% None
SharpLink tracing overhead 目标 <=5%。
1% recorded + 99% PropagationData
不要硬套 <=5%。PropagationData 本身要求创建/传播 Activity context。这里的 Go 条件是:
100% recorded
主要作为 correctness/upper-bound control,不要求和 off 接近。
统一的正式 macro matrix
#122 完成后至少跑:
Server
Client
每个至少:
固定 tiny unary workload 优先,因为 payload/serialization 太大时会掩盖 per-call diagnostics overhead。
base/head 同机交替 >=5 轮。
必须新增/强化的 correctness tests
CallContext
Metrics
Tracing
风险
风险:把公开 context 改成 pool/reuse
业务代码、interceptor、logger、AsyncLocal continuation 可能捕获它。跨调用重用会产生极难复现的数据串扰。禁止。
风险:错误理解
ActivitySamplingResultPropagationData不是None。先写行为测试,再做 tag enrichment candidate。风险:把 exporter 成本归因给 Runtime
所有实验必须有 no-export/listener-only control。
风险:Telemetry test 使用 process-global listener 导致并行串扰
在 #83 完成前/期间按现有 global-test 约束隔离;不要为了 perf test 随意恢复 assembly-wide serial,也不要让 listener 泄漏到其他测试。
风险:candidate 只改善 microbenchmark
最终 Go 必须有 #122 formal macro evidence。
回退规则
每个 A/B/C candidate 独立 commit。
立即 revert candidate,如果出现:
SharpLinkCallContext.Current缺失/串扰;benchmark、correctness tests、No-Go evidence 可保留。
本 issue 的完成定义
本 issue 可以在 A/B/C 全部 No-Go 时正常关闭;“没有值得做的优化”是有效结论。