From b5aa4e3be9a6eb2bd3817653e8d69e815db32827 Mon Sep 17 00:00:00 2001 From: Hikmatulloh Hari Mukti Date: Mon, 25 Dec 2023 10:47:54 +0700 Subject: [PATCH] perf: simplify encoder's lru --- encoder/encoder.go | 139 +++----------- encoder/encoder_test.go | 408 ++++++++++++++++++++++------------------ encoder/lru.go | 72 +++++++ encoder/lru_test.go | 90 +++++++++ 4 files changed, 410 insertions(+), 299 deletions(-) create mode 100644 encoder/lru.go create mode 100644 encoder/lru_test.go diff --git a/encoder/encoder.go b/encoder/encoder.go index 58e78a91..484fd260 100644 --- a/encoder/encoder.go +++ b/encoder/encoder.go @@ -6,7 +6,6 @@ package encoder import ( "bytes" - "container/list" "context" "encoding/binary" "errors" @@ -20,7 +19,6 @@ import ( "github.com/muktihari/fit/profile/untyped/fieldnum" "github.com/muktihari/fit/profile/untyped/mesgnum" "github.com/muktihari/fit/proto" - "golang.org/x/exp/slices" ) var ( @@ -70,15 +68,7 @@ type Encoder struct { dataSize uint32 // Data size of messages in bytes for a single Fit file. crc16 hash.Hash16 // Calculate the CRC-16 checksum for ensuring header and message integrity. - // Tracks whether a message definition has been previously written. - // The list size is determined by the number of desirable multiple local message types specified by WithNormalHeader(n). - // Default size is 1 to accommodate local message type zero (0). - localMesgDefinitions *list.List - - // Tracks the Least Recently Used (LRU) element in the localMesgDefinitions list. - // Each element in this list is a pointer to an element in localMesgDefinitions. - // This helps determine which element should be removed when localMesgDefinitions is full. - localMesgDefinitionsLRU *list.List + localMesgNumLRU *lru // LRU cache for writing local message definition // This timestamp reference is retrieved from the first message containing a valid timestamp field. // It is initialized only if the 'compressedTimestamp' option is applied and reset when decoding is completed. @@ -185,14 +175,18 @@ func New(w io.Writer, opts ...Option) *Encoder { opt.apply(options) } + var lruCapacity byte = 1 + if options.headerOption == headerOptionNormal && options.multipleLocalMessageType > 0 { + lruCapacity = options.multipleLocalMessageType + 1 + } + return &Encoder{ - w: w, - options: options, - crc16: crc16.New(crc16.MakeFitTable()), - protocolValidator: proto.NewValidator(options.protocolVersion), - messageValidator: options.messageValidator, - localMesgDefinitions: list.New(), - localMesgDefinitionsLRU: list.New(), + w: w, + options: options, + crc16: crc16.New(crc16.MakeFitTable()), + protocolValidator: proto.NewValidator(options.protocolVersion), + messageValidator: options.messageValidator, + localMesgNumLRU: newLRU(lruCapacity), defaultFileHeader: proto.FileHeader{ Size: proto.DefaultFileHeaderSize, ProtocolVersion: byte(options.protocolVersion), @@ -356,17 +350,14 @@ func (e *Encoder) encodeHeader(header *proto.FileHeader) error { header.ProtocolVersion = byte(e.options.protocolVersion) b, _ := header.MarshalBinary() - if header.Size == 12 { + if header.Size < 14 { n, err := e.w.Write(b[:header.Size]) e.n += int64(n) return err } _, _ = e.crc16.Write(b[:header.Size-2]) - - var crc = make([]byte, 2) - binary.LittleEndian.PutUint16(crc, e.crc16.Sum16()) - copy(b[header.Size-2:], crc) + binary.LittleEndian.PutUint16(b[header.Size-2:], e.crc16.Sum16()) header.CRC = e.crc16.Sum16() e.crc16.Reset() // this hash will be re-used for calculating data integrity. @@ -405,9 +396,7 @@ func (e *Encoder) encodeMessage(w io.Writer, mesg *proto.Message) error { return fmt.Errorf("message validation failed: %w", err) } - var b []byte - var localMesgNum byte - var writeable bool + var isNewMesgDef bool e.buf.Reset() // Writing strategy based on the selected header option: @@ -419,31 +408,27 @@ func (e *Encoder) encodeMessage(w io.Writer, mesg *proto.Message) error { } _, _ = mesgDef.WriteTo(e.buf) - b = e.buf.Bytes() // Should be copied before put on LRU. - if e.options.multipleLocalMessageType == 0 { - writeable = e.isMesgDefinitionWriteable(b) // Local Message Type Zero - } else { - localMesgNum, writeable = e.redefineLocalMesgNum(b) // Multiple Local Message Type - b[0] = (b[0] &^ proto.LocalMesgNumMask) | localMesgNum // localMesgNum redefined, update the message definition header. - } + b := e.buf.Bytes() + + var localMesgNum byte + localMesgNum, isNewMesgDef = e.localMesgNumLRU.Put(b) + b[0] = (b[0] &^ proto.LocalMesgNumMask) | localMesgNum // update the message definition header. mesg.Header = (mesg.Header &^ proto.LocalMesgNumMask) | localMesgNum case headerOptionCompressedTimestamp: e.compressTimestampIntoHeader(mesg) - // Timestamp field might be omitted, update the message definition to match the resulting message. mesgDef := proto.CreateMessageDefinition(mesg) if err := e.protocolValidator.ValidateMessageDefinition(&mesgDef); err != nil { return err } - _, _ = mesgDef.WriteTo(e.buf) - b = e.buf.Bytes() // Should be copied before putting it to LRU. - writeable = e.isMesgDefinitionWriteable(b) + _, _ = mesgDef.WriteTo(e.buf) + _, isNewMesgDef = e.localMesgNumLRU.Put(e.buf.Bytes()) } - if writeable { - if err := e.writeMessage(w, b); err != nil { + if isNewMesgDef { + if err := e.writeMessage(w, e.buf.Bytes()); err != nil { return fmt.Errorf("write message definition failed: %w", err) } } @@ -453,69 +438,12 @@ func (e *Encoder) encodeMessage(w io.Writer, mesg *proto.Message) error { if err != nil { return fmt.Errorf("marshal failed: %w", err) } - b = e.buf.Bytes() - if err := e.writeMessage(w, b); err != nil { + if err := e.writeMessage(w, e.buf.Bytes()); err != nil { return fmt.Errorf("write message failed: %w", err) } return nil } -func (e *Encoder) isMesgDefinitionWriteable(b []byte) bool { - if e.localMesgDefinitions.Len() == 0 { - e.localMesgDefinitions.PushFront(slices.Clone(b)) - return true - } - - if isEqual(e.localMesgDefinitions.Front().Value.([]byte), b) { - return false - } - - e.localMesgDefinitions.Remove(e.localMesgDefinitions.Front()) - e.localMesgDefinitions.PushFront(slices.Clone(b)) - - return true -} - -func (e *Encoder) redefineLocalMesgNum(b []byte) (newLocalMesgNum byte, writeable bool) { - max := e.options.multipleLocalMessageType - var index byte = 0 - for elem := e.localMesgDefinitions.Front(); elem != nil; elem = elem.Next() { - val := elem.Value.([]byte) - if isEqual(val[1:], b[1:]) { // ignore header since localMesgNum in header is everchanging. - e.localMesgDefinitionsLRU.MoveToBack(elem) // Recently used items is moved to the back. - return index, false - } - index++ - } - - b = slices.Clone(b) - - index = byte(e.localMesgDefinitions.Len()) - if byte(e.localMesgDefinitions.Len()) <= max { - e.localMesgDefinitionsLRU.PushBack(e.localMesgDefinitions.PushBack(b)) - return index, true - } - - index = 0 - for elem := e.localMesgDefinitions.Front(); elem != nil; elem = elem.Next() { - if elem == e.localMesgDefinitionsLRU.Front().Value.(*list.Element) { - break - } - index++ - } - index %= max + 1 // overflowing index - - // Remove Least Recently Used Item from the List. - lruElemValue := e.localMesgDefinitionsLRU.Front().Value.(*list.Element) - elem := e.localMesgDefinitions.InsertAfter(b, lruElemValue) - e.localMesgDefinitions.Remove(lruElemValue) - - e.localMesgDefinitionsLRU.Remove(e.localMesgDefinitionsLRU.Front()) - e.localMesgDefinitionsLRU.PushBack(elem) - - return index, true -} - func (e *Encoder) compressTimestampIntoHeader(mesg *proto.Message) { field := mesg.FieldByNum(fieldnum.TimestampCorrelationTimestamp) if field == nil { @@ -587,21 +515,6 @@ func (e *Encoder) encodeCRC(crc *uint16) error { func (e *Encoder) reset() { e.dataSize = 0 e.crc16.Reset() - e.localMesgDefinitions = list.New() - e.localMesgDefinitionsLRU = list.New() + e.localMesgNumLRU.Reset() e.timestampReference = 0 } - -// List of private functions should be declared after methods: - -func isEqual(x, y []byte) bool { - if len(x) != len(y) { - return false - } - for i := range x { - if x[i] != y[i] { - return false - } - } - return true -} diff --git a/encoder/encoder_test.go b/encoder/encoder_test.go index a656a5c3..84673f8b 100644 --- a/encoder/encoder_test.go +++ b/encoder/encoder_test.go @@ -6,7 +6,6 @@ package encoder import ( "bytes" - "container/list" "context" "encoding/binary" "errors" @@ -712,7 +711,7 @@ func TestEncodeMessageWithMultipleLocalMessageType(t *testing.T) { mesgs[i] = mesgs[i].Clone() } - enc := New(nil, WithNormalHeader(3)) + enc := New(nil, WithNormalHeader(2)) for i, mesg := range mesgs { w := new(bytes.Buffer) err := enc.encodeMessage(w, &mesg) @@ -723,9 +722,23 @@ func TestEncodeMessageWithMultipleLocalMessageType(t *testing.T) { mesgDefHeader := w.Bytes() expectedHeader := (mesgDefHeader[0] &^ proto.LocalMesgNumMask) | byte(i) if mesgDefHeader[0] != expectedHeader { - t.Fatalf("expected 0b%08b, got: 0b%08b", expectedHeader, mesgDefHeader[0]) + t.Fatalf("[%d] expected 0b%08b, got: 0b%08b", i, expectedHeader, mesgDefHeader[0]) } } + + // add 4th mesg, header should be 0, reset. + mesg := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ + fieldnum.RecordTimestamp: datetime.ToUint32(now), + }) + w := new(bytes.Buffer) + if err := enc.encodeMessage(w, &mesg); err != nil { + t.Fatal(err) + } + mesgDefHeader := w.Bytes() + expectedHeader := byte(0) + if mesgDefHeader[0] != expectedHeader { + t.Fatalf("expected 0b%08b, got: 0b%08b", expectedHeader, mesgDefHeader[0]) + } }) } @@ -774,153 +787,153 @@ func TestEncodeMessages(t *testing.T) { } } -func TestRedefineLocalMesgNum(t *testing.T) { - type _struct struct { - name string - b []byte - listCapacity byte - list *list.List - lru *list.List - num byte - writtable bool - } - - tt := []_struct{ - { - name: "init value", - b: []byte{0, 1}, - list: list.New(), - lru: list.New(), - num: 0, - writtable: true, - }, - { - name: "eq with 1st index", - b: []byte{0, 1}, - list: func() *list.List { - l := list.New() - l.PushFront([]byte{0, 1}) - return l - }(), - lru: list.New(), - num: 0, - writtable: false, - }, - { - name: "eq with 2st index", - b: []byte{0, 2}, - list: func() *list.List { - l := list.New() - l.PushBack([]byte{0, 1}) - l.PushBack([]byte{0, 2}) - return l - }(), - lru: list.New(), - num: 1, - writtable: false, - }, - func() _struct { - ls := list.New() - lru := list.New() - lru.PushBack(ls.PushBack([]byte{0, 1})) - lru.PushBack(ls.PushBack([]byte{0, 2})) - - return _struct{ - name: "full, replace LRU item on the first index", - b: []byte{0, 3}, - listCapacity: 1, - list: ls, - lru: lru, - num: 0, - writtable: true, - } - }(), - func() _struct { - ls, lru := list.New(), list.New() - - lru.PushBack(ls.PushBack([]byte{0, 1})) - lru.PushBack(ls.PushBack([]byte{0, 2})) - lru.PushBack(ls.PushBack([]byte{0, 3})) - - lru.MoveToBack(lru.Front()) - - return _struct{ - name: "full, replace LRU item on 2nd index", - b: []byte{0, 4}, - listCapacity: 2, - list: ls, - lru: lru, - num: 1, - writtable: true, - } - }(), - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - enc := New(nil) - enc.options.multipleLocalMessageType = tc.listCapacity - enc.localMesgDefinitions = tc.list - enc.localMesgDefinitionsLRU = tc.lru - - num, writtable := enc.redefineLocalMesgNum(tc.b) - if num != tc.num { - t.Fatalf("expected: %d, got: %d", tc.num, num) - } - if writtable != tc.writtable { - t.Fatalf("expected: %t, got: %t", tc.writtable, writtable) - } - - }) - } -} - -func TestIsMesgDefinitionWriteable(t *testing.T) { - tt := []struct { - name string - b []byte - list *list.List - writeable bool - }{ - { - name: "init element value", - b: []byte{1, 1}, - list: list.New(), - writeable: true, - }, - { - name: "eq with 1st element value", - b: []byte{1, 1}, - list: func() *list.List { - ls := list.New() - ls.PushFront([]byte{1, 1}) - return ls - }(), - writeable: false, - }, - { - name: "not eq, replace existing", - b: []byte{1, 1}, - list: func() *list.List { - ls := list.New() - ls.PushFront([]byte{0, 1}) - return ls - }(), - writeable: true, - }, - } - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - enc := New(nil) - enc.localMesgDefinitions = tc.list - - writeable := enc.isMesgDefinitionWriteable(tc.b) - if writeable != tc.writeable { - t.Fatalf("expected: %t, got: %t", tc.writeable, writeable) - } - }) - } -} +// func TestRedefineLocalMesgNum(t *testing.T) { +// type _struct struct { +// name string +// b []byte +// listCapacity byte +// list *list.List +// lru *list.List +// num byte +// writtable bool +// } + +// tt := []_struct{ +// { +// name: "init value", +// b: []byte{0, 1}, +// list: list.New(), +// lru: list.New(), +// num: 0, +// writtable: true, +// }, +// { +// name: "eq with 1st index", +// b: []byte{0, 1}, +// list: func() *list.List { +// l := list.New() +// l.PushFront([]byte{0, 1}) +// return l +// }(), +// lru: list.New(), +// num: 0, +// writtable: false, +// }, +// { +// name: "eq with 2st index", +// b: []byte{0, 2}, +// list: func() *list.List { +// l := list.New() +// l.PushBack([]byte{0, 1}) +// l.PushBack([]byte{0, 2}) +// return l +// }(), +// lru: list.New(), +// num: 1, +// writtable: false, +// }, +// func() _struct { +// ls := list.New() +// lru := list.New() +// lru.PushBack(ls.PushBack([]byte{0, 1})) +// lru.PushBack(ls.PushBack([]byte{0, 2})) + +// return _struct{ +// name: "full, replace LRU item on the first index", +// b: []byte{0, 3}, +// listCapacity: 1, +// list: ls, +// lru: lru, +// num: 0, +// writtable: true, +// } +// }(), +// func() _struct { +// ls, lru := list.New(), list.New() + +// lru.PushBack(ls.PushBack([]byte{0, 1})) +// lru.PushBack(ls.PushBack([]byte{0, 2})) +// lru.PushBack(ls.PushBack([]byte{0, 3})) + +// lru.MoveToBack(lru.Front()) + +// return _struct{ +// name: "full, replace LRU item on 2nd index", +// b: []byte{0, 4}, +// listCapacity: 2, +// list: ls, +// lru: lru, +// num: 1, +// writtable: true, +// } +// }(), +// } + +// for _, tc := range tt { +// t.Run(tc.name, func(t *testing.T) { +// enc := New(nil) +// enc.options.multipleLocalMessageType = tc.listCapacity +// enc.localMesgDefinitions = tc.list +// enc.localMesgDefinitionsLRU = tc.lru + +// num, writtable := enc.redefineLocalMesgNum(tc.b) +// if num != tc.num { +// t.Fatalf("expected: %d, got: %d", tc.num, num) +// } +// if writtable != tc.writtable { +// t.Fatalf("expected: %t, got: %t", tc.writtable, writtable) +// } + +// }) +// } +// } + +// func TestIsMesgDefinitionWriteable(t *testing.T) { +// tt := []struct { +// name string +// b []byte +// list *list.List +// writeable bool +// }{ +// { +// name: "init element value", +// b: []byte{1, 1}, +// list: list.New(), +// writeable: true, +// }, +// { +// name: "eq with 1st element value", +// b: []byte{1, 1}, +// list: func() *list.List { +// ls := list.New() +// ls.PushFront([]byte{1, 1}) +// return ls +// }(), +// writeable: false, +// }, +// { +// name: "not eq, replace existing", +// b: []byte{1, 1}, +// list: func() *list.List { +// ls := list.New() +// ls.PushFront([]byte{0, 1}) +// return ls +// }(), +// writeable: true, +// }, +// } +// for _, tc := range tt { +// t.Run(tc.name, func(t *testing.T) { +// enc := New(nil) +// enc.localMesgDefinitions = tc.list + +// writeable := enc.isMesgDefinitionWriteable(tc.b) +// if writeable != tc.writeable { +// t.Fatalf("expected: %t, got: %t", tc.writeable, writeable) +// } +// }) +// } +// } func TestCompressTimestampInHeader(t *testing.T) { now := time.Now() @@ -1024,36 +1037,10 @@ func TestCompressTimestampInHeader(t *testing.T) { } } -func TestIsEqual(t *testing.T) { - tt := []struct { - name string - prev []byte - target []byte - eq bool - }{ - {name: "same as prev", prev: []byte{1, 2}, target: []byte{1, 2}, eq: true}, - {name: "diff len", prev: []byte{1, 2}, target: []byte{1}, eq: false}, - {name: "diff byte", prev: []byte{1, 2}, target: []byte{2, 1}, eq: false}, - } - - for _, tc := range tt { - t.Run(tc.name, func(t *testing.T) { - eq := isEqual(tc.prev, tc.target) - if eq != tc.eq { - t.Fatalf("expected: %t, got: %t", tc.eq, eq) - } - }) - } -} - -func BenchmarkEncode(b *testing.B) { - b.StopTimer() - - const RecordSize = 100_000 - +func createFitForBenchmark(recodSize int) *proto.Fit { now := time.Now() fit := new(proto.Fit) - fit.Messages = make([]proto.Message, 0, RecordSize) + fit.Messages = make([]proto.Message, 0, recodSize) fit.Messages = append(fit.Messages, factory.CreateMesg(mesgnum.FileId).WithFieldValues(map[byte]any{ fieldnum.FileIdType: typedef.FileActivity, @@ -1095,9 +1082,24 @@ func BenchmarkEncode(b *testing.B) { }), ) - for i := 0; i < RecordSize-len(fit.Messages); i++ { + for i := 0; i < recodSize-len(fit.Messages); i++ { now = now.Add(time.Second) // only time is moving forward - fit.Messages = append(fit.Messages, factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ + if i%100 == 0 { // add event every 100 message + fit.Messages = append(fit.Messages, factory.CreateMesgOnly(mesgnum.Event).WithFields( + factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventActivity)), + factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(uint8(typedef.EventTypeStop)), + )) + now = now.Add(10 * time.Second) // gap + fit.Messages = append(fit.Messages, factory.CreateMesgOnly(mesgnum.Event).WithFields( + factory.CreateField(mesgnum.Event, fieldnum.EventTimestamp).WithValue(datetime.ToUint32(now)), + factory.CreateField(mesgnum.Event, fieldnum.EventEvent).WithValue(uint8(typedef.EventActivity)), + factory.CreateField(mesgnum.Event, fieldnum.EventEventType).WithValue(uint8(typedef.EventTypeStart)), + )) + now = now.Add(time.Second) // gap + } + + record := factory.CreateMesg(mesgnum.Record).WithFieldValues(map[byte]any{ fieldnum.RecordTimestamp: datetime.ToUint32(now), fieldnum.RecordPositionLat: int32(-90481372), fieldnum.RecordPositionLong: int32(1323227263), @@ -1107,14 +1109,48 @@ func BenchmarkEncode(b *testing.B) { fieldnum.RecordCadence: uint8(85), fieldnum.RecordAltitude: uint16((166.0 + 500.0) * 5.0), fieldnum.RecordTemperature: int8(32), - })) + }) + + if i%200 == 0 { // assume every 200 record hr sensor is not sending any data + record.RemoveFieldByNum(fieldnum.RecordHeartRate) + } + + fit.Messages = append(fit.Messages, record) } - enc := New(io.Discard) + return fit +} + +func BenchmarkEncode(b *testing.B) { + b.StopTimer() + fit := createFitForBenchmark(100_000) b.StartTimer() - for i := 0; i < b.N; i++ { - _ = enc.Encode(fit) - enc.reset() - } + b.Run("normal header zero", func(b *testing.B) { + b.StopTimer() + enc := New(io.Discard) + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = enc.Encode(fit) + enc.reset() + } + }) + b.Run("normal header 15", func(b *testing.B) { + b.StopTimer() + enc := New(io.Discard, WithNormalHeader(15)) + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = enc.Encode(fit) + enc.reset() + } + }) + b.Run("compressed timestamp header", func(b *testing.B) { + b.StopTimer() + enc := New(io.Discard, WithCompressedTimestampHeader()) + b.StartTimer() + for i := 0; i < b.N; i++ { + _ = enc.Encode(fit) + enc.reset() + } + }) } diff --git a/encoder/lru.go b/encoder/lru.go new file mode 100644 index 00000000..2f6666b1 --- /dev/null +++ b/encoder/lru.go @@ -0,0 +1,72 @@ +package encoder + +import "golang.org/x/exp/slices" + +// lru implements simple lru algorithm. Item search best case: O(1), worst case: O(n), depends how recently used is it. +type lru struct { + // Holds the actual items representated in bytes. + items [][]byte + + // Holds items's indexes as its value. The order of value will be shifted based on recent write. + // Example order: [0 (least recently used), 1, 2, 3, ..., 15 (most recently used)] + bucket []byte +} + +// newLRU creates new lru with fixed size, where should be > 0. +func newLRU(size byte) *lru { + return &lru{ + items: make([][]byte, size), + bucket: make([]byte, 0, size), + } +} + +// Reset reset variables so lru can be reused again without reallocation. +func (l *lru) Reset() { + for i := range l.items { + l.items[i] = nil + } + l.bucket = l.bucket[:0] +} + +// Putwill compare the equality of item with lru' items using cmp and store the item accordingly. +func (l *lru) Put(item []byte) (itemIndex byte, isNewItem bool) { + if bucketIndex := l.bucketIndex(item); bucketIndex != -1 { + return l.markAsRecentlyUsed(bucketIndex), false + } + if len(l.bucket) != len(l.items) { + return l.store(item), true + } + return l.replaceLeastRecentlyUsed(item), true +} + +func (l *lru) store(item []byte) (itemIndex byte) { + itemIndex = byte(len(l.bucket)) + l.items[itemIndex] = slices.Clone(item) + l.bucket = append(l.bucket, itemIndex) + return +} + +func (l *lru) markAsRecentlyUsed(bucketIndex int) (itemIndex byte) { + itemIndex = l.bucket[bucketIndex] + l.bucket = append(l.bucket[:bucketIndex], l.bucket[bucketIndex+1:]...) // splice bucketIndex from the bucket + l.bucket = append(l.bucket, itemIndex) // place at most recent index + return +} + +func (l *lru) replaceLeastRecentlyUsed(item []byte) (itemIndex byte) { + itemIndex = l.bucket[0] // take item's index out of bucket + copy(l.bucket[:len(l.bucket)-1], l.bucket[1:]) // left shift bucket + l.bucket[len(l.bucket)-1] = itemIndex // place at most recent index + l.items[itemIndex] = slices.Clone(item) + return +} + +func (l *lru) bucketIndex(item []byte) int { + for i := len(l.bucket); i > 0; i-- { + cur := l.bucket[i-1] + if slices.Equal(l.items[cur], item) { + return i - 1 + } + } + return -1 +} diff --git a/encoder/lru_test.go b/encoder/lru_test.go new file mode 100644 index 00000000..948c7bc5 --- /dev/null +++ b/encoder/lru_test.go @@ -0,0 +1,90 @@ +package encoder + +import ( + "testing" + + "github.com/muktihari/fit/proto" +) + +func TestLRU(t *testing.T) { + const size uint8 = proto.LocalMesgNumMask + 1 + l := newLRU(size) + + // place (size * 10) different items, the lru will be shifted in roundroubin order. + for i := byte(0); i < size*10; i++ { + localMesgNum, isNew := l.Put([]byte{i}) + if localMesgNum != i%size { + t.Fatalf("expected: %d, got: %d", i, localMesgNum) + } + isNewExpected := true + if isNew != isNewExpected { + t.Fatalf("expected: %t, got: %t", isNewExpected, isNew) + } + } + + // put same items should shift the lru bucket + for i := byte(0); i < size; i++ { + item := l.items[i] + localMesgNUm, _ := l.Put(item) + if localMesgNUm != i { + t.Fatalf("expected: %d, got: %d", i, localMesgNUm) + } + if l.bucket[size-1] != i { + t.Fatalf("expected: %d, got: %d", i, l.bucket[size-1]) + } + } + + // check index exist + if lruIndex := l.bucketIndex(l.items[l.bucket[1]]); lruIndex != 1 { + t.Fatalf("expected lru index: %d, got: %d", 1, lruIndex) + } + + // check index not exist + if lruIndex := l.bucketIndex([]byte{255, 255}); lruIndex != -1 { + t.Fatalf("expected lru index: %d, got: %d", -1, lruIndex) + } + + l.Reset() + if len(l.bucket) != 0 { + t.Fatalf("expected lruBuckt is %d, got: %d", 0, len(l.bucket)) + } + for i := range l.items { + if l.items[i] != nil { + t.Fatalf("[%d] expected nil, got: %v", i, l.items[i]) + } + } +} + +func BenchmarkLRU(b *testing.B) { + var size byte = proto.LocalMesgNumMask + 1 + b.Run("100k items, zero alloc when item exist", func(b *testing.B) { + b.StopTimer() + l := newLRU(size) + items := make([][]byte, 100_000) + for i := range items { + items[i] = []byte{byte(i % int(size))} + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + for i := range items { + l.Put(items[i]) + } + } + }) + b.Run("100k items, alloc since we clone the item", func(b *testing.B) { + b.StopTimer() + l := newLRU(size) + items := make([][]byte, 100_000) + for i := range items { + items[i] = []byte{byte(i)} + } + b.StartTimer() + + for i := 0; i < b.N; i++ { + for i := range items { + l.Put(items[i]) + } + } + }) +}