diff --git a/demo_packet.go b/demo_packet.go index 0d809a3..c9fb219 100644 --- a/demo_packet.go +++ b/demo_packet.go @@ -60,6 +60,13 @@ func (ms pendingMessages) Less(i, j int) bool { // multiple inner packets from a single CDemoPacket. This is the main structure // that contains all other data types in the demo file. func (p *Parser) onCDemoPacket(m *dota.CDemoPacket) error { + return p.processDemoPacket(m.GetData()) +} + +// processDemoPacket is the core of the CDemoPacket handler. It takes the raw +// packet payload so the fast envelope path (envelope_fast.go) can call it +// without materializing a proto message. +func (p *Parser) processDemoPacket(data []byte) error { // Reuse a parser-level buffer to store pending messages. Messages are read // first as pending messages then sorted before dispatch. onCDemoPacket is // never re-entrant (it is dispatched only via callByDemoType, never nested @@ -67,14 +74,32 @@ func (p *Parser) onCDemoPacket(m *dota.CDemoPacket) error { // and avoids a heap allocation per embedded message. ms := p.pendingMsgBuf[:0] + // The inner stream is bit-shifted after the leading 6-bit readUBitVar, so + // message bodies almost never sit on a byte boundary and must be copied out. + // Carve those copies from a single reused arena sized to the packet instead + // of allocating per message: the buffers only live until dispatch below (the + // protobuf unmarshal copies what it keeps), so reusing the arena across + // packets is safe for the same reason reusing pendingMsgBuf is. Message + // headers take space too, so the payload total always fits in len(data). + if cap(p.packetArena) < len(data) { + p.packetArena = make([]byte, 0, len(data)) + } + arena := p.packetArena[:0] + // Read all messages from the buffer. Messages are packed serially as // {type, size, data}. We keep reading until until less than a byte remains. - r := newReader(m.GetData()) + r := newReader(data) for r.remBytes() > 0 { t := int32(r.readUBitVar()) size := r.readVarUint32() - buf := r.readBytes(size) - ms = append(ms, pendingMessage{p.Tick, t, buf}) + start := len(arena) + end := start + int(size) + if end > cap(arena) { + _panicf("onCDemoPacket: message size %d exceeds packet buffer", size) + } + arena = arena[:end] + r.readBytesInto(arena[start:end]) + ms = append(ms, pendingMessage{p.Tick, t, arena[start:end:end]}) } // Sort messages to ensure dependencies are met. For example, we need to @@ -83,10 +108,12 @@ func (p *Parser) onCDemoPacket(m *dota.CDemoPacket) error { // and avoids the reflection allocations of sort.Sort's interface path. sort.Stable(ms) - // Dispatch messages in order, stopping on handler error. + // Dispatch messages in order, stopping on handler error. dispatchPacket + // takes the fast envelope path for hot internal-only message types and + // falls back to the full protobuf callback path otherwise. var err error for i := range ms { - if err = p.Callbacks.callByPacketType(ms[i].t, ms[i].buf); err != nil { + if err = p.dispatchPacket(ms[i].t, ms[i].buf); err != nil { break } } @@ -110,7 +137,7 @@ func (p *Parser) onCDemoFullPacket(m *dota.CDemoFullPacket) error { // Then the CDemoPacket. if m.Packet != nil { - if err := p.onCDemoPacket(m.GetPacket()); err != nil { + if err := p.processDemoPacket(m.GetPacket().GetData()); err != nil { return err } } diff --git a/entity.go b/entity.go index f8fdb30..5c7d6fb 100644 --- a/entity.go +++ b/entity.go @@ -176,6 +176,9 @@ func (e *Entity) GetIndex() int32 { // FindEntity finds a given Entity by index func (p *Parser) FindEntity(index int32) *Entity { + if index < 0 || int(index) >= len(p.entities) { + return nil + } return p.entities[index] } @@ -207,7 +210,7 @@ func (p *Parser) FindEntityByHandle(handle uint64) *Entity { func (p *Parser) FilterEntity(fb func(*Entity) bool) []*Entity { entities := make([]*Entity, 0, 0) for _, et := range p.entities { - if fb(et) { + if et != nil && fb(et) { entities = append(entities, et) } } @@ -223,18 +226,25 @@ type entityOpTuple struct { // Internal Callback for OnCSVCMsg_PacketEntities. func (p *Parser) onCSVCMsg_PacketEntities(m *dota.CSVCMsg_PacketEntities) error { + return p.processPacketEntities(m.GetEntityData(), m.GetUpdatedEntries(), m.GetLegacyIsDelta()) +} + +// processPacketEntities is the core of the PacketEntities handler. It takes +// the three envelope fields the parser actually uses so the fast envelope +// path (envelope_fast.go) can call it without materializing a proto message. +func (p *Parser) processPacketEntities(entityData []byte, updatedEntries int32, isDelta bool) error { r := &p.entityReader - r.reset(m.GetEntityData()) + r.reset(entityData) var index = int32(-1) - var updates = int(m.GetUpdatedEntries()) + var updates = int(updatedEntries) var cmd uint32 var classId int32 var serial int32 var e *Entity var op EntityOp - if !m.GetLegacyIsDelta() { + if !isDelta { if p.entityFullPackets > 0 { return nil } diff --git a/envelope_fast.go b/envelope_fast.go new file mode 100644 index 0000000..4c0bc89 --- /dev/null +++ b/envelope_fast.go @@ -0,0 +1,251 @@ +package manta + +import ( + "github.com/dotabuff/manta/dota" + "google.golang.org/protobuf/encoding/protowire" +) + +// Hand-rolled envelope decoders for the hottest internal messages. +// +// The reflective protobuf unmarshal allocates the message, a pointer per +// scalar field, and — dominating the profile — a fresh copy of every bytes +// field (CDemoPacket.data, CSVCMsg_PacketEntities.entity_data, +// CSVCMsg_UpdateStringTable.string_data effectively copy the whole replay a +// second time). The parser's internal handlers only need a few scalar fields +// plus the payload, and consume the payload synchronously, so when a message +// type has no user callbacks registered we can decode the envelope by hand +// with protowire and alias the payload instead of copying it. +// +// Gating: NewParser registers exactly one internal handler per relevant list +// before returning, and users can only register afterwards, so +// len(list) == 1 means "internal handler only". Any user registration makes +// the list longer and permanently reverts that type to the full protobuf +// path, where the user-visible message owns its own copies as before. +// +// Aliasing lifetimes: entity_data and string_data alias the demo-packet arena +// (p.packetArena), which is stable for the duration of the packet's dispatch +// loop; both are fully consumed before the handler returns (string-table +// values are copied out by readBitsAsBytes/readString). CDemoPacket.data +// aliases the outer-message buffer, which is stable until the next +// readOuterMessage; processDemoPacket copies message bodies into the arena +// before dispatching. + +// dispatchDemo routes an outer demo message, taking the fast envelope path +// for CDemoPacket/CDemoSignonPacket when only the internal handler is +// registered. +func (p *Parser) dispatchDemo(t int32, buf []byte) error { + switch t { + case int32(dota.EDemoCommands_DEM_Packet): + if len(p.Callbacks.onCDemoPacket) == 1 { + return p.fastDemoPacket(buf) + } + case int32(dota.EDemoCommands_DEM_SignonPacket): + if len(p.Callbacks.onCDemoSignonPacket) == 1 { + return p.fastDemoPacket(buf) + } + } + return p.Callbacks.callByDemoType(t, buf) +} + +// dispatchPacket routes an embedded packet message, taking the fast envelope +// path for the hot types when only the internal handler is registered. +func (p *Parser) dispatchPacket(t int32, buf []byte) error { + switch t { + case int32(dota.NET_Messages_net_Tick): + if len(p.Callbacks.onCNETMsg_Tick) == 1 { + return p.fastNetTick(buf) + } + case int32(dota.SVC_Messages_svc_UpdateStringTable): + if len(p.Callbacks.onCSVCMsg_UpdateStringTable) == 1 { + return p.fastUpdateStringTable(buf) + } + case int32(dota.SVC_Messages_svc_PacketEntities): + if len(p.Callbacks.onCSVCMsg_PacketEntities) == 1 { + return p.fastPacketEntities(buf) + } + case int32(dota.EBaseGameEvents_GE_Source1LegacyGameEvent): + if len(p.Callbacks.onCMsgSource1LegacyGameEvent) == 1 && p.skipGameEvent(buf) { + return nil + } + } + return p.Callbacks.callByPacketType(t, buf) +} + +// skipGameEvent peeks the eventid (field 2, varint) of a +// CMsgSource1LegacyGameEvent and reports whether the event is a known type +// with no registered handlers, in which case the full unmarshal (a message +// plus a reflect.New per key) can be skipped. The internal handler returns +// nil for exactly that case; unknown event ids fall back to the full path so +// its error behaviour is preserved. +func (p *Parser) skipGameEvent(buf []byte) bool { + for len(buf) > 0 { + num, typ, n := protowire.ConsumeTag(buf) + if n < 0 { + return false + } + buf = buf[n:] + if num == 2 && typ == protowire.VarintType { + v, n := protowire.ConsumeVarint(buf) + if n < 0 { + return false + } + name, ok := p.gameEventNames[int32(v)] + if !ok { + return false + } + return p.gameEventHandlers[name] == nil + } + n = protowire.ConsumeFieldValue(num, typ, buf) + if n < 0 { + return false + } + buf = buf[n:] + } + return false +} + +// fastDemoPacket decodes the CDemoPacket envelope: data = field 3 (bytes). +func (p *Parser) fastDemoPacket(buf []byte) error { + var data []byte + for len(buf) > 0 { + num, typ, n := protowire.ConsumeTag(buf) + if n < 0 { + return _errorf("fastDemoPacket: invalid tag") + } + buf = buf[n:] + if num == 3 && typ == protowire.BytesType { + v, n := protowire.ConsumeBytes(buf) + if n < 0 { + return _errorf("fastDemoPacket: invalid data field") + } + data = v + buf = buf[n:] + continue + } + n = protowire.ConsumeFieldValue(num, typ, buf) + if n < 0 { + return _errorf("fastDemoPacket: invalid field %d", num) + } + buf = buf[n:] + } + return p.processDemoPacket(data) +} + +// fastNetTick decodes the CNETMsg_Tick envelope: tick = field 1 (varint). +func (p *Parser) fastNetTick(buf []byte) error { + var tick uint64 + for len(buf) > 0 { + num, typ, n := protowire.ConsumeTag(buf) + if n < 0 { + return _errorf("fastNetTick: invalid tag") + } + buf = buf[n:] + if num == 1 && typ == protowire.VarintType { + v, n := protowire.ConsumeVarint(buf) + if n < 0 { + return _errorf("fastNetTick: invalid tick field") + } + tick = v + buf = buf[n:] + continue + } + n = protowire.ConsumeFieldValue(num, typ, buf) + if n < 0 { + return _errorf("fastNetTick: invalid field %d", num) + } + buf = buf[n:] + } + p.NetTick = uint32(tick) + return nil +} + +// fastUpdateStringTable decodes the CSVCMsg_UpdateStringTable envelope: +// table_id = 1 (varint), num_changed_entries = 2 (varint), +// string_data = 3 (bytes). +func (p *Parser) fastUpdateStringTable(buf []byte) error { + var tableId, numChanged int32 + var data []byte + for len(buf) > 0 { + num, typ, n := protowire.ConsumeTag(buf) + if n < 0 { + return _errorf("fastUpdateStringTable: invalid tag") + } + buf = buf[n:] + switch { + case num == 1 && typ == protowire.VarintType: + v, n := protowire.ConsumeVarint(buf) + if n < 0 { + return _errorf("fastUpdateStringTable: invalid table_id") + } + tableId = int32(v) + buf = buf[n:] + case num == 2 && typ == protowire.VarintType: + v, n := protowire.ConsumeVarint(buf) + if n < 0 { + return _errorf("fastUpdateStringTable: invalid num_changed_entries") + } + numChanged = int32(v) + buf = buf[n:] + case num == 3 && typ == protowire.BytesType: + v, n := protowire.ConsumeBytes(buf) + if n < 0 { + return _errorf("fastUpdateStringTable: invalid string_data") + } + data = v + buf = buf[n:] + default: + n = protowire.ConsumeFieldValue(num, typ, buf) + if n < 0 { + return _errorf("fastUpdateStringTable: invalid field %d", num) + } + buf = buf[n:] + } + } + return p.processUpdateStringTable(tableId, numChanged, data) +} + +// fastPacketEntities decodes the CSVCMsg_PacketEntities envelope: +// updated_entries = 2 (varint), legacy_is_delta = 3 (varint bool), +// entity_data = 7 (bytes). +func (p *Parser) fastPacketEntities(buf []byte) error { + var updatedEntries int32 + var isDelta bool + var entityData []byte + for len(buf) > 0 { + num, typ, n := protowire.ConsumeTag(buf) + if n < 0 { + return _errorf("fastPacketEntities: invalid tag") + } + buf = buf[n:] + switch { + case num == 2 && typ == protowire.VarintType: + v, n := protowire.ConsumeVarint(buf) + if n < 0 { + return _errorf("fastPacketEntities: invalid updated_entries") + } + updatedEntries = int32(v) + buf = buf[n:] + case num == 3 && typ == protowire.VarintType: + v, n := protowire.ConsumeVarint(buf) + if n < 0 { + return _errorf("fastPacketEntities: invalid legacy_is_delta") + } + isDelta = v != 0 + buf = buf[n:] + case num == 7 && typ == protowire.BytesType: + v, n := protowire.ConsumeBytes(buf) + if n < 0 { + return _errorf("fastPacketEntities: invalid entity_data") + } + entityData = v + buf = buf[n:] + default: + n = protowire.ConsumeFieldValue(num, typ, buf) + if n < 0 { + return _errorf("fastPacketEntities: invalid field %d", num) + } + buf = buf[n:] + } + } + return p.processPacketEntities(entityData, updatedEntries, isDelta) +} diff --git a/field_state.go b/field_state.go index 5a5e830..854d5f6 100644 --- a/field_state.go +++ b/field_state.go @@ -142,20 +142,63 @@ func (s *fieldState) set(fp *fieldPath, c cell) { // []float32 and binary-blob []byte) are also deep-copied, since a caller may // mutate a slice returned from Entity.Get/Map; strings and boxed integers are // immutable and shared by value. +// +// The copy is carved from two slab allocations (one []fieldState, one []cell) +// sized by a counting pre-pass, instead of allocating a struct and a cell +// array per nested state. Cell slices are cap-limited to their exact length, +// so a later grow in set() allocates a fresh array rather than stomping a +// neighbouring state's arena region. An entity's clone shares its slabs with +// no other entity, so the slabs die with the entity. func (s *fieldState) clone() *fieldState { - c := &fieldState{state: make([]cell, len(s.state))} - copy(c.state, s.state) - for i := range c.state { - switch v := c.state[i].ref.(type) { + nStates, nCells := s.cloneSize() + a := cloneArena{ + states: make([]fieldState, nStates), + cells: make([]cell, nCells), + } + root := &a.states[0] + a.states = a.states[1:] + s.cloneInto(root, &a) + return root +} + +// cloneArena holds the remaining unassigned slab space during a clone. +type cloneArena struct { + states []fieldState + cells []cell +} + +// cloneSize counts the nested states and total cells reachable from s. +func (s *fieldState) cloneSize() (nStates, nCells int) { + nStates, nCells = 1, len(s.state) + for i := range s.state { + if sub, ok := s.state[i].sub(); ok { + cs, cc := sub.cloneSize() + nStates += cs + nCells += cc + } + } + return +} + +// cloneInto deep-copies s into dst, carving all nested storage from the arena. +func (s *fieldState) cloneInto(dst *fieldState, a *cloneArena) { + n := len(s.state) + dst.state = a.cells[:n:n] + a.cells = a.cells[n:] + copy(dst.state, s.state) + for i := range dst.state { + switch v := dst.state[i].ref.(type) { case *fieldState: - c.state[i].ref = v.clone() + sub := &a.states[0] + a.states = a.states[1:] + v.cloneInto(sub, a) + dst.state[i].ref = sub case []float32: - c.state[i].ref = append([]float32(nil), v...) + dst.state[i].ref = append([]float32(nil), v...) case []byte: - c.state[i].ref = append([]byte(nil), v...) + dst.state[i].ref = append([]byte(nil), v...) } } - return c } func max(a, b int) int { diff --git a/parser.go b/parser.go index 2fdd228..3bd2ae2 100644 --- a/parser.go +++ b/parser.go @@ -42,7 +42,7 @@ type Parser struct { classesByName map[string]*class classIdSize uint32 classInfo bool - entities map[int32]*Entity + entities []*Entity entityFullPackets int entityHandlers []EntityHandler gameEventHandlers map[string][]GameEventHandler @@ -52,6 +52,7 @@ type Parser struct { modifierTableEntryHandlers []ModifierTableEntryHandler serializers map[string]*serializer pendingMsgBuf pendingMessages + packetArena []byte snappyScratch []byte entityReader reader entityTuples []entityOpTuple @@ -80,7 +81,7 @@ func NewStreamParser(r io.Reader) (*Parser, error) { classBaselineStates: make(map[int32]*fieldState), classesById: make(map[int32]*class), classesByName: make(map[string]*class), - entities: make(map[int32]*Entity), + entities: make([]*Entity, 1< r.size { + _panicf("readBytesInto: insufficient buffer (%d of %d)", r.pos, r.size) + } + copy(dst, r.buf[r.pos-n:r.pos]) + return + } + + i := 0 + for int(n)-i >= 4 { + binary.LittleEndian.PutUint32(dst[i:], r.readBits(32)) + i += 4 + } + for ; i < int(n); i++ { + dst[i] = byte(r.readBits(8)) + } +} + // readLeUint32 reads an little-endian uint32 func (r *reader) readLeUint32() uint32 { + // Unaligned: read straight from the bit accumulator. Bits are consumed + // LSB-first from little-endian bytes, so this equals the LE decode of the + // next four stream bytes without the readBytes slow-path allocation. The + // aligned path keeps the zero-copy readBytes fast path. + if r.bitCount&7 != 0 { + return r.readBits(32) + } return binary.LittleEndian.Uint32(r.readBytes(4)) } // readLeUint64 reads a little-endian uint64 func (r *reader) readLeUint64() uint64 { + // See readLeUint32: two accumulator words replace the unaligned readBytes + // allocation (readBits is capped at 32 bits per call). + if r.bitCount&7 != 0 { + lo := uint64(r.readBits(32)) + return lo | uint64(r.readBits(32))<<32 + } return binary.LittleEndian.Uint64(r.readBytes(8)) } @@ -297,7 +337,9 @@ func (r *reader) readStringN(n uint32) string { // readString reads a null terminated string func (r *reader) readString() string { - buf := make([]byte, 0) + // Most strings here are short field/table keys; a small starting capacity + // avoids the repeated growth of a cap-0 append chain. + buf := make([]byte, 0, 32) for { b := r.readByte() if b == 0 { @@ -390,13 +432,23 @@ func (r *reader) read3BitNormal() []float32 { // readBitsAsBytes reads the given number of bits in groups of bytes func (r *reader) readBitsAsBytes(n uint32) []byte { - tmp := make([]byte, 0) + // Allocate the exact result size up front (the caller retains the slice, so + // a fresh allocation is required) and fill a 32-bit word at a time instead + // of growing a cap-0 slice byte by byte. + tmp := make([]byte, (n+7)/8) + i := 0 + for n >= 32 { + binary.LittleEndian.PutUint32(tmp[i:], r.readBits(32)) + i += 4 + n -= 32 + } for n >= 8 { - tmp = append(tmp, r.readByte()) + tmp[i] = r.readByte() + i++ n -= 8 } if n > 0 { - tmp = append(tmp, byte(r.readBits(n))) + tmp[i] = byte(r.readBits(n)) } return tmp } diff --git a/string_table.go b/string_table.go index 6d6a5c4..f8378a7 100644 --- a/string_table.go +++ b/string_table.go @@ -136,34 +136,41 @@ func (p *Parser) onCSVCMsg_CreateStringTable(m *dota.CSVCMsg_CreateStringTable) // Internal callback for CSVCMsg_UpdateStringTable. func (p *Parser) onCSVCMsg_UpdateStringTable(m *dota.CSVCMsg_UpdateStringTable) error { + return p.processUpdateStringTable(m.GetTableId(), m.GetNumChangedEntries(), m.GetStringData()) +} + +// processUpdateStringTable is the core of the UpdateStringTable handler. It +// takes the three envelope fields the parser actually uses so the fast +// envelope path (envelope_fast.go) can call it without materializing a proto +// message. +func (p *Parser) processUpdateStringTable(tableId, numChanged int32, data []byte) error { // TODO: integrate - t, ok := p.stringTables.Tables[m.GetTableId()] + t, ok := p.stringTables.Tables[tableId] if !ok { - _panicf("missing string table %d", m.GetTableId()) + _panicf("missing string table %d", tableId) } if v(5) { - _debugf("tick=%d name=%s changedEntries=%d size=%d", p.Tick, t.name, m.GetNumChangedEntries(), len(m.GetStringData())) + _debugf("tick=%d name=%s changedEntries=%d size=%d", p.Tick, t.name, numChanged, len(data)) } // Parse the updates out of the string table data - items, err := parseStringTable(m.GetStringData(), m.GetNumChangedEntries(), t.name, t.userDataFixedSize, t.userDataSizeBits, t.flags, t.varintBitCounts) + items, err := parseStringTable(data, numChanged, t.name, t.userDataFixedSize, t.userDataSizeBits, t.flags, t.varintBitCounts) if err != nil { return err } // Apply the updates to the parser state for _, item := range items { - index := item.Index - if _, ok := t.Items[index]; ok { - if item.Key != "" && item.Key != t.Items[index].Key { - t.Items[index].Key = item.Key + if existing, ok := t.Items[item.Index]; ok { + if item.Key != "" && item.Key != existing.Key { + existing.Key = item.Key } if len(item.Value) > 0 { - t.Items[index].Value = item.Value + existing.Value = item.Value } } else { - t.Items[index] = item + t.Items[item.Index] = item } } @@ -193,7 +200,16 @@ func parseStringTable(buf []byte, numUpdates int32, name string, userDataFixed b } }() - items = make([]*stringTableItem, 0) + // Preallocate the result and back the items with a single slab so a large + // update costs two allocations instead of one per item plus append growth. + // The cap is bounded so a corrupt numUpdates cannot trigger a huge + // allocation; append simply grows past it if a table is legitimately larger. + prealloc := int(numUpdates) + if prealloc > 4096 { + prealloc = 4096 + } + items = make([]*stringTableItem, 0, prealloc) + slab := make([]stringTableItem, 0, prealloc) // Create a reader for the buffer r := newReader(buf) @@ -202,8 +218,13 @@ func parseStringTable(buf []byte, numUpdates int32, name string, userDataFixed b // If the first item is at index 0 it will use a incr operation. index := int32(-1) - // Maintain a list of key history - keys := make([]string, 0, stringtableKeyHistorySize) + // Maintain a ring buffer of key history. histCount is the total number of + // keys ever appended; the ring holds the most recent + // stringtableKeyHistorySize of them, so history position pos (counted from + // the oldest retained key) lives at (histCount-size+pos) mod size once the + // ring is full, and at pos before that. + var keyHist [stringtableKeyHistorySize]string + histCount := 0 // Some tables have no data if len(buf) == 0 { @@ -252,10 +273,18 @@ func parseStringTable(buf []byte, numUpdates int32, name string, userDataFixed b pos := r.readBits(5) size := r.readBits(5) - if int(pos) >= len(keys) { + retained := histCount + if retained > stringtableKeyHistorySize { + retained = stringtableKeyHistorySize + } + if int(pos) >= retained { key += r.readString() } else { - s := keys[pos] + abs := int(pos) + if histCount > stringtableKeyHistorySize { + abs = histCount - stringtableKeyHistorySize + int(pos) + } + s := keyHist[abs%stringtableKeyHistorySize] if int(size) > len(s) { key += s + r.readString() } else { @@ -266,12 +295,8 @@ func parseStringTable(buf []byte, numUpdates int32, name string, userDataFixed b key = r.readString() } - if len(keys) >= stringtableKeyHistorySize { - copy(keys[0:], keys[1:]) - keys[len(keys)-1] = "" - keys = keys[:len(keys)-1] - } - keys = append(keys, key) + keyHist[histCount%stringtableKeyHistorySize] = key + histCount++ } // Some entries have a value. @@ -302,7 +327,12 @@ func parseStringTable(buf []byte, numUpdates int32, name string, userDataFixed b } } - items = append(items, &stringTableItem{index, key, value}) + // Append to the slab and take the address of the slab element. If the + // slab regrows, previously taken pointers keep referencing the old + // backing array; that is safe because items are only ever read and + // mutated through those pointers, never through the slab again. + slab = append(slab, stringTableItem{index, key, value}) + items = append(items, &slab[len(slab)-1]) } return items, nil