Skip to content

Reduce allocations in AVP decode and message encode hot paths#229

Merged
fiorix merged 1 commit into
fiorix:mainfrom
kulcsartibor:main
Apr 25, 2026
Merged

Reduce allocations in AVP decode and message encode hot paths#229
fiorix merged 1 commit into
fiorix:mainfrom
kulcsartibor:main

Conversation

@kulcsartibor

Copy link
Copy Markdown
Contributor

PR Summary

Files changed

Committed:

  • diam/avp.go
  • diam/datatype/decoder.go
  • diam/datatype/decoder_test.go
  • diam/datatype/datatype.go
  • diam/dict/util.go
  • diam/dict/parser.go
  • diam/group.go
  • diam/header.go
  • diam/uintconv.go

Optimizations implemented

1. Typed FindAVPByCode — avoid interface{} boxing (diam/dict/util.go, diam/avp.go)

Added FindAVPByCode(appid, code, vendorID uint32) as a typed fast-path for the hot decode loop. The existing FindAVPWithVendor takes code interface{}, which boxes the uint32 on every call and requires a type switch. The decode path at avp.go:95 now calls the typed method directly. The original FindAVPWithVendor is preserved for backward compatibility.

2. Array-indexed decoder dispatch with map fallback (diam/datatype/decoder.go)

Replaced the map[TypeID]DecoderFunc lookup in Decode() with a fixed-size [maxTypeID]DecoderFunc array. TypeIDs are small contiguous integers (0–18), so array indexing is a single bounds check vs. map hashing. Decode() falls back to the exported Decoder map for any TypeID the array doesn't cover, preserving the ability for external code to register custom decoders via datatype.Decoder[myType] = myFunc. The Decoder map now also includes QoSFilterRuleType and IPv6Type so it stays consistent with the array. Added test cases to verify the registration of custom TypeID and decoder function.

3. Skip intermediate Grouped AVP copy (diam/avp.go, diam/group.go)

Grouped AVPs were decoded twice: first datatype.DecodeGrouped copied all payload bytes into a datatype.Grouped (make + copy), then diam.DecodeGrouped immediately converted it back to []byte to parse child AVPs. The new DecodeGroupedFromBytes takes the raw payload slice directly, eliminating the intermediate allocation. DecodeFromBytes now checks the dictionary type before dispatching, short-circuiting the grouped path. The original DecodeGrouped is preserved as a wrapper for backward compatibility.

4. Sentinel errors with contextual wrapping (diam/avp.go)

Replaced 5 fmt.Errorf calls on error paths in DecodeFromBytes with pre-allocated errors.New sentinel variables (errAVPHeaderTooShort, errAVPDataTooShort, errAVPVendorTooShort). Same for errAVPSerializeNilData in Serialize/SerializeTo. Removes formatting overhead and allocations on error paths, and reduces function code size which can help compiler inlining/optimization of the surrounding hot path. On the two errAVPDataTooShort return sites, the sentinel is wrapped with fmt.Errorf("%w: have %d need %d", ...) so debugging retains byte-count context while errors.Is identity is preserved.

5. putUint24 direct buffer write (diam/uintconv.go, diam/avp.go, diam/header.go)

Added putUint24(b []byte, n uint32) (package-internal) that writes 3 big-endian bytes directly into a target buffer. Replaced all copy(b, uint32to24(n)) calls in AVP.SerializeTo (1 call) and Header.SerializeTo (2 calls). Each uint32to24 allocated a 3-byte slice that was immediately copied and discarded. This single change made Header.SerializeTo zero-allocation.

6. Dictionary inheritance for apps with no own AVPs (diam/dict/parser.go)

mergeInheritedAVPs originally built its app set only from p.avpcode keys, so applications declared in the dictionary with zero AVPs of their own (e.g. <application id="1001" type="acct"/>) were skipped and base AVPs were never inherited into their lookup index. FindAVPByCode would then miss and return Unknown AVPs, breaking CER parsing for such apps. The merge now also seeds the app set from p.appcode, so every declared app gets its ancestor chain processed at load time. Fixes TestHandleCER_HandshakeMetadataTCP and TestHandleCER_HandshakeMetadata_CustomIP, which were hanging on <-sm.HandshakeNotify().


Benchmark results (Apple M1 Max, 10 cores)

Benchmark Before After Δ ns/op Δ allocs
DecodeAVP 103 ns, 4 allocs, 80 B 81 ns, 3 allocs, 72 B -21.4% -25%
ReadMessage 1,557 ns, 47 allocs, 1,160 B 1,144 ns, 31 allocs, 1,048 B -26.5% -34.0%
EncodeHeader 17.1 ns, 1 alloc, 24 B 2.5 ns, 0 allocs, 0 B -85.4% -100%
EncodeAVP 39.7 ns, 2 allocs 39.7 ns, 2 allocs ~0% 0%
WriteMessage 473 ns, 14 allocs 473 ns, 14 allocs ~0% 0%

Backward compatibility

All changes are backward-compatible:

  • No exported types, functions, or method signatures were removed or changed.
  • Original FindAVPWithVendor, FindAVP, DecodeGrouped, uint32to24, and the Decoder map are all preserved.
  • Decode() still resolves custom decoders registered via datatype.Decoder[myType] = myFunc (fallback after the array miss).
  • New exports added: FindAVPByCode, DecodeGroupedFromBytes.
  • All existing tests pass (go test ./...).

@kulcsartibor kulcsartibor marked this pull request as draft April 23, 2026 08:27
@fiorix

fiorix commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Thanks for the work on this. main has moved forward — five PRs merged today (#230, #231, #232, #233, #234), plus a new v4.2.1 tag. Could you rebase onto current main before marking ready for review? None of the recent changes touch the files in this PR so the rebase should be mechanical. Once it's rebased and marked ready, I'll review.

@kulcsartibor kulcsartibor force-pushed the main branch 3 times, most recently from 11b1c5a to bcdf7bf Compare April 23, 2026 11:23
@kulcsartibor

Copy link
Copy Markdown
Contributor Author

I made few changes to the decoder.go. With migrating to an array lookup from map speeds up the parsing by around 45%. This has the drawback that internal parser cannot be overridden just by putting items to the Decoder[]. So created the RegisterDecoder method to allow built in type overrides too.

The datatype.go I have theIPv6Type at the end of the list, not nice but avoid breaking downstream code if someone has a hardcoded integer comparison: if avp.Data.Type() == 10 { ... }

@kulcsartibor kulcsartibor marked this pull request as ready for review April 23, 2026 12:09
@kulcsartibor kulcsartibor marked this pull request as draft April 23, 2026 12:26
@kulcsartibor kulcsartibor marked this pull request as ready for review April 23, 2026 13:29
@fiorix

fiorix commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Nice work, tests pass locally and the perf motivation is clear. A few things before I approve:

1. datatype.Decoder semantic change is a breaking API shift for v4
The last commit drops built-ins from the exported Decoder map entirely. Downstream code that iterates or reads from Decoder for built-in types now sees an empty slot. Two options:

  • Keep Decoder pre-populated with built-ins (as before) and have Decode prefer decoderArray; slightly slower on map overrides but zero back-compat break.
  • Keep the extension-only model but call it out in the PR description so I can add a CHANGELOG note and migration pointer to RegisterDecoder.

Prefer the first unless you have a reason against it.

2. Split between FindAVPByCode (new) and FindAVPWithVendor (existing)
avp.go uses the new typed fast path; message.go and pretty_dump.go still call FindAVPWithVendor. Fine for this PR, but worth a TODO or a follow-up to migrate; otherwise we carry two lookup paths indefinitely.

3. Sentinel error wrapping is inconsistent
errAVPDataTooShort is returned bare once and wrapped with %w: have X need Y twice. errors.Is works in both cases, but the asymmetry is odd. Wrap all sites or none.

4. mergeInheritedAVPs memory
Every child app gets a full copy of ancestor entries in both indices. For the shipped dictionaries this is fine, but worth a quick sanity check on allocation size after Load() for the Gx/Gy/S6a set; if it's meaningful, a comment noting the trade-off would help future readers.

5. Minor

  • RegisterDecoder returning ErrCannotUnregisterBuiltin for nil-on-builtin is the right call, keep it.
  • IPv6Type position in datatype.go; agreed, leave it at the end.

Main ask is #1. Once that's resolved I'll do a final pass and merge.

@kulcsartibor

kulcsartibor commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

Pushed the changes.
#1 RegisterDecoder mirrors built-in writes into Decoder so introspection stays accurate.
#4 Maybe there is a better approach to merge the dictionary with little less memory footprint. Most of diameter apps have base and app specific, so only the base is duplicated.

@kulcsartibor

kulcsartibor commented Apr 23, 2026

Copy link
Copy Markdown
Contributor Author

For the #2
Maybe in v5 or later it would be nicer to change the lookup as little. The interface{} if from before g 1.18 without generics

// diam/dict/util.go — current
func (p *Parser) FindAVPWithVendor(appid uint32, code interface{}, vendorID uint32) (*AVP, error) {
    switch codeVal := code.(type) {
    case string:
        // ...
    case uint32:
        // ...
    case int:
        // ...
    default:
        return nil, fmt.Errorf("Unsupported AVP code type %T(%#v)", codeVal, code)
    }
}

Problems:

  1. A float64 or []byte in code compiles but panics at runtime.
  2. Every call boxes the argument (small heap allocation) and does a type switch (branch mispredict on the hot path).
  3. IDEs can't suggest the right type at the call site.
  4. The int case exists only because old Go code defaulted integer literals to int; modern callers don't need it.

Implement it like

type AVPCode interface {
    uint32 | string
}

func FindAVPWithVendor[T AVPCode](p *Parser, appid uint32, code T, vendorID uint32) (*AVP, error) {
    switch c := any(code).(type) {
    case uint32:
        return p.findByCode(appid, c, vendorID)
    case string:
        return p.findByName(appid, c, vendorID)
    }
    // Unreachable: the constraint rules out everything else at compile time.
    return nil, fmt.Errorf("dict: unreachable")
}

@fiorix

fiorix commented Apr 25, 2026

Copy link
Copy Markdown
Owner

#1 is clean, thanks. Verified locally with behavioral tests on top of your branch: Decoder has all 19 built-ins at init, direct writes to Decoder[builtin] are ignored by Decode (array wins), RegisterDecoder on a built-in mirrors both array and map, restore works, nil-on-builtin returns ErrCannotUnregisterBuiltin without breaking the built-in. Dispatch order unchanged.

One subtlety: direct writes to Decoder[builtin] still mutate the map but are silently no-op'd by Decode. So Decoder can drift from real dispatch behavior if someone bypasses RegisterDecoder. The doc warns about this, which is enough.

#2 Agreed, v5 material. Generic constraint uint32 | string is the right shape; drop the int case in the process. Open an issue tagged v5 so it doesn't get lost?

#3 Still worth doing in this PR; 2-line change. Either wrap all three sites or drop %w from the two that have it. Consistency matters more than which direction you pick.

#4 Fine to ship as-is. A one-line comment on mergeInheritedAVPs noting the trade-off (base AVPs duplicated per child app, bounded by dictionary size at Load time, not per-message) would help future readers. The shipped dictionaries are small enough that duplication is noise.

Once #3 is fixed, please squash the eight commits into a single commit with a clean message (subject under 72 chars, body describing the perf wins + RegisterDecoder API + FindAVPByCode). I'll merge on green CI.

@kulcsartibor

Copy link
Copy Markdown
Contributor Author

Fixed and pushed the changes in squashed commit.

@fiorix

fiorix commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Squash and #3 look good. Two small things before I merge:

  1. Please rebase onto current main (3 docs/cleanup commits landed today, none touch the files in this PR: fd700a8, 101c2e2, de48ccf).
  2. Move the "bounded duplication" note from the commit message into the mergeInheritedAVPs doc comment, e.g. one line like: // Memory: base AVPs are duplicated per child app at Load time, not per message; bounded by dictionary size. The commit-message version is fine but a future reader of the source won't see it.

Once those are in, I'll merge on green CI.

Reduces ReadMessage allocations by ~34% and latency by ~26% on the hot
decode path through five targeted changes:
- Add typed `dict.FindAVPByCode(appid, code, vendorID uint32)` to
  eliminate `interface{}` boxing and the type switch in the inner decode
  loop. The existing `FindAVPWithVendor` is preserved for v4
  compatibility; migration to the typed primitive is left as v5 follow-up.
- Pre-merge inherited AVPs into child app indices at `Load()` time, so
  every `FindAVPByCode` lookup resolves in a single map access instead
  of walking the parent chain at runtime. Bounded duplication: base
  AVPs are copied per declared child app at load time, not per message.
- Replace map-based decoder dispatch with an array-indexed table for
  built-in TypeIDs (0..maxTypeID). The `Decoder` map remains
  pre-populated with every built-in for back-compat; iteration and
  direct reads continue to return the library defaults. Direct writes
  to `Decoder[builtinType]` are silently no-op'd by `Decode` (the
  array wins) — documented; use `RegisterDecoder` to override built-ins.
- Add `RegisterDecoder(t, f) error` for installing custom decoders or
  overriding built-ins. Built-in writes mirror into both `decoderArray`
  (the dispatch table) and `Decoder` (the introspection map) so the
  exported map gives an accurate view of what is actually registered.
  Returns `ErrCannotUnregisterBuiltin` on a nil DecoderFunc for a
  built-in TypeID — the original built-in cannot be recovered, so
  silently disabling Decode is refused.
- Pre-allocate sentinel errors on the AVP decode hot path; replace
  `fmt.Errorf` formatting overhead with bare sentinels wrapped via
  `%w: have N need M` at the truncation sites that carry byte counts.
  All four truncation sentinels (`errAVPHeader/Data/Vendor TooShort`)
  are now consistently wrapped; `errors.Is` works everywhere.
- Skip the intermediate `make+copy` for Grouped AVP bodies via
  `DecodeGroupedFromBytes`, which takes the payload slice directly.
- Use `putUint24` for direct big-endian 3-byte writes in
  `AVP.SerializeTo` and `Header.SerializeTo`, eliminating the per-call
  3-byte slice allocation. Header serialisation is now zero-alloc.
No wire-format or dictionary-schema changes. New exported API:
`FindAVPByCode`, `DecodeGroupedFromBytes`, `RegisterDecoder`,
`ErrCannotUnregisterBuiltin`. v5 follow-ups tracked separately:
generic `FindAVPWithVendor[T uint32 | string]` to retire the
`interface{}` lookup path; migration of `message.go` and
`pretty_dump.go` callers to `FindAVPByCode`.
@kulcsartibor

Copy link
Copy Markdown
Contributor Author

Updated and squashed the commit. There is an issue with one test: server_close_test.go:24
It should run only in linux if sctp is available. I have a fix for but did not want to mix with the PR.

@fiorix

fiorix commented Apr 25, 2026

Copy link
Copy Markdown
Owner

Rebase and #4 are clean. Verified locally: single commit on top of current main, doc comment now carries the memory note, all tests pass.

The server_close_test.go issue is pre-existing (came in via #231); please open a separate PR for that fix so we can keep this one focused.

Approving and will merge on green CI.

@fiorix fiorix left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

LGTM. Will merge once CI is green.

@fiorix fiorix merged commit 454c08c into fiorix:main Apr 25, 2026
1 check passed
tridentsx pushed a commit to tridentsx/go-diameter that referenced this pull request May 9, 2026
Reduces ReadMessage allocations by ~34% and latency by ~26% on the hot
decode path through five targeted changes:
- Add typed `dict.FindAVPByCode(appid, code, vendorID uint32)` to
  eliminate `interface{}` boxing and the type switch in the inner decode
  loop. The existing `FindAVPWithVendor` is preserved for v4
  compatibility; migration to the typed primitive is left as v5 follow-up.
- Pre-merge inherited AVPs into child app indices at `Load()` time, so
  every `FindAVPByCode` lookup resolves in a single map access instead
  of walking the parent chain at runtime. Bounded duplication: base
  AVPs are copied per declared child app at load time, not per message.
- Replace map-based decoder dispatch with an array-indexed table for
  built-in TypeIDs (0..maxTypeID). The `Decoder` map remains
  pre-populated with every built-in for back-compat; iteration and
  direct reads continue to return the library defaults. Direct writes
  to `Decoder[builtinType]` are silently no-op'd by `Decode` (the
  array wins) — documented; use `RegisterDecoder` to override built-ins.
- Add `RegisterDecoder(t, f) error` for installing custom decoders or
  overriding built-ins. Built-in writes mirror into both `decoderArray`
  (the dispatch table) and `Decoder` (the introspection map) so the
  exported map gives an accurate view of what is actually registered.
  Returns `ErrCannotUnregisterBuiltin` on a nil DecoderFunc for a
  built-in TypeID — the original built-in cannot be recovered, so
  silently disabling Decode is refused.
- Pre-allocate sentinel errors on the AVP decode hot path; replace
  `fmt.Errorf` formatting overhead with bare sentinels wrapped via
  `%w: have N need M` at the truncation sites that carry byte counts.
  All four truncation sentinels (`errAVPHeader/Data/Vendor TooShort`)
  are now consistently wrapped; `errors.Is` works everywhere.
- Skip the intermediate `make+copy` for Grouped AVP bodies via
  `DecodeGroupedFromBytes`, which takes the payload slice directly.
- Use `putUint24` for direct big-endian 3-byte writes in
  `AVP.SerializeTo` and `Header.SerializeTo`, eliminating the per-call
  3-byte slice allocation. Header serialisation is now zero-alloc.
No wire-format or dictionary-schema changes. New exported API:
`FindAVPByCode`, `DecodeGroupedFromBytes`, `RegisterDecoder`,
`ErrCannotUnregisterBuiltin`. v5 follow-ups tracked separately:
generic `FindAVPWithVendor[T uint32 | string]` to retire the
`interface{}` lookup path; migration of `message.go` and
`pretty_dump.go` callers to `FindAVPByCode`.
arberkatellari pushed a commit to arberkatellari/go-diameter that referenced this pull request May 12, 2026
Reduces ReadMessage allocations by ~34% and latency by ~26% on the hot
decode path through five targeted changes:
- Add typed `dict.FindAVPByCode(appid, code, vendorID uint32)` to
  eliminate `interface{}` boxing and the type switch in the inner decode
  loop. The existing `FindAVPWithVendor` is preserved for v4
  compatibility; migration to the typed primitive is left as v5 follow-up.
- Pre-merge inherited AVPs into child app indices at `Load()` time, so
  every `FindAVPByCode` lookup resolves in a single map access instead
  of walking the parent chain at runtime. Bounded duplication: base
  AVPs are copied per declared child app at load time, not per message.
- Replace map-based decoder dispatch with an array-indexed table for
  built-in TypeIDs (0..maxTypeID). The `Decoder` map remains
  pre-populated with every built-in for back-compat; iteration and
  direct reads continue to return the library defaults. Direct writes
  to `Decoder[builtinType]` are silently no-op'd by `Decode` (the
  array wins) — documented; use `RegisterDecoder` to override built-ins.
- Add `RegisterDecoder(t, f) error` for installing custom decoders or
  overriding built-ins. Built-in writes mirror into both `decoderArray`
  (the dispatch table) and `Decoder` (the introspection map) so the
  exported map gives an accurate view of what is actually registered.
  Returns `ErrCannotUnregisterBuiltin` on a nil DecoderFunc for a
  built-in TypeID — the original built-in cannot be recovered, so
  silently disabling Decode is refused.
- Pre-allocate sentinel errors on the AVP decode hot path; replace
  `fmt.Errorf` formatting overhead with bare sentinels wrapped via
  `%w: have N need M` at the truncation sites that carry byte counts.
  All four truncation sentinels (`errAVPHeader/Data/Vendor TooShort`)
  are now consistently wrapped; `errors.Is` works everywhere.
- Skip the intermediate `make+copy` for Grouped AVP bodies via
  `DecodeGroupedFromBytes`, which takes the payload slice directly.
- Use `putUint24` for direct big-endian 3-byte writes in
  `AVP.SerializeTo` and `Header.SerializeTo`, eliminating the per-call
  3-byte slice allocation. Header serialisation is now zero-alloc.
No wire-format or dictionary-schema changes. New exported API:
`FindAVPByCode`, `DecodeGroupedFromBytes`, `RegisterDecoder`,
`ErrCannotUnregisterBuiltin`. v5 follow-ups tracked separately:
generic `FindAVPWithVendor[T uint32 | string]` to retire the
`interface{}` lookup path; migration of `message.go` and
`pretty_dump.go` callers to `FindAVPByCode`.
arberkatellari pushed a commit to arberkatellari/go-diameter that referenced this pull request May 12, 2026
Reduces ReadMessage allocations by ~34% and latency by ~26% on the hot
decode path through five targeted changes:
- Add typed `dict.FindAVPByCode(appid, code, vendorID uint32)` to
  eliminate `interface{}` boxing and the type switch in the inner decode
  loop. The existing `FindAVPWithVendor` is preserved for v4
  compatibility; migration to the typed primitive is left as v5 follow-up.
- Pre-merge inherited AVPs into child app indices at `Load()` time, so
  every `FindAVPByCode` lookup resolves in a single map access instead
  of walking the parent chain at runtime. Bounded duplication: base
  AVPs are copied per declared child app at load time, not per message.
- Replace map-based decoder dispatch with an array-indexed table for
  built-in TypeIDs (0..maxTypeID). The `Decoder` map remains
  pre-populated with every built-in for back-compat; iteration and
  direct reads continue to return the library defaults. Direct writes
  to `Decoder[builtinType]` are silently no-op'd by `Decode` (the
  array wins) — documented; use `RegisterDecoder` to override built-ins.
- Add `RegisterDecoder(t, f) error` for installing custom decoders or
  overriding built-ins. Built-in writes mirror into both `decoderArray`
  (the dispatch table) and `Decoder` (the introspection map) so the
  exported map gives an accurate view of what is actually registered.
  Returns `ErrCannotUnregisterBuiltin` on a nil DecoderFunc for a
  built-in TypeID — the original built-in cannot be recovered, so
  silently disabling Decode is refused.
- Pre-allocate sentinel errors on the AVP decode hot path; replace
  `fmt.Errorf` formatting overhead with bare sentinels wrapped via
  `%w: have N need M` at the truncation sites that carry byte counts.
  All four truncation sentinels (`errAVPHeader/Data/Vendor TooShort`)
  are now consistently wrapped; `errors.Is` works everywhere.
- Skip the intermediate `make+copy` for Grouped AVP bodies via
  `DecodeGroupedFromBytes`, which takes the payload slice directly.
- Use `putUint24` for direct big-endian 3-byte writes in
  `AVP.SerializeTo` and `Header.SerializeTo`, eliminating the per-call
  3-byte slice allocation. Header serialisation is now zero-alloc.
No wire-format or dictionary-schema changes. New exported API:
`FindAVPByCode`, `DecodeGroupedFromBytes`, `RegisterDecoder`,
`ErrCannotUnregisterBuiltin`. v5 follow-ups tracked separately:
generic `FindAVPWithVendor[T uint32 | string]` to retire the
`interface{}` lookup path; migration of `message.go` and
`pretty_dump.go` callers to `FindAVPByCode`.
danbogos pushed a commit to cgrates/go-diameter that referenced this pull request May 12, 2026
Reduces ReadMessage allocations by ~34% and latency by ~26% on the hot
decode path through five targeted changes:
- Add typed `dict.FindAVPByCode(appid, code, vendorID uint32)` to
  eliminate `interface{}` boxing and the type switch in the inner decode
  loop. The existing `FindAVPWithVendor` is preserved for v4
  compatibility; migration to the typed primitive is left as v5 follow-up.
- Pre-merge inherited AVPs into child app indices at `Load()` time, so
  every `FindAVPByCode` lookup resolves in a single map access instead
  of walking the parent chain at runtime. Bounded duplication: base
  AVPs are copied per declared child app at load time, not per message.
- Replace map-based decoder dispatch with an array-indexed table for
  built-in TypeIDs (0..maxTypeID). The `Decoder` map remains
  pre-populated with every built-in for back-compat; iteration and
  direct reads continue to return the library defaults. Direct writes
  to `Decoder[builtinType]` are silently no-op'd by `Decode` (the
  array wins) — documented; use `RegisterDecoder` to override built-ins.
- Add `RegisterDecoder(t, f) error` for installing custom decoders or
  overriding built-ins. Built-in writes mirror into both `decoderArray`
  (the dispatch table) and `Decoder` (the introspection map) so the
  exported map gives an accurate view of what is actually registered.
  Returns `ErrCannotUnregisterBuiltin` on a nil DecoderFunc for a
  built-in TypeID — the original built-in cannot be recovered, so
  silently disabling Decode is refused.
- Pre-allocate sentinel errors on the AVP decode hot path; replace
  `fmt.Errorf` formatting overhead with bare sentinels wrapped via
  `%w: have N need M` at the truncation sites that carry byte counts.
  All four truncation sentinels (`errAVPHeader/Data/Vendor TooShort`)
  are now consistently wrapped; `errors.Is` works everywhere.
- Skip the intermediate `make+copy` for Grouped AVP bodies via
  `DecodeGroupedFromBytes`, which takes the payload slice directly.
- Use `putUint24` for direct big-endian 3-byte writes in
  `AVP.SerializeTo` and `Header.SerializeTo`, eliminating the per-call
  3-byte slice allocation. Header serialisation is now zero-alloc.
No wire-format or dictionary-schema changes. New exported API:
`FindAVPByCode`, `DecodeGroupedFromBytes`, `RegisterDecoder`,
`ErrCannotUnregisterBuiltin`. v5 follow-ups tracked separately:
generic `FindAVPWithVendor[T uint32 | string]` to retire the
`interface{}` lookup path; migration of `message.go` and
`pretty_dump.go` callers to `FindAVPByCode`.
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.

2 participants