Skip to content

feat(ddtrace/tracer): promote span fields out of meta map into a typed SpanAttributes struct #4538

Open
darccio wants to merge 65 commits intomainfrom
dario.castane/apmlp-856/promote-redundant-span-fields
Open

feat(ddtrace/tracer): promote span fields out of meta map into a typed SpanAttributes struct #4538
darccio wants to merge 65 commits intomainfrom
dario.castane/apmlp-856/promote-redundant-span-fields

Conversation

@darccio
Copy link
Copy Markdown
Member

@darccio darccio commented Mar 13, 2026

For reviewers, a walkthrough doc: https://docs.google.com/document/d/1kWp1ZxC5s9laqbva9cqrY0cixQYQm3b8m5Y3VA8oX6A/edit?usp=sharing

What does this PR do?

Introduces two new types in ddtrace/tracer/internal and wires them into the span lifecycle:

SpanAttributes — a compact, fixed-size struct that stores the four V1-protocol promoted span fields (env, version, component, span.kind) in a typed [4]string array indexed by integer AttrKey constants, backed by a 1-byte presence bitmask (setMask). The bitmask distinguishes "field never set" from "field explicitly set to empty string", which a plain map[string]string cannot express. Total size: 72 bytes. Read and write operations are bitmask tests and array accesses — no string comparison, no hash, no allocation.

SpanMeta — replaces span.meta map[string]string as the span's metadata field. Internally it holds a flat map[string]string (m) for arbitrary tags and a *SpanAttributes (attrs) for the four promoted keys. The custom msgp codec merges both sources transparently so the V0.4 wire format is unchanged. A SetMap/DeleteMap fast path writes directly to m for non-promoted keys (used by the init hot-path); the Set/Delete path routes promoted keys to attrs via copy-on-write.

Copy-on-write shared attrs — the tracer holds two process-level SpanAttributes instances (sharedAttrs and sharedAttrsForMainSvc, the latter pre-populated with version for main-service spans under universalVersion=false). Every new span starts by sharing one of these pointers via NewSpanMeta(sharedAttrs). A COW clone is triggered only when a span sets a per-span promoted field (component, span.kind, or a differing env/version), meaning spans that never override promoted fields allocate no SpanAttributes of their own.

Inline() — zero-allocation Merge() at finish time — at span.finish(), after context.finish() completes all trace-level tag propagation, s.meta.Inline() copies all set attrs values into m (at most 4 writes). After this point attrs is still authoritative for Get(), but Merge() can detect that attrsNotInM() == 0 and return sm.m directly — eliminating the make(map[string]string) that would be required to return a read-only copy of the merged view of promoted fields and m. EncodeMsg and Msgsize similarly skip the attrs loop entirely when all attrs are already in m.

V1 payloadpayload_v1.go skips promoted keys when iterating the meta map for the attributes array (AttrKeyForTag guard), then reads env, version, component, span.kind via meta.Get() for the dedicated fields 13–16. The size calculation uses Count() - AttrCount() to count only the flat-map entries.

span_msgp.go — the generated codec is replaced by a hand-maintained equivalent. EncodeMsg delegates to SpanMeta.EncodeMsg; DecodeMsg reads directly into SpanMeta.DecodeMsg which stores all keys (including promoted ones on the decode path) in the flat map, avoiding any SpanAttributes allocation at decode time. A helper script (scripts/msgp_span_meta_omitempty.go) patches the omitempty guard that the msgp generator cannot produce for types implementing msgp.Encodable.

Compile-time assertions ([1]byte{}[AttrKey-N]) guard the numeric values of the AttrKey constants, turning any future reordering into a build error.

Motivation

Previously span.meta was a plain map[string]string. The four promoted fields (env, version, component, span.kind) were stored in that map alongside arbitrary tags and had to be looked up by string key throughout the hot path: V1 encoding, sampler, peer-service detection, stats concentrator, and debug logging each paid a map lookup and a string hash. In addition:

  • During the evolution of the design, every call to Merge() (used by the stats concentrator and V0.4 payload builder) unconditionally allocated a fresh map[string]string even for the common case where all promoted attrs are set, costing ~200–400 bytes per finished span.
  • The map cannot distinguish "field never set" from "explicitly set to """, which is relevant for the V1 encoder's field-presence semantics.

This change eliminates the per-span allocation for the promoted fields on the read path (replaced by array index + bitmask), eliminates the Merge() allocation on the finish path (replaced by direct sm.m return after Inline()), and reduces allocation pressure during span construction by sharing process-level attrs across all spans via COW.

Reviewer's Checklist

  • Changed code has unit tests for its functionality at or near 100% coverage.
  • There is a benchmark for any new code, or changes to existing code.
  • New code is free of linting errors. You can check this by running make lint locally.
  • New code doesn't break existing tests. You can check this by running make test locally.
  • Add an appropriate team label so this PR gets put in the right place for the release notes.
  • All generated files are up to date. You can check this by running make generate locally.
  • Non-trivial go.mod changes, e.g. adding new modules, are reviewed by @DataDog/dd-trace-go-guild. Make sure all nested modules are up to date by running make fix-modules locally.

Unsure? Have a question? Request a review!

@datadog-prod-us1-4
Copy link
Copy Markdown

datadog-prod-us1-4 bot commented Mar 13, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

🎯 Code Coverage (details)
Patch Coverage: 45.16%
Overall Coverage: 59.86% (+3.72%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 423355d | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 13, 2026

Codecov Report

❌ Patch coverage is 45.07772% with 212 lines in your changes missing coverage. Please review.
✅ Project coverage is 60.51%. Comparing base (b22fbe0) to head (423355d).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
ddtrace/tracer/internal/span_meta.go 16.50% 164 Missing and 3 partials ⚠️
ddtrace/tracer/internal/span_attributes.go 62.74% 16 Missing and 3 partials ⚠️
ddtrace/tracer/span_msgp.go 60.41% 17 Missing and 2 partials ⚠️
ddtrace/tracer/civisibility_tslv.go 20.00% 4 Missing ⚠️
ddtrace/tracer/span.go 85.71% 2 Missing and 1 partial ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
ddtrace/mocktracer/mockspan.go 15.75% <100.00%> (ø)
ddtrace/tracer/abandonedspans.go 90.64% <100.00%> (ø)
ddtrace/tracer/payload_v1.go 68.69% <100.00%> (ø)
ddtrace/tracer/sampler.go 95.96% <100.00%> (ø)
ddtrace/tracer/spancontext.go 92.73% <100.00%> (ø)
ddtrace/tracer/stats.go 98.50% <100.00%> (ø)
ddtrace/tracer/tracer.go 88.88% <100.00%> (ø)
ddtrace/tracer/writer.go 91.83% <100.00%> (ø)
ddtrace/tracer/span.go 86.37% <85.71%> (ø)
ddtrace/tracer/civisibility_tslv.go 24.75% <20.00%> (ø)
... and 3 more

... and 429 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pr-commenter
Copy link
Copy Markdown

pr-commenter bot commented Mar 13, 2026

Benchmarks

Benchmark execution time: 2026-03-27 18:59:17

Comparing candidate commit 423355d in PR branch dario.castane/apmlp-856/promote-redundant-span-fields with baseline commit b22fbe0 in branch main.

Found 3 performance improvements and 11 performance regressions! Performance is the same for 202 metrics, 8 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:BenchmarkPartialFlushing/Disabled-25

  • 🟥 execution_time [+6.692ms; +9.109ms] or [+2.528%; +3.440%]

scenario:BenchmarkPartialFlushing/Enabled-25

  • 🟥 execution_time [+7.973ms; +10.093ms] or [+2.991%; +3.786%]

scenario:BenchmarkPayloadVersions/simple_1000spans/v0_4-25

  • 🟩 execution_time [-17.020µs; -16.000µs] or [-7.946%; -7.470%]

scenario:BenchmarkPayloadVersions/simple_1000spans/v1_0-25

  • 🟥 execution_time [+7.063µs; +7.835µs] or [+2.479%; +2.750%]

scenario:BenchmarkPayloadVersions/simple_100spans/v0_4-25

  • 🟩 execution_time [-1.587µs; -1.468µs] or [-6.462%; -5.978%]

scenario:BenchmarkPayloadVersions/simple_100spans/v1_0-25

  • 🟥 execution_time [+600.897ns; +724.303ns] or [+2.030%; +2.447%]

scenario:BenchmarkPayloadVersions/simple_10spans/v0_4-25

  • 🟩 execution_time [-146.569ns; -131.631ns] or [-4.735%; -4.252%]

scenario:BenchmarkPayloadVersions/simple_1spans/v0_4-25

  • 🟥 execution_time [+17.791ns; +29.969ns] or [+2.612%; +4.400%]

scenario:BenchmarkSetTagString-25

  • 🟥 execution_time [+4.221ns; +7.055ns] or [+6.979%; +11.665%]

scenario:BenchmarkSetTagStringPtr-25

  • 🟥 execution_time [+3.649ns; +5.431ns] or [+4.604%; +6.851%]

scenario:BenchmarkSetTagStringer-25

  • 🟥 execution_time [+5.214ns; +5.748ns] or [+7.958%; +8.772%]

scenario:BenchmarkSingleSpanRetention/no-rules-25

  • 🟥 execution_time [+7.851µs; +9.061µs] or [+3.240%; +3.739%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-all-25

  • 🟥 execution_time [+8.077µs; +9.588µs] or [+3.284%; +3.899%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-half-25

  • 🟥 execution_time [+6.215µs; +7.758µs] or [+2.525%; +3.151%]

@darccio darccio marked this pull request as ready for review March 16, 2026 12:04
@darccio darccio requested review from a team as code owners March 16, 2026 12:04
@kakkoyun
Copy link
Copy Markdown
Member

kakkoyun commented Mar 16, 2026

Benchmarks

Benchmark execution time: 2026-03-16 12:16:14

scenario:BenchmarkOTelApiWithCustomTags/datadog_otel_api-25

  • 🟥 allocated_mem [+88 bytes; +97 bytes] or [+2.343%; +2.575%]

scenario:BenchmarkPartialFlushing/Disabled-25

  • 🟥 allocated_mem [+19.081MB; +19.086MB] or [+6.028%; +6.029%]

scenario:BenchmarkPartialFlushing/Enabled-25

  • 🟥 allocated_mem [+19.075MB; +19.089MB] or [+5.544%; +5.548%]
  • 🟥 avgHeapInUse(Mb) [+0.990MB; +4.774MB] or [+2.147%; +10.358%]

scenario:BenchmarkSetTagStringPtr-25

  • 🟥 execution_time [+2.831ns; +4.811ns] or [+3.579%; +6.083%]

scenario:BenchmarkSetTagStringer-25

  • 🟥 execution_time [+2.455ns; +3.107ns] or [+3.715%; +4.702%]

scenario:BenchmarkSingleSpanRetention/no-rules-25

  • 🟥 allocated_mem [+9.701KB; +9.703KB] or [+6.359%; +6.360%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-all-25

  • 🟥 allocated_mem [+9.779KB; +9.956KB] or [+6.305%; +6.419%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-half-25

  • 🟥 allocated_mem [+9.798KB; +9.911KB] or [+6.315%; +6.388%]

scenario:BenchmarkStartSpan-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+6.370%; +6.370%]
  • 🟥 execution_time [+40.501ns; +57.499ns] or [+2.554%; +3.626%]

scenario:BenchmarkStartSpanConfig/scenario_WithStartSpanConfig-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+4.936%; +4.936%]

scenario:BenchmarkStartSpanConfig/scenario_none-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+4.209%; +4.209%]

scenario:BenchmarkTracerAddSpans-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+4.362%; +4.362%]

I wasn't expecting this.

@kakkoyun
Copy link
Copy Markdown
Member

Benchmarks

Benchmark execution time: 2026-03-16 12:16:14

scenario:BenchmarkOTelApiWithCustomTags/datadog_otel_api-25

  • 🟥 allocated_mem [+88 bytes; +97 bytes] or [+2.343%; +2.575%]

scenario:BenchmarkPartialFlushing/Disabled-25

  • 🟥 allocated_mem [+19.081MB; +19.086MB] or [+6.028%; +6.029%]

scenario:BenchmarkPartialFlushing/Enabled-25

  • 🟥 allocated_mem [+19.075MB; +19.089MB] or [+5.544%; +5.548%]
  • 🟥 avgHeapInUse(Mb) [+0.990MB; +4.774MB] or [+2.147%; +10.358%]

scenario:BenchmarkSetTagStringPtr-25

  • 🟥 execution_time [+2.831ns; +4.811ns] or [+3.579%; +6.083%]

scenario:BenchmarkSetTagStringer-25

  • 🟥 execution_time [+2.455ns; +3.107ns] or [+3.715%; +4.702%]

scenario:BenchmarkSingleSpanRetention/no-rules-25

  • 🟥 allocated_mem [+9.701KB; +9.703KB] or [+6.359%; +6.360%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-all-25

  • 🟥 allocated_mem [+9.779KB; +9.956KB] or [+6.305%; +6.419%]

scenario:BenchmarkSingleSpanRetention/with-rules/match-half-25

  • 🟥 allocated_mem [+9.798KB; +9.911KB] or [+6.315%; +6.388%]

scenario:BenchmarkStartSpan-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+6.370%; +6.370%]
  • 🟥 execution_time [+40.501ns; +57.499ns] or [+2.554%; +3.626%]

scenario:BenchmarkStartSpanConfig/scenario_WithStartSpanConfig-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+4.936%; +4.936%]

scenario:BenchmarkStartSpanConfig/scenario_none-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+4.209%; +4.209%]

scenario:BenchmarkTracerAddSpans-25

  • 🟥 allocated_mem [+96 bytes; +96 bytes] or [+4.362%; +4.362%]

I wasn't expecting this.

Okay this is because of the dual write.

@darccio darccio requested a review from a team as a code owner March 16, 2026 19:18
Copy link
Copy Markdown
Member

@kakkoyun kakkoyun left a comment

Choose a reason for hiding this comment

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

This is on great trajectory.

However, I think we should meditate on the API a little more. spanMeta types is good place to hide all the implementation details. We shouldn't allow accessing spanMeta.m and spanMeta.attrs directly.


//go:linkname spanStart github.com/DataDog/dd-trace-go/v2/ddtrace/tracer.spanStart
func spanStart(operationName string, options ...tracer.StartSpanOption) *tracer.Span
func spanStart(operationName string, sharedAttrs unsafe.Pointer, options ...tracer.StartSpanOption) *tracer.Span
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think we should go level higher here if it is possible. sharedAttrs should be an implementation detail.

Suggested change
func spanStart(operationName string, sharedAttrs unsafe.Pointer, options ...tracer.StartSpanOption) *tracer.Span
func spanStart(operationName string, meta spanMeta, options ...tracer.StartSpanOption) *tracer.Span

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@kakkoyun mocktracer doesn't have visibility of the unexported spanMeta type. How should this be done?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

😭 mocktracer strikes again. We should find a better solution for it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@kakkoyun It's called inspectable tracer 😁 #4512

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.

I agree with Kemal; would #4512 unblock this for us?

@darccio darccio force-pushed the dario.castane/apmlp-856/promote-redundant-span-fields branch from 08a2e11 to 1ce48fe Compare March 19, 2026 07:46
@darccio darccio requested a review from kakkoyun March 20, 2026 09:09
@darccio darccio marked this pull request as draft March 20, 2026 10:30
@darccio darccio marked this pull request as ready for review March 23, 2026 08:44
@darccio
Copy link
Copy Markdown
Member Author

darccio commented Mar 23, 2026

@kakkoyun PR ready. Changes in fe22828 are needed to introduce significant improvements over multiple benchmarks.

Copy link
Copy Markdown
Member

@kakkoyun kakkoyun left a comment

Choose a reason for hiding this comment

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

The API surface improved quite a bit. We are adding a lot of complexity for this optimization but I'm glad we containing it in an internal package.

That being said, I still see some room for improvement. There are several methods that kind of access the internal workings of the SpanMeta and SpanAttributes.

Methods in particula: ReplaceSharedAttrs, IsPromotedKeyLen
It would be amazing to make Inline transparent as well.

Ideally, tracer/span.go shouldn't now about interworking of the SpanMeta the logic around promoting and inlining.

@darccio darccio requested a review from kakkoyun March 24, 2026 16:04
darccio added 19 commits March 27, 2026 09:10
Add Put() as an inlineable flat-map write for paths wehre promoted keys are already handled. Rename Merge() to Map() and make it transparently inline promoted attrs into sm.m on first call.
@darccio darccio force-pushed the dario.castane/apmlp-856/promote-redundant-span-fields branch from f692e05 to d80aad0 Compare March 27, 2026 11:44
Copy link
Copy Markdown
Contributor

@mtoffl01 mtoffl01 left a comment

Choose a reason for hiding this comment

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

component and span.kind are never set on the tracer's shared attrs — they are not "process-level shared attributes," rather they're purely per-span fields set by integrations. Including them in SpanAttributes means every integration span triggers a COW clone, even though the "shared" part of the struct (env, version, language) is never what changed. Would it make sense to only promote the truly process-level fields into SpanAttributes and leave component/span.kind in the flat map? This approach might not be fully optimal for the V1 encoder, but it's still better than before, and v1 can still read those dedicated fields from the map.

Alternatively, I could see this being more optimal if it was structured as one copy per integration.

dataStreams: dataStreamsProcessor,
logFile: logFile,
}
// Build the shared SpanAttributes that every span will start from.
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.

it would be nice to put some of this in a separate helper function, dedicated to building sharedAttrs.


//go:linkname spanStart github.com/DataDog/dd-trace-go/v2/ddtrace/tracer.spanStart
func spanStart(operationName string, options ...tracer.StartSpanOption) *tracer.Span
func spanStart(operationName string, sharedAttrs unsafe.Pointer, options ...tracer.StartSpanOption) *tracer.Span
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.

I agree with Kemal; would #4512 unblock this for us?

Comment on lines +38 to +55
// SpanMeta replaces a plain map[string]string for the Span.meta field.
// Promoted attributes (env, version, component, span.kind, language) live in
// attrs and are excluded from the map m. The msgp codec merges both sources
// transparently so the wire format is unchanged.
//
// Set routes promoted keys to attrs (with copy-on-write) and others to the
// flat map. Promoted keys never appear in sm.m until Map() is called.
//
// Map() inlines promoted attrs into sm.m under mu, then returns sm.m directly
// (zero allocation on the hot stats path). EncodeMsg, Msgsize, Range,
// SerializableCount, and IsZero also acquire mu so they see a consistent view
// of sm.m during concurrent serialization.
type SpanMeta struct {
m map[string]string
attrs *SpanAttributes
mu locking.Mutex
inlined atomic.Bool
}
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.

The distinction between attrs and m isn't very clear from reading SpanMeta alone. I think we can do a better job of defining their roles...

  1. rename attrs to either sharedAttrs (shows commonality with the tracer's sharedAttrs field) or promotedAttrs (makes the "promoted" behavior clear) , or
  2. comment could mention that attrs starts as a "borrowed" pointer from tracer's process-level SpanAttributes and is cloned into a span-specific copy only on write

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

This and the rest done in eece436

@darccio
Copy link
Copy Markdown
Member Author

darccio commented Mar 27, 2026

Would it make sense to only promote the truly process-level fields into SpanAttributes and leave component/span.kind in the flat map?

You are right, I tried to max the promoted fields. Let me demote them.

Alternatively, I could see this being more optimal if it was structured as one copy per integration.

I see your point but it's difficult. Let try this in a future PR.

it would be nice to put some of this in a separate helper function, dedicated to building sharedAttrs.

Good idea!

I agree with Kemal; would #4512 unblock this for us?

That PR will deprecate mocktracer - once all the mocktracer uses are removed from the codebase - so this should disappear along it.

The distinction between attrs and m isn't very clear from reading SpanMeta alone.

Applying it right now.

Great feedback, thanks!

…naming for shared attributes, improve internal naming
@darccio darccio force-pushed the dario.castane/apmlp-856/promote-redundant-span-fields branch from 161ea73 to 78586fe Compare March 27, 2026 16:27
@darccio
Copy link
Copy Markdown
Member Author

darccio commented Mar 27, 2026

@kakkoyun

It would be amazing to make Inline transparent as well.

I tried really hard to avoid it, so I renamed it to Finish. It's close to impossible to get it right, because not "inlining" brings more complexity and required code to avoid race conditions. Locking can work but it goes against the spirit of the design.

@darccio darccio force-pushed the dario.castane/apmlp-856/promote-redundant-span-fields branch from aedc9f7 to 9020723 Compare March 27, 2026 17:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants