perf(coderd/rbac): build span role attributes only when recording - #27310
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Authcheck-path benchmark (#27309 vs #27310)
Effect of this PR on the endpoint (
This change is an allocation reduction and is time-neutral. Wall time is flat within noise: the small low-org deltas and the +2.3% blip at 100 orgs (whose allocations are flat) sit inside this shared host's run-to-run variance, which swings ±10-23% at low org counts. The reproducible, clean signal (allocation counts have ±0% variance) is fewer bytes and allocations per request: ~4-5% less on the endpoint path at common org counts (1-10). On the pure per-object path the reduction grows with org count (B/op −2.6% → −7.3% at 100 orgs), since it removes the per- This confirms the mechanism: the removed Full benchstat (#27309 vs #27310)Authored with Coder Agents. |
92a7289 to
8ca4355
Compare
6bb1809 to
2c9356d
Compare
rbacTraceAttributes materialized the subject's role names (one string allocation per role) and was passed into every Filter, Authorize, and Prepare span at creation time, so the O(roles) work ran even when no tracer was recording. It also called SafeRoleNames twice. Replace it with setRBACAttributes, which attaches the same attributes after the span is created and only when span.IsRecording() is true, and reads SafeRoleNames once. Recorded spans are unchanged; untraced and unsampled calls skip the per-role work. This removes the per-group overhead that made small /authcheck requests slower once checks were batched through Filter.
2c9356d to
440e7ae
Compare

rbacTraceAttributesmaterialized the subject's role names (one string allocation per role) and was passed into everyFilter,Authorize, andPreparespan at creation time, so the O(roles) work ran even when no tracer was recording. It also calledSafeRoleNames()twice.Replace it with
setRBACAttributes, which attaches the same attributes after the span is created and only whenspan.IsRecording()is true, readingSafeRoleNames()once. Recorded spans are unchanged; untraced and unsampled calls skip the per-role work.This originated from #27309: once
/authcheckchecks are batched throughrbac.Filter, each below-threshold group paid the role-attribute build for theFilterspan and for every per-objectAuthorizespan, so the redundant per-call work showed up as extra allocations per request.Benchmarks
AMD EPYC 9575F,benchstat, no tracer configured (exercises theIsRecording()==falsepath).BenchmarkRBACManyOrgs(general RBAC eval), before vs after: wall time flat (geomean −0.04%), allocations strictly lower everywhere (geomean B/op −0.52%;Authorize−1.0 to −1.3% B/op), no regressions.Authcheck path (
BenchmarkAuthcheckGrouping, #27309 vs #27310, back-to-back): this change is an allocation reduction and is time-neutral. On the endpoint (Grouped) path, per-request allocations drop ~4-5% B/op at common org counts (1-10); on the pure per-object path the reduction grows with org count (B/op −2.6% → −7.3% at 100 orgs). Wall time is flat within noise: low-org deltas sit inside this host's ±10-23% run-to-run variance, so no wall-time claim is made.Net: same speed, less garbage per request, which also lowers GC pressure under real concurrent load.
Decision log
Filterspan wraps the whole filtering routine (total latency +num_objects); it is the valuable span and is kept. The costly part wasrbacTraceAttributes, not the span itself.rbacTraceAttributeswas O(roles): it allocated a string per role for thesubject_rolesattribute and calledSafeRoleNames()twice. OnFilter's below-threshold fallback it ran once for theFilterspan and again for each per-objectAuthorizespan, so a group of N objects paid N+1 builds vs the old loop's N. Benchmarks confirm this as real per-call allocation; its wall-time cost is below the authcheck benchmark's noise floor.IsRecording()requires the span object, so the three callsites moved fromStartSpan(ctx, rbacTraceAttributes(...))toStartSpan(ctx)thensetRBACAttributes(span, ...). No spans were removed or renamed; recorded output is identical.subject_roles/num_subject_roles/etc. are not computed. Unsampled spans emit nothing anyway, so there is no observable output change.Authored with Coder Agents.