Skip to content

perf(propagation): use a hex lookup table to decode traceparent - #8739

Open
itssaharsh wants to merge 6 commits into
open-telemetry:mainfrom
itssaharsh:propagation-hex-lookup-table
Open

perf(propagation): use a hex lookup table to decode traceparent#8739
itssaharsh wants to merge 6 commits into
open-telemetry:mainfrom
itssaharsh:propagation-hex-lookup-table

Conversation

@itssaharsh

@itssaharsh itssaharsh commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

perf(propagation): use a hex lookup table to decode traceparent

Summary

Replaces encoding/hex and the manual upperHex validation in the traceparent decode path with a single-pass reverse lookup table. (encoding/hex is still used by Inject for encoding.)

The "Why"

Currently, extractPart does a UTF-8 range loop over the string just to reject uppercase characters (to satisfy the W3C spec), and then runs encoding/hex.Decode.

By using a 256-byte reverse lookup table, we can handle both decoding and validation in one go. Invalid and uppercase characters map to 0xff. Since no valid nibble sets its top 4 bits, OR-ing the looked-up values and checking those bits catches invalid input in one check at the end.

Net effect: one less function, no separate validation scan, and no []byte conversion.

Impact

                                │ prop-base.txt │            prop-new.txt             │
                                │    sec/op     │   sec/op     vs base                │
Extract/Sampled-16                  904.3n ± 2%   826.0n ± 3%   -8.66% (p=0.000 n=12)
Extract/BogusVersion-16             196.4n ± 1%   197.7n ± 5%        ~ (p=0.378 n=12)
Extract/FutureAdditionalData-16     887.5n ± 1%   788.6n ± 1%  -11.14% (p=0.000 n=12)
geomean                             540.2n        504.9n        -6.52%

BogusVersion is unchanged as expected, since it bails on the version field before decoding anything substantial.

go test -run='^$' -bench='^BenchmarkExtract$' -benchmem -count=12 ./propagation/ > base.txt
# apply the patch
go test -run='^$' -bench='^BenchmarkExtract$' -benchmem -count=12 ./propagation/ > new.txt
benchstat base.txt new.txt

I ran this three times and got -8.66%, -13.96% and -15.65% on Extract/Sampled, with the baseline itself moving between 884n and 1181n. That's WSL2 on a laptop, so I'd read it as roughly 9-15% rather than a firm number. Direction was consistent and every run was p<0.001.

Allocations: unchanged. I'd assumed the []byte(part) cast was allocating and that removing it would show up here. It wasn't, escape analysis already kept it on the stack, so the win is entirely from dropping the range loop and the generic decode.

Implementation Notes

  • Code gen: The table is generated via gotmpl into propagation/internal/hextable/, matching the convention in sdk/log/internal/gen.go. trace/hex.go has an identical table but it's unexported and trace is a separate module the root module depends on, so propagation can't import it.
  • Reviewer question: If gotmpl feels like overkill for a single consumer, say so. propagation is in the root module, so a plain internal/hextable package would work with no codegen at all. Easy swap.
  • Future cleanup: Happy to migrate trace/hex.go onto the same template in a follow-up so there's genuinely one copy. Left it out to keep this focused.
  • Safety: This parses external headers, so replacing stdlib deserves scrutiny. The loop body is four lines with no conditionals in it, and validity is one check at the end. The 0xff sentinel and why it works are documented on the table. trace_context_test.go already has uppercase rejection tests for all four fields (version, trace ID, span ID, flags), which is exactly what upperHex provided, and those pass unchanged.
  • Still not optimal: the loop keeps 3 bounds checks per iteration (part[i], part[i+1], dst[i/2]). Unlike TraceIDFromHex these can't be removed with constant indices, since n varies across the fields (2, 32, 16).

Testing

  • go test -race ./... in the root module and sdk, 33 packages, all passing
  • make generate toolchain-check license-check misspell go-mod-tidy golangci-lint-fix verify-readmes verify-mods clean, no files modified. make generate reproduces the checked-in hextable.go byte for byte
  • Full make precommit doesn't go green on my machine, but both failures reproduce on an unmodified tree: otlpmetricgrpc's TestSelfObservability gets DeadlineExceeded instead of Unavailable because WSL2 doesn't refuse a dial to 127.0.0.1:1 fast enough, and otlploghttp needs ~66s under -race against the 60s default

extractPart called encoding/hex behind an upperHex guard, and upperHex
ranged over the string decoding UTF-8 just to reject the upper-case
A-F the spec disallows.

A reverse lookup table does both jobs at once: invalid characters map
to 0xff, so OR-ing every looked-up value and checking the top 4 bits
catches anything invalid, upper-case included. That drops upperHex and
the []byte conversion along with it.

Extract runs once per incoming request:

  Extract/Sampled-16               904.3n ± 2%  826.0n ± 3%   -8.66%
  Extract/FutureAdditionalData-16  887.5n ± 1%  788.6n ± 1%  -11.14%

p=0.000, n=12. Allocations are unchanged; the []byte conversion was
already stack allocated.

The table goes in internal/shared so there is a single source of truth
if other packages need it later.
@codecov

codecov Bot commented Aug 12, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.5%. Comparing base (9ba91a2) to head (066e043).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##            main   #8739     +/-   ##
=======================================
- Coverage   88.5%   88.5%   -0.1%     
=======================================
  Files        333     333             
  Lines      21101   21100      -1     
=======================================
- Hits       18687   18683      -4     
- Misses      2414    2417      +3     
Files with missing lines Coverage Δ
propagation/trace_context.go 98.3% <100.0%> (-0.1%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment thread CHANGELOG.md Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Optimizes W3C traceparent extraction in go.opentelemetry.io/otel/propagation by replacing encoding/hex decoding plus a separate uppercase-validation scan with a single-pass reverse-lookup table that both decodes and rejects spec-disallowed uppercase hex.

Changes:

  • Rework traceparent part decoding to use a reverse hex lookup table with a single validity check.
  • Add a generated internal/hextable table (via gotmpl) and a shared template for reuse.
  • Add a changelog entry describing the performance/behavior-preserving decoding change.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
propagation/trace_context.go Switches extractPart to table-based hex decode + validation in one pass.
propagation/internal/hextable/hextable.go Adds generated 256-byte reverse lookup table for lowercase-hex decode/validation.
propagation/internal/gen.go Adds go:generate directive to generate hextable.go from the shared template.
internal/shared/hextable/hextable.go.tmpl Introduces reusable template for generating the reverse lookup table.
CHANGELOG.md Documents the traceparent decoding optimization in the Unreleased section.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread CHANGELOG.md Outdated
Co-authored-by: Tyler Yahn <MrAlias@users.noreply.github.com>
@itssaharsh
itssaharsh requested a review from MrAlias August 19, 2026 19:18
@itssaharsh

Copy link
Copy Markdown
Contributor Author

Is there anything I should add here :) @MrAlias

Comment thread internal/shared/hextable/hextable.go.tmpl Outdated
The template only ever had one consumer. trace/hex.go still has its own
private copy, so the generation machinery added moving parts without
removing the duplication it was meant to prevent.

If we ever do consolidate with trace/hex.go, we can revisit this then.
@itssaharsh

itssaharsh commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

I wanted to ask will this get merged when the next release is out?
@dashpole @MrAlias

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants