Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

btf: fix slow LoadKernelSpec by making Spec.Copy lazy #1235

Merged
merged 3 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
237 changes: 164 additions & 73 deletions btf/btf.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ var (
// ID represents the unique ID of a BTF object.
type ID = sys.BTFID

// Spec allows querying a set of Types and loading the set into the
// kernel.
type Spec struct {
// immutableTypes is a set of types which musn't be changed.
type immutableTypes struct {
// All types contained by the spec, not including types from the base in
// case the spec was parsed from split BTF.
types []Type
Expand All @@ -44,13 +43,132 @@ type Spec struct {

// Types indexed by essential name.
// Includes all struct flavors and types with the same name.
namedTypes map[essentialName][]Type
namedTypes map[essentialName][]TypeID

// Byte order of the types. This affects things like struct member order
// when using bitfields.
byteOrder binary.ByteOrder
}

func (s *immutableTypes) typeByID(id TypeID) (Type, bool) {
if id < s.firstTypeID {
return nil, false
}

index := int(id - s.firstTypeID)
if index >= len(s.types) {
return nil, false
}

return s.types[index], true
}

// mutableTypes is a set of types which may be changed.
type mutableTypes struct {
imm immutableTypes
copies map[Type]Type // map[orig]copy
copiedTypeIDs map[Type]TypeID //map[copy]origID
}

// add a type to the set of mutable types.
//
// Copies type and all of its children once. Repeated calls with the same type
// do not copy again.
func (mt *mutableTypes) add(typ Type, typeIDs map[Type]TypeID) Type {
return modifyGraphPreorder(typ, func(t Type) (Type, bool) {
cpy, ok := mt.copies[t]
if ok {
// This has been copied previously, no need to continue.
return cpy, false
}

cpy = t.copy()
mt.copies[t] = cpy

if id, ok := typeIDs[t]; ok {
mt.copiedTypeIDs[cpy] = id
}

// This is a new copy, keep copying children.
return cpy, true
})
}

// copy a set of mutable types.
func (mt *mutableTypes) copy() mutableTypes {
mtCopy := mutableTypes{
mt.imm,
make(map[Type]Type, len(mt.copies)),
make(map[Type]TypeID, len(mt.copiedTypeIDs)),
}

copies := make(map[Type]Type, len(mt.copies))
for orig, copy := range mt.copies {
// NB: We make a copy of copy, not orig, so that changes to mutable types
// are preserved.
copyOfCopy := mtCopy.add(copy, mt.copiedTypeIDs)
copies[orig] = copyOfCopy
}

// mtCopy.copies is currently map[copy]copyOfCopy, replace it with
// map[orig]copyOfCopy.
mtCopy.copies = copies
return mtCopy
}

func (mt *mutableTypes) typeID(typ Type) (TypeID, error) {
if _, ok := typ.(*Void); ok {
// Equality is weird for void, since it is a zero sized type.
return 0, nil
}

id, ok := mt.copiedTypeIDs[typ]
if !ok {
return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
}

return id, nil
}

func (mt *mutableTypes) typeByID(id TypeID) (Type, bool) {
immT, ok := mt.imm.typeByID(id)
if !ok {
return nil, false
}

return mt.add(immT, mt.imm.typeIDs), true
}

func (mt *mutableTypes) anyTypesByName(name string) ([]Type, error) {
immTypes := mt.imm.namedTypes[newEssentialName(name)]
if len(immTypes) == 0 {
return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
}

// Return a copy to prevent changes to namedTypes.
result := make([]Type, 0, len(immTypes))
for _, id := range immTypes {
immT, ok := mt.imm.typeByID(id)
if !ok {
return nil, fmt.Errorf("no type with ID %d", id)
}

// Match against the full name, not just the essential one
// in case the type being looked up is a struct flavor.
if immT.TypeName() == name {
result = append(result, mt.add(immT, mt.imm.typeIDs))
}
}
return result, nil
}

// Spec allows querying a set of Types and loading the set into the
// kernel.
type Spec struct {
mutableTypes

// String table from ELF.
strings *stringTable

// Byte order of the ELF we decoded the spec from, may be nil.
byteOrder binary.ByteOrder
}

// LoadSpec opens file and calls LoadSpecFromReader on it.
Expand Down Expand Up @@ -181,7 +299,7 @@ func loadSpecFromELF(file *internal.SafeELFFile) (*Spec, error) {
return nil, err
}

err = fixupDatasec(spec.types, sectionSizes, offsets)
err = fixupDatasec(spec.imm.types, sectionSizes, offsets)
if err != nil {
return nil, err
}
Expand All @@ -197,7 +315,7 @@ func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error
)

if base != nil {
if base.firstTypeID != 0 {
if base.imm.firstTypeID != 0 {
return nil, fmt.Errorf("can't use split BTF as base")
}

Expand All @@ -217,16 +335,22 @@ func loadRawSpec(btf io.ReaderAt, bo binary.ByteOrder, base *Spec) (*Spec, error
typeIDs, typesByName := indexTypes(types, firstTypeID)

return &Spec{
namedTypes: typesByName,
typeIDs: typeIDs,
types: types,
firstTypeID: firstTypeID,
strings: rawStrings,
byteOrder: bo,
mutableTypes{
immutableTypes{
types,
typeIDs,
firstTypeID,
typesByName,
bo,
},
make(map[Type]Type),
make(map[Type]TypeID),
},
rawStrings,
}, nil
}

func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentialName][]Type) {
func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentialName][]TypeID) {
namedTypes := 0
for _, typ := range types {
if typ.TypeName() != "" {
Expand All @@ -238,13 +362,15 @@ func indexTypes(types []Type, firstTypeID TypeID) (map[Type]TypeID, map[essentia
}

typeIDs := make(map[Type]TypeID, len(types))
typesByName := make(map[essentialName][]Type, namedTypes)
typesByName := make(map[essentialName][]TypeID, namedTypes)

for i, typ := range types {
id := firstTypeID + TypeID(i)
typeIDs[typ] = id

if name := newEssentialName(typ.TypeName()); name != "" {
typesByName[name] = append(typesByName[name], typ)
typesByName[name] = append(typesByName[name], id)
}
typeIDs[typ] = firstTypeID + TypeID(i)
}

return typeIDs, typesByName
Expand Down Expand Up @@ -492,17 +618,9 @@ func fixupDatasecLayout(ds *Datasec) error {

// Copy creates a copy of Spec.
func (s *Spec) Copy() *Spec {
types := copyTypes(s.types, nil)
typeIDs, typesByName := indexTypes(types, s.firstTypeID)

// NB: Other parts of spec are not copied since they are immutable.
return &Spec{
types,
typeIDs,
s.firstTypeID,
typesByName,
s.mutableTypes.copy(),
s.strings,
s.byteOrder,
}
}

Expand All @@ -519,8 +637,8 @@ func (sw sliceWriter) Write(p []byte) (int, error) {
// nextTypeID returns the next unallocated type ID or an error if there are no
// more type IDs.
func (s *Spec) nextTypeID() (TypeID, error) {
id := s.firstTypeID + TypeID(len(s.types))
if id < s.firstTypeID {
id := s.imm.firstTypeID + TypeID(len(s.imm.types))
if id < s.imm.firstTypeID {
return 0, fmt.Errorf("no more type IDs")
}
return id, nil
Expand All @@ -531,33 +649,19 @@ func (s *Spec) nextTypeID() (TypeID, error) {
// Returns an error wrapping ErrNotFound if a Type with the given ID
// does not exist in the Spec.
func (s *Spec) TypeByID(id TypeID) (Type, error) {
if id < s.firstTypeID {
return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s.firstTypeID, ErrNotFound)
}

index := int(id - s.firstTypeID)
if index >= len(s.types) {
return nil, fmt.Errorf("look up type with ID %d: %w", id, ErrNotFound)
typ, ok := s.typeByID(id)
if !ok {
return nil, fmt.Errorf("look up type with ID %d (first ID is %d): %w", id, s.imm.firstTypeID, ErrNotFound)
}

return s.types[index], nil
return typ, nil
}

// TypeID returns the ID for a given Type.
//
// Returns an error wrapping ErrNoFound if the type isn't part of the Spec.
func (s *Spec) TypeID(typ Type) (TypeID, error) {
if _, ok := typ.(*Void); ok {
// Equality is weird for void, since it is a zero sized type.
return 0, nil
}

id, ok := s.typeIDs[typ]
if !ok {
return 0, fmt.Errorf("no ID for type %s: %w", typ, ErrNotFound)
}

return id, nil
return s.mutableTypes.typeID(typ)
}

// AnyTypesByName returns a list of BTF Types with the given name.
Expand All @@ -568,21 +672,7 @@ func (s *Spec) TypeID(typ Type) (TypeID, error) {
//
// Returns an error wrapping ErrNotFound if no matching Type exists in the Spec.
func (s *Spec) AnyTypesByName(name string) ([]Type, error) {
types := s.namedTypes[newEssentialName(name)]
if len(types) == 0 {
return nil, fmt.Errorf("type name %s: %w", name, ErrNotFound)
}

// Return a copy to prevent changes to namedTypes.
result := make([]Type, 0, len(types))
for _, t := range types {
// Match against the full name, not just the essential one
// in case the type being looked up is a struct flavor.
if t.TypeName() == name {
result = append(result, t)
}
}
return result, nil
return s.mutableTypes.anyTypesByName(name)
}

// AnyTypeByName returns a Type with the given name.
Expand Down Expand Up @@ -671,26 +761,27 @@ func LoadSplitSpecFromReader(r io.ReaderAt, base *Spec) (*Spec, error) {

// TypesIterator iterates over types of a given spec.
type TypesIterator struct {
types []Type
index int
spec *Spec
id TypeID
done bool
// The last visited type in the spec.
Type Type
}

// Iterate returns the types iterator.
func (s *Spec) Iterate() *TypesIterator {
// We share the backing array of types with the Spec. This is safe since
// we don't allow deletion or shuffling of types.
return &TypesIterator{types: s.types, index: 0}
return &TypesIterator{spec: s, id: s.imm.firstTypeID}
}

// Next returns true as long as there are any remaining types.
func (iter *TypesIterator) Next() bool {
if len(iter.types) <= iter.index {
if iter.done {
return false
}

iter.Type = iter.types[iter.index]
iter.index++
return true
var ok bool
iter.Type, ok = iter.spec.typeByID(iter.id)
iter.id++
iter.done = !ok
return !iter.done
}
Loading
Loading