Files
livekit/pkg/sfu/buffer/dependencydescriptorparser_test.go
T
Raja SubramanianandClaude Fable 5.1 bc849eb47d sfu/buffer: parse the dependency descriptor without allocating (#4953)
* sfu/buffer: parse the dependency descriptor without allocating

Every VP9 and AV1 SVC packet allocated six objects in the dependency
descriptor parse: the descriptor, a clone of the frame template with
its three slices, a resolution copy, and a frame entity that took the
address of a parameter.

The pooled ExtDependencyDescriptor now owns the storage for the
descriptor, the template and the resolution, with fixed arrays for the
template's slices. The reader copies the matching template into that
storage.

Delta frame parse: 6 allocs, 176 B, 174 ns -> 0 allocs, 0 B, 122 ns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* sfu/dd: never copy a parsed frame into a structure template

A caller can hand Unmarshal a descriptor whose FrameDependencies points
at one of the structure's templates. Copying the packet's custom fields
into it would change the template for every later packet, so the reader
allocates a fresh template in that case.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 19:46:20 +05:30

237 lines
8.0 KiB
Go

// Copyright 2026 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package buffer
import (
"testing"
"github.com/pion/rtp"
"github.com/stretchr/testify/require"
"github.com/livekit/protocol/logger"
dd "github.com/livekit/livekit-server/pkg/sfu/rtpextension/dependencydescriptor"
)
const ddTestExtID = uint8(1)
// single spatial layer, single temporal layer, one decode target, one chain
func newL1T1Structure(structureID int) *dd.FrameDependencyStructure {
return &dd.FrameDependencyStructure{
StructureId: structureID,
NumDecodeTargets: 1,
NumChains: 1,
DecodeTargetProtectedByChain: []int{0},
Templates: []*dd.FrameDependencyTemplate{
{ // key frame
DecodeTargetIndications: []dd.DecodeTargetIndication{dd.DecodeTargetSwitch},
ChainDiffs: []int{0},
},
{ // delta frame
DecodeTargetIndications: []dd.DecodeTargetIndication{dd.DecodeTargetRequired},
FrameDiffs: []int{1},
ChainDiffs: []int{1},
},
},
}
}
type ddTestFeeder struct {
t *testing.T
parser *DependencyDescriptorParser
// structure the writer marshals against, i. e. what the publisher last sent
structure *dd.FrameDependencyStructure
seq uint16
}
func newDDTestFeeder(t *testing.T) *ddTestFeeder {
return &ddTestFeeder{
t: t,
parser: NewDependencyDescriptorParser(ddTestExtID, logger.GetLogger(), func(int32, int32) {}, false),
}
}
// keyFrame sends a key frame carrying `structure`. Passing the same structure id as
// the previous key frame models a publisher that repeats an unchanged structure,
// which is what screen content with frequent key frames does.
func (f *ddTestFeeder) keyFrame(frameNumber uint16, structure *dd.FrameDependencyStructure) (*ExtDependencyDescriptor, error) {
f.structure = structure
return f.feed(frameNumber, structure.Templates[0], structure)
}
func (f *ddTestFeeder) deltaFrame(frameNumber uint16) (*ExtDependencyDescriptor, error) {
return f.feed(frameNumber, f.structure.Templates[1], nil)
}
func (f *ddTestFeeder) feed(
frameNumber uint16,
template *dd.FrameDependencyTemplate,
attachedStructure *dd.FrameDependencyStructure,
) (*ExtDependencyDescriptor, error) {
f.t.Helper()
ddVal := &dd.DependencyDescriptor{
FirstPacketInFrame: true,
LastPacketInFrame: true,
FrameNumber: frameNumber,
FrameDependencies: template,
AttachedStructure: attachedStructure,
}
buf, err := (&dd.DependencyDescriptorExtension{Descriptor: ddVal, Structure: f.structure}).Marshal()
require.NoError(f.t, err)
f.seq++
pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: f.seq}}
require.NoError(f.t, pkt.SetExtension(ddTestExtID, buf))
extDD, _, err := f.parser.Parse(pkt)
return extDD, err
}
// A key frame that repeats the current structure must not start dropping frames that
// precede it. Upstream advanced the drop threshold on every structure bearing key
// frame, so with frequent key frames (screen content) a late or retransmitted frame
// arriving after one was discarded as "earlier than current structure".
func TestDependencyDescriptorParserLateFrameAfterRepeatedStructure(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
_, err := f.keyFrame(0, structure)
require.NoError(t, err)
_, err = f.deltaFrame(1)
require.NoError(t, err)
_, err = f.deltaFrame(3)
require.NoError(t, err)
// key frame repeating the same structure id
_, err = f.keyFrame(4, structure)
require.NoError(t, err)
// frame 2 finally shows up (reordered or retransmitted). The structure has not
// changed, so it is still decodable and must be forwarded.
extDD, err := f.deltaFrame(2)
require.NoError(t, err)
require.NotNil(t, extDD)
require.EqualValues(t, 2, extDD.ExtFrameNum)
}
// A key frame carrying a *different* structure does invalidate everything before it:
// earlier frames reference templates that no longer exist.
func TestDependencyDescriptorParserLateFrameAfterStructureChange(t *testing.T) {
f := newDDTestFeeder(t)
_, err := f.keyFrame(0, newL1T1Structure(0))
require.NoError(t, err)
_, err = f.deltaFrame(1)
require.NoError(t, err)
_, err = f.deltaFrame(3)
require.NoError(t, err)
_, err = f.keyFrame(4, newL1T1Structure(1))
require.NoError(t, err)
_, err = f.deltaFrame(2)
require.ErrorIs(t, err, ErrFrameEarlierThanKeyFrame)
}
// An out-of-order key frame must still be dropped: accepting it would regress
// structureExtFrameNum (ExtKeyFrameNum) and replay a stale structure update.
func TestDependencyDescriptorParserOutOfOrderKeyFrame(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
_, err := f.keyFrame(0, structure)
require.NoError(t, err)
_, err = f.deltaFrame(1)
require.NoError(t, err)
_, err = f.keyFrame(4, structure)
require.NoError(t, err)
_, err = f.keyFrame(2, structure)
require.ErrorIs(t, err, ErrFrameEarlierThanKeyFrame)
}
// ExtKeyFrameNum keeps tracking every structure bearing key frame, unchanged by the
// split of the drop threshold into its own field.
func TestDependencyDescriptorParserExtKeyFrameNum(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
extDD, err := f.keyFrame(0, structure)
require.NoError(t, err)
require.EqualValues(t, 0, extDD.ExtKeyFrameNum)
extDD, err = f.deltaFrame(1)
require.NoError(t, err)
require.EqualValues(t, 0, extDD.ExtKeyFrameNum)
// repeated structure still advances ExtKeyFrameNum
extDD, err = f.keyFrame(4, structure)
require.NoError(t, err)
require.EqualValues(t, 4, extDD.ExtKeyFrameNum)
extDD, err = f.deltaFrame(5)
require.NoError(t, err)
require.EqualValues(t, 4, extDD.ExtKeyFrameNum)
}
// a pooled ExtDependencyDescriptor carries the storage for its descriptor, so parsing a delta
// frame must not allocate and a reused descriptor must not show the previous packet's values
func TestDependencyDescriptorParserNoAlloc(t *testing.T) {
f := newDDTestFeeder(t)
structure := newL1T1Structure(0)
structure.Resolutions = []dd.RenderResolution{{Width: 640, Height: 360}}
extDD, err := f.keyFrame(0, structure)
require.NoError(t, err)
require.Equal(t, 0, len(extDD.Descriptor.FrameDependencies.FrameDiffs))
require.Equal(t, dd.RenderResolution{Width: 640, Height: 360}, *extDD.Descriptor.Resolution)
ReleaseExtDependencyDescriptor(extDD)
extDD, err = f.deltaFrame(1)
require.NoError(t, err)
require.Equal(t, []int{1}, extDD.Descriptor.FrameDependencies.FrameDiffs)
require.Equal(t, []int{1}, extDD.Descriptor.FrameDependencies.ChainDiffs)
require.Nil(t, extDD.Descriptor.AttachedStructure)
ReleaseExtDependencyDescriptor(extDD)
// the parsed template is a copy, the structure's template is untouched
require.Equal(t, []int{1}, structure.Templates[1].FrameDiffs)
// marshal the packets first, only Parse runs under the allocation count
bufs := make([][]byte, 128)
for i := range bufs {
ddVal := &dd.DependencyDescriptor{
FirstPacketInFrame: true,
LastPacketInFrame: true,
FrameNumber: uint16(i + 2),
FrameDependencies: structure.Templates[1],
}
bufs[i], err = (&dd.DependencyDescriptorExtension{Descriptor: ddVal, Structure: structure}).Marshal()
require.NoError(t, err)
}
pkt := &rtp.Packet{Header: rtp.Header{SequenceNumber: f.seq}}
require.NoError(t, pkt.SetExtension(ddTestExtID, bufs[0]))
i := 0
allocs := testing.AllocsPerRun(100, func() {
pkt.SequenceNumber++
require.NoError(t, pkt.SetExtension(ddTestExtID, bufs[i]))
i++
extDD, _, err := f.parser.Parse(pkt)
require.NoError(t, err)
ReleaseExtDependencyDescriptor(extDD)
})
require.Zero(t, allocs)
}