From 05aea74462458ac8991251ffb7bcf738be56b360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Geyslan=20Greg=C3=B3rio?= Date: Thu, 12 Dec 2024 12:54:44 -0300 Subject: [PATCH] wip two changelog types --- pkg/changelog/changelog.go | 223 +++------------------- pkg/changelog/changelog_benchmark_test.go | 12 +- pkg/changelog/changelog_kind.go | 115 +++++++++++ pkg/changelog/changelog_kind_test.go | 195 +++++++++++++++++++ pkg/changelog/changelog_test.go | 170 ++++++----------- pkg/changelog/entry.go | 144 ++++++++++++++ pkg/proctree/fileinfo.go | 8 +- pkg/proctree/taskinfo.go | 22 +-- 8 files changed, 558 insertions(+), 331 deletions(-) create mode 100644 pkg/changelog/changelog_kind.go create mode 100644 pkg/changelog/changelog_kind_test.go create mode 100644 pkg/changelog/entry.go diff --git a/pkg/changelog/changelog.go b/pkg/changelog/changelog.go index b8e33937232b..afeb4b64f8f5 100644 --- a/pkg/changelog/changelog.go +++ b/pkg/changelog/changelog.go @@ -6,221 +6,46 @@ import ( "github.com/aquasecurity/tracee/pkg/logger" ) -// -// Entries, entryList and entry structures are used to manage a list of changes. -// - -// MemberKind represents the unique identifier for each kind of entry in the Entries. -// It is used to categorize different kinds of changes tracked by the Entries. -// -// NOTE: Declare your own MemberKind constants sequentially starting from 0, -// since they are used as the indexes in the flags slice passed to NewEntries and -// other methods. For example: -// -// const MyKind1 MemberKind = 0 -// const MyKind2 MemberKind = 1 -// -// var flags = []MaxEntries{ -// MyKind1: 3, -// MyKind2: 5, -// } -type MemberKind uint8 - -// MaxEntries represents the maximum number of entries that can be stored for a given kind of entry. -type MaxEntries uint8 - -// Entries is the main structure that manages a list of changes (entries). -// It keeps track of specifically configured members indicated by MemberKind identifiers. -// When instantiating an Entries struct, one must supply a relevant mapping between the desired -// unique members and the maximum amount of changes that member can track. -// -// ATTENTION: You should use Entries within a struct and provide methods to access it, -// coordinating access through your struct mutexes. DO NOT EXPOSE the Entries object directly to -// the outside world as it is not thread-safe. -type Entries[T comparable] struct { - kindLists []entryList[T] // slice of entryList for each kind of entry. -} - -// entryList is an internal structure that manages a list of changes (entries) for a specific kind of entry. -type entryList[T comparable] struct { - maxEntries MaxEntries // maximum number of entries. - entries []entry[T] // list of entries. -} - -// entry is an internal structure representing a single change in the entryList. -// It includes the timestamp and the value of the change. -type entry[T comparable] struct { - timestamp time.Time // timestamp of the change - value T // value of the change +// Changelog manages a list of changes (entries) for a single type. +type Changelog[T comparable] struct { + list entryList[T] } -// Public - -// NewEntries initializes a new `Entries` structure using the provided `MaxEntries` slice. -func NewEntries[T comparable](maxEntries []MaxEntries) *Entries[T] { - newKindList := make([]entryList[T], 0, len(maxEntries)) - - for _, max := range maxEntries { - if max == 0 { - logger.Fatalw("maxEntries must be greater than 0") - } - - newEntryList := entryList[T]{ - maxEntries: max, - entries: make([]entry[T], 0), - } - newKindList = append(newKindList, newEntryList) +// NewChangelog initializes a new `Changelog` with the specified maximum number of entries. +func NewChangelog[T comparable](maxEntries MaxEntries) *Changelog[T] { + if maxEntries <= 0 { + logger.Fatalw("maxEntries must be greater than 0") } - return &Entries[T]{ - kindLists: newKindList, + return &Changelog[T]{ + list: newEntryList[T](maxEntries), } } -// Set adds or updates an entry in the Entries for the specified `MemberKind` ordered by timestamp. +// Set adds or updates an entry in the Changelog, ordered by timestamp. // If the new entry has the same value as the latest one, only the timestamp is updated. -// If there are already the maximum number of entries for this kind, it reuses or replaces an existing entry. -// -// ATTENTION: Make sure to pass a value of the correct type for the specified `MemberKind`. -func (e *Entries[T]) Set(kind MemberKind, value T, timestamp time.Time) { - if int(kind) >= len(e.kindLists) { - logger.Errorw("kind is not present in the entries", "kind", kind) - return - } - - entries := e.kindLists[kind].entries - - // if there are entries for kind check if the last entry has the same value - if len(entries) > 0 { - lastIdx := len(entries) - 1 - if entries[lastIdx].value == value && timestamp.After(entries[lastIdx].timestamp) { - // only update timestamp and return - entries[lastIdx].timestamp = timestamp - return - } - } - - newEntry := entry[T]{ - timestamp: timestamp, - value: value, - } - - // if there is space, insert the new entry at the correct position - - maxEntries := int(e.kindLists[kind].maxEntries) - if len(entries) < maxEntries { - insertPos := findInsertIdx(entries, timestamp) - if insertPos == len(entries) { - entries = append(entries, newEntry) - } else { - entries = insertAt(insertPos, entries, newEntry) - } - e.kindLists[kind].entries = entries // update kindLists - return - } - - // as there is no space, replace an entry - - replaceIdx := len(entries) - 1 // default index to replace - if timestamp.After(entries[replaceIdx].timestamp) { - // reallocate values to the left - shiftLeft(entries) - } else { - // find the correct position to store the entry - replaceIdx = findInsertIdx(entries, timestamp) - 1 - if replaceIdx == -1 { - replaceIdx = 0 - } - } - entries[replaceIdx] = newEntry +func (c *Changelog[T]) Set(value T, timestamp time.Time) { + c.list = c.list.set(value, timestamp) } -// Get retrieves the value of the entry for the specified `MemberKind` at or before the given timestamp. +// Get retrieves the value of the entry at or before the given timestamp. // If no matching entry is found, it returns the default value for the entry type. -func (e *Entries[T]) Get(kind MemberKind, timestamp time.Time) T { - if e.noEntries(kind) { - return getZero[T]() - } - - entries := e.kindLists[kind].entries - for i := len(entries) - 1; i >= 0; i-- { - if entries[i].timestamp.Before(timestamp) || entries[i].timestamp.Equal(timestamp) { - return entries[i].value - } - } - - return getZero[T]() +func (c *Changelog[T]) Get(timestamp time.Time) T { + return c.list.get(timestamp) } -// GetCurrent retrieves the most recent value for the specified `MemberKind`. +// GetCurrent retrieves the most recent value. // If no entry is found, it returns the default value for the entry type. -func (e *Entries[T]) GetCurrent(kind MemberKind) T { - if e.noEntries(kind) { - return getZero[T]() - } - - entries := e.kindLists[kind].entries - return entries[len(entries)-1].value -} - -// // GetAll retrieves all values for the specified `MemberKind`, from the newest to the oldest. -func (e *Entries[T]) GetAll(kind MemberKind) []T { - if e.noEntries(kind) { - return nil - } - - entries := e.kindLists[kind].entries - values := make([]T, 0, len(entries)) - for i := len(entries) - 1; i >= 0; i-- { - values = append(values, entries[i].value) - } - - return values -} - -// Count returns the number of entries recorded for the specified `MemberKind`. -func (e *Entries[T]) Count(kind MemberKind) int { - if e.noEntries(kind) { - return 0 - } - - return len(e.kindLists[kind].entries) -} - -// private - -// noEntries checks if there are no entries for the specified `MemberKind`. -func (e *Entries[T]) noEntries(kind MemberKind) bool { - return int(kind) >= len(e.kindLists) || len(e.kindLists[kind].entries) == 0 -} - -// utility - -// insertAt inserts a new entry at the specified index in the entries list. -func insertAt[T comparable](idx int, entries []entry[T], newEntry entry[T]) []entry[T] { - return append(entries[:idx], append([]entry[T]{newEntry}, entries[idx:]...)...) -} - -// findInsertIdx finds the correct index to insert a new entry based on its timestamp. -func findInsertIdx[T comparable](entries []entry[T], timestamp time.Time) int { - for i := len(entries) - 1; i >= 0; i-- { - if entries[i].timestamp.Before(timestamp) { - return i + 1 - } - } - - return len(entries) +func (c *Changelog[T]) GetCurrent() T { + return c.list.getCurrent() } -// shiftLeft shifts entries within the given indexes to the left, discarding the oldest entry. -func shiftLeft[T comparable](entries []entry[T]) { - for i := 0; i < len(entries)-1; i++ { - entries[i] = entries[i+1] - } +// GetAll retrieves all values, from the newest to the oldest. +func (c *Changelog[T]) GetAll() []T { + return c.list.getAll() } -// getZero returns the zero value for the type `T`. -func getZero[T comparable]() T { - var zero T - return zero +// Count returns the number of entries recorded. +func (c *Changelog[T]) Count() int { + return len(c.list.entries) } diff --git a/pkg/changelog/changelog_benchmark_test.go b/pkg/changelog/changelog_benchmark_test.go index 4299ed3ca8d7..d7f8ed12bac6 100644 --- a/pkg/changelog/changelog_benchmark_test.go +++ b/pkg/changelog/changelog_benchmark_test.go @@ -151,7 +151,7 @@ func Benchmark_Set(b *testing.B) { b.Run("All Scenarios", func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - clv := NewEntries[int](entryFlagsAllScenarios) + clv := NewChangelogKind[int](entryFlagsAllScenarios) b.StartTimer() for _, tc := range testCasesAllScenarios { clv.Set(testInt0, tc.value, tc.time) @@ -162,7 +162,7 @@ func Benchmark_Set(b *testing.B) { b.Run("Within Limit", func(b *testing.B) { for i := 0; i < b.N; i++ { b.StopTimer() - clv := NewEntries[int](entryFlagsWithinLimit) + clv := NewChangelogKind[int](entryFlagsWithinLimit) b.StartTimer() for _, tc := range testCasesWithinLimit { clv.Set(testInt0, tc.value, tc.time) @@ -173,7 +173,7 @@ func Benchmark_Set(b *testing.B) { func Benchmark_Get(b *testing.B) { b.Run("All Scenarios", func(b *testing.B) { - clv := NewEntries[int](entryFlagsAllScenarios) + clv := NewChangelogKind[int](entryFlagsAllScenarios) for _, tc := range testCasesAllScenarios { clv.Set(testInt0, tc.value, tc.time) } @@ -187,7 +187,7 @@ func Benchmark_Get(b *testing.B) { }) b.Run("Within Limit", func(b *testing.B) { - clv := NewEntries[int](entryFlagsWithinLimit) + clv := NewChangelogKind[int](entryFlagsWithinLimit) for _, tc := range testCasesWithinLimit { clv.Set(testInt0, tc.value, tc.time) } @@ -203,7 +203,7 @@ func Benchmark_Get(b *testing.B) { func Benchmark_GetCurrent(b *testing.B) { b.Run("All Scenarios", func(b *testing.B) { - clv := NewEntries[int](entryFlagsAllScenarios) + clv := NewChangelogKind[int](entryFlagsAllScenarios) for _, tc := range testCasesAllScenarios { clv.Set(testInt0, tc.value, tc.time) } @@ -215,7 +215,7 @@ func Benchmark_GetCurrent(b *testing.B) { }) b.Run("Within Limit", func(b *testing.B) { - clv := NewEntries[int](entryFlagsWithinLimit) + clv := NewChangelogKind[int](entryFlagsWithinLimit) for _, tc := range testCasesWithinLimit { clv.Set(testInt0, tc.value, tc.time) } diff --git a/pkg/changelog/changelog_kind.go b/pkg/changelog/changelog_kind.go new file mode 100644 index 000000000000..1459c0d162ab --- /dev/null +++ b/pkg/changelog/changelog_kind.go @@ -0,0 +1,115 @@ +package changelog + +import ( + "time" + + "github.com/aquasecurity/tracee/pkg/logger" +) + +// MemberKind represents the unique identifier for each kind of entry in the ChangelogKind. +// It is used to categorize different kinds of changes tracked by the ChangelogKind. +// +// NOTE: Declare your own MemberKind constants sequentially starting from 0, +// since they are used as the indexes in the flags slice passed to NewChangelog and +// other methods. For example: +// +// const MyKind1 MemberKind = 0 +// const MyKind2 MemberKind = 1 +// +// var flags = []MaxEntries{ +// MyKind1: 3, +// MyKind2: 5, +// } +type MemberKind uint8 + +// MaxEntries represents the maximum number of entries that can be stored for a given kind of entry. +type MaxEntries uint8 + +// ChangelogKind is the main structure that manages a list of changes (entries). +// It keeps track of specifically configured members indicated by MemberKind identifiers. +// When instantiating an ChangelogKind struct, one must supply a relevant mapping between the desired +// unique members and the maximum amount of changes that member can track. +// +// ATTENTION: You should use ChangelogKind within a struct and provide methods to access it, +// coordinating access through your struct mutexes. DO NOT EXPOSE the ChangelogKind object directly to +// the outside world as it is not thread-safe. +type ChangelogKind[T comparable] struct { + kindLists []entryList[T] // list of entries for each kind +} + +// NewChangelogKind initializes a new `ChangelogKind` structure using the provided `MaxEntries` slice. +func NewChangelogKind[T comparable](maxEntries []MaxEntries) *ChangelogKind[T] { + newKindList := make([]entryList[T], 0, len(maxEntries)) + + for _, max := range maxEntries { + if max == 0 { + logger.Fatalw("maxEntries must be greater than 0") + } + + newKindList = append(newKindList, newEntryList[T](max)) + } + + return &ChangelogKind[T]{ + kindLists: newKindList, + } +} + +// Set adds or updates an entry in the ChangelogKind for the specified `MemberKind` ordered by timestamp. +// If the new entry has the same value as the latest one, only the timestamp is updated. +// If there are already the maximum number of entries for this kind, it reuses or replaces an existing entry. +// +// ATTENTION: Make sure to pass a value of the correct type for the specified `MemberKind`. +func (e *ChangelogKind[T]) Set(kind MemberKind, value T, timestamp time.Time) { + if int(kind) >= len(e.kindLists) { + logger.Errorw("kind is not present in the entries", "kind", kind) + return + } + + kindList := e.kindLists[kind] + e.kindLists[kind] = kindList.set(value, timestamp) +} + +// Get retrieves the value of the entry for the specified `MemberKind` at or before the given timestamp. +// If no matching entry is found, it returns the default value for the entry type. +func (e *ChangelogKind[T]) Get(kind MemberKind, timestamp time.Time) T { + if e.invalidKind(kind) { + return getZero[T]() + } + + return e.kindLists[kind].get(timestamp) +} + +// GetCurrent retrieves the most recent value for the specified `MemberKind`. +// If no entry is found, it returns the default value for the entry type. +func (e *ChangelogKind[T]) GetCurrent(kind MemberKind) T { + if e.invalidKind(kind) { + return getZero[T]() + } + + return e.kindLists[kind].getCurrent() +} + +// // GetAll retrieves all values for the specified `MemberKind`, from the newest to the oldest. +func (e *ChangelogKind[T]) GetAll(kind MemberKind) []T { + if e.invalidKind(kind) { + return nil + } + + return e.kindLists[kind].getAll() +} + +// Count returns the number of entries recorded for the specified `MemberKind`. +func (e *ChangelogKind[T]) Count(kind MemberKind) int { + if e.invalidKind(kind) { + return 0 + } + + return len(e.kindLists[kind].entries) +} + +// private + +// invalidKind checks if the specified `MemberKind` is invalid. +func (e *ChangelogKind[T]) invalidKind(kind MemberKind) bool { + return int(kind) >= len(e.kindLists) +} diff --git a/pkg/changelog/changelog_kind_test.go b/pkg/changelog/changelog_kind_test.go new file mode 100644 index 000000000000..4309485cda30 --- /dev/null +++ b/pkg/changelog/changelog_kind_test.go @@ -0,0 +1,195 @@ +package changelog + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/aquasecurity/tracee/pkg/utils" +) + +const ( + // int members + testInt0 MemberKind = iota + testInt1 + testInt2 +) + +const ( + // string members + testString MemberKind = iota +) + +func getTimeFromSec(second int) time.Time { + return time.Unix(int64(second), 0) +} + +func TestChangelogKind_GetZeroValue(t *testing.T) { + flags := []MaxEntries{ + testInt0: 1, + } + changelog := NewChangelogKind[int](flags) + time0 := getTimeFromSec(0) + + // Assert zero value before any set + assert.Equal(t, 0, changelog.Get(testInt0, time0), "Expected zero value for testInt0") + assert.Equal(t, 0, changelog.GetCurrent(testInt0), "Expected zero value for testInt0") + + // Set and assert value + changelog.Set(testInt0, 3001, time0) + assert.Equal(t, 3001, changelog.Get(testInt0, time0), "Expected testInt0 to be 3001") + assert.Equal(t, 3001, changelog.GetCurrent(testInt0), "Expected current testInt0 to be 3001") + + // Check the count of entries + assert.Equal(t, 1, changelog.Count(testInt0), "Expected 1 entry") + assert.Equal(t, 0, changelog.Count(testInt1), "Expected 0 entries") +} + +func TestChangelogKind_ShiftAndReplace(t *testing.T) { + flags := []MaxEntries{ + testString: 2, + } + changelog := NewChangelogKind[string](flags) + + // Set entries and assert initial values + changelog.Set(testString, "initial", getTimeFromSec(0)) + changelog.Set(testString, "updated", getTimeFromSec(1)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(0)), "Expected first entry to be 'initial'") + assert.Equal(t, "updated", changelog.Get(testString, getTimeFromSec(1)), "Expected second entry to be 'updated'") + + // Test shifting and replacement + changelog.Set(testString, "final", getTimeFromSec(2)) + assert.Equal(t, "updated", changelog.Get(testString, getTimeFromSec(1)), "Expected oldest entry to be removed") + assert.Equal(t, "final", changelog.Get(testString, getTimeFromSec(2)), "Expected newest entry to be 'final'") + assert.Equal(t, "final", changelog.GetCurrent(testString), "Expected current entry to be 'final'") + + // Check the count of entries + assert.Equal(t, 2, changelog.Count(testString), "Expected 2 entries") +} + +func TestChangelogKind_ReplaceMostRecentWithSameValue(t *testing.T) { + flags := []MaxEntries{ + testString: 2, + } + changelog := NewChangelogKind[string](flags) + + // Set entries and assert initial value + changelog.Set(testString, "initial", getTimeFromSec(0)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(0)), "Expected first entry to be 'initial'") + changelog.Set(testString, "initial", getTimeFromSec(1)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(1)), "Expected first entry to have timestamp updated") + + // Test replacement of most recent entry with same value + changelog.Set(testString, "second", getTimeFromSec(2)) + assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(1)), "Expected first entry to be 'initial'") + assert.Equal(t, "second", changelog.Get(testString, getTimeFromSec(2)), "Expected second entry to have timestamp updated") + + // Check the count of entries + assert.Equal(t, 2, changelog.Count(testString), "Expected 2 entries") +} + +func TestChangelogKind_InsertWithOlderTimestamp(t *testing.T) { + flags := []MaxEntries{ + testString: 3, + } + changelog := NewChangelogKind[string](flags) + now := getTimeFromSec(0) + + // Insert entries with increasing timestamps + changelog.Set(testString, "first", now) + changelog.Set(testString, "second", now.Add(1*time.Second)) + changelog.Set(testString, "third", now.Add(2*time.Second)) + + // Insert an entry with an older timestamp + changelog.Set(testString, "older", now.Add(1*time.Millisecond)) + + // Check the count of entries + assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") + + // Verify the order of entries + assert.Equal(t, "older", changelog.Get(testString, now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") + assert.Equal(t, "second", changelog.Get(testString, now.Add(1*time.Second)), "Expected 'second' to be the second entry") + assert.Equal(t, "third", changelog.Get(testString, now.Add(2*time.Second)), "Expected 'third' to be the last entry") + + // Insert an entry with an intermediate timestamp + changelog.Set(testString, "second-third", now.Add(1*time.Second+1*time.Millisecond)) + + // Verify the order of entries + assert.Equal(t, "older", changelog.Get(testString, now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") + assert.Equal(t, "second-third", changelog.Get(testString, now.Add(1*time.Second+1*time.Millisecond)), "Expected 'second-third' to be the second entry") + assert.Equal(t, "third", changelog.Get(testString, now.Add(2*time.Second)), "Expected 'third' to be the last entry") + + // Check the count of entries + assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") +} + +func TestChangelogKind_InsertSameValueWithNewTimestamp(t *testing.T) { + flags := []MaxEntries{ + testString: 3, + } + changelog := NewChangelogKind[string](flags) + + // Insert entries with increasing timestamps + changelog.Set(testString, "same", getTimeFromSec(0)) + + // Replace the last entry with the same value but a new timestamp + changelog.Set(testString, "same", getTimeFromSec(1)) + + // Verify the order of entries + assert.Equal(t, "same", changelog.Get(testString, getTimeFromSec(1)), "Expected 'same' to be the second entry") + + // Insert entries with sequential timestamps + changelog.Set(testString, "new", getTimeFromSec(2)) + changelog.Set(testString, "other", getTimeFromSec(3)) + + // Replace the last entry with the same value but a new timestamp + changelog.Set(testString, "other", getTimeFromSec(4)) + + // Verify the order of entries + assert.Equal(t, "same", changelog.Get(testString, getTimeFromSec(1)), "Expected 'same' to be the first entry") + assert.Equal(t, "new", changelog.Get(testString, getTimeFromSec(2)), "Expected 'new' to be the second entry") + assert.Equal(t, "other", changelog.Get(testString, getTimeFromSec(4)), "Expected 'other' to be the last entry") + + // Check the count of entries + assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") +} + +// TestChangelogKind_PrintSizes prints the sizes of the structs used in the ChangelogKind type. +// Run it as DEBUG test to see the output. +func TestChangelogKind_PrintSizes(t *testing.T) { + flags := []MaxEntries{ + testInt0: 1, + } + + changelog1 := NewChangelogKind[int](flags) + utils.PrintStructSizes(os.Stdout, changelog1) + + entryList1 := entryList[int]{ + maxEntries: flags[testInt0], + entries: []entry[int]{}, + } + utils.PrintStructSizes(os.Stdout, entryList1) + + entry1 := entry[int]{} + utils.PrintStructSizes(os.Stdout, entry1) + + // + + flags = []MaxEntries{ + testString: 1, + } + + changelog2 := NewChangelogKind[string](flags) + utils.PrintStructSizes(os.Stdout, changelog2) + + entryList2 := entryList[string]{ + maxEntries: flags[testString], + entries: []entry[string]{}, + } + utils.PrintStructSizes(os.Stdout, entryList2) + + entry2 := entry[string]{} + utils.PrintStructSizes(os.Stdout, entry2) +} diff --git a/pkg/changelog/changelog_test.go b/pkg/changelog/changelog_test.go index 860518af1430..b8d2d27b0aa7 100644 --- a/pkg/changelog/changelog_test.go +++ b/pkg/changelog/changelog_test.go @@ -10,186 +10,134 @@ import ( "github.com/aquasecurity/tracee/pkg/utils" ) -const ( - // int members - testInt0 MemberKind = iota - testInt1 - testInt2 -) - -const ( - // string members - testString MemberKind = iota -) - -func getTimeFromSec(second int) time.Time { - return time.Unix(int64(second), 0) -} - -func TestChangelogEntries_GetZeroValue(t *testing.T) { - flags := []MaxEntries{ - testInt0: 1, - } - changelog := NewEntries[int](flags) +func TestChangelog_GetZeroValue(t *testing.T) { + changelog := NewChangelog[int](1) time0 := getTimeFromSec(0) // Assert zero value before any set - assert.Equal(t, 0, changelog.Get(testInt0, time0), "Expected zero value for testInt0") - assert.Equal(t, 0, changelog.GetCurrent(testInt0), "Expected zero value for testInt0") + assert.Equal(t, 0, changelog.Get(time0), "Expected zero value for testInt0") + assert.Equal(t, 0, changelog.GetCurrent(), "Expected zero value for testInt0") // Set and assert value - changelog.Set(testInt0, 3001, time0) - assert.Equal(t, 3001, changelog.Get(testInt0, time0), "Expected testInt0 to be 3001") - assert.Equal(t, 3001, changelog.GetCurrent(testInt0), "Expected current testInt0 to be 3001") + changelog.Set(3001, time0) + assert.Equal(t, 3001, changelog.Get(time0), "Expected testInt0 to be 3001") + assert.Equal(t, 3001, changelog.GetCurrent(), "Expected current testInt0 to be 3001") // Check the count of entries - assert.Equal(t, 1, changelog.Count(testInt0), "Expected 1 entry") - assert.Equal(t, 0, changelog.Count(testInt1), "Expected 0 entries") + assert.Equal(t, 1, changelog.Count(), "Expected 1 entry") } -func TestChangelogEntries_ShiftAndReplace(t *testing.T) { - flags := []MaxEntries{ - testString: 2, - } - changelog := NewEntries[string](flags) +func TestChangelog_ShiftAndReplace(t *testing.T) { + changelog := NewChangelog[string](2) // Set entries and assert initial values - changelog.Set(testString, "initial", getTimeFromSec(0)) - changelog.Set(testString, "updated", getTimeFromSec(1)) - assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(0)), "Expected first entry to be 'initial'") - assert.Equal(t, "updated", changelog.Get(testString, getTimeFromSec(1)), "Expected second entry to be 'updated'") + changelog.Set("initial", getTimeFromSec(0)) + changelog.Set("updated", getTimeFromSec(1)) + assert.Equal(t, "initial", changelog.Get(getTimeFromSec(0)), "Expected first entry to be 'initial'") + assert.Equal(t, "updated", changelog.Get(getTimeFromSec(1)), "Expected second entry to be 'updated'") // Test shifting and replacement - changelog.Set(testString, "final", getTimeFromSec(2)) - assert.Equal(t, "updated", changelog.Get(testString, getTimeFromSec(1)), "Expected oldest entry to be removed") - assert.Equal(t, "final", changelog.Get(testString, getTimeFromSec(2)), "Expected newest entry to be 'final'") - assert.Equal(t, "final", changelog.GetCurrent(testString), "Expected current entry to be 'final'") + changelog.Set("final", getTimeFromSec(2)) + assert.Equal(t, "updated", changelog.Get(getTimeFromSec(1)), "Expected oldest entry to be removed") + assert.Equal(t, "final", changelog.Get(getTimeFromSec(2)), "Expected newest entry to be 'final'") + assert.Equal(t, "final", changelog.GetCurrent(), "Expected current entry to be 'final'") // Check the count of entries - assert.Equal(t, 2, changelog.Count(testString), "Expected 2 entries") + assert.Equal(t, 2, changelog.Count(), "Expected 2 entries") } -func TestChangelogEntries_ReplaceMostRecentWithSameValue(t *testing.T) { - flags := []MaxEntries{ - testString: 2, - } - changelog := NewEntries[string](flags) +func TestChangelog_ReplaceMostRecentWithSameValue(t *testing.T) { + changelog := NewChangelog[string](2) // Set entries and assert initial value - changelog.Set(testString, "initial", getTimeFromSec(0)) - assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(0)), "Expected first entry to be 'initial'") - changelog.Set(testString, "initial", getTimeFromSec(1)) - assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(1)), "Expected first entry to have timestamp updated") + changelog.Set("initial", getTimeFromSec(0)) + assert.Equal(t, "initial", changelog.Get(getTimeFromSec(0)), "Expected first entry to be 'initial'") + changelog.Set("initial", getTimeFromSec(1)) + assert.Equal(t, "initial", changelog.Get(getTimeFromSec(1)), "Expected first entry to have timestamp updated") // Test replacement of most recent entry with same value - changelog.Set(testString, "second", getTimeFromSec(2)) - assert.Equal(t, "initial", changelog.Get(testString, getTimeFromSec(1)), "Expected first entry to be 'initial'") - assert.Equal(t, "second", changelog.Get(testString, getTimeFromSec(2)), "Expected second entry to have timestamp updated") + changelog.Set("second", getTimeFromSec(2)) + assert.Equal(t, "initial", changelog.Get(getTimeFromSec(1)), "Expected first entry to be 'initial'") + assert.Equal(t, "second", changelog.Get(getTimeFromSec(2)), "Expected second entry to have timestamp updated") // Check the count of entries - assert.Equal(t, 2, changelog.Count(testString), "Expected 2 entries") + assert.Equal(t, 2, changelog.Count(), "Expected 2 entries") } -func TestChangelogEntries_InsertWithOlderTimestamp(t *testing.T) { - flags := []MaxEntries{ - testString: 3, - } - changelog := NewEntries[string](flags) +func TestChangelog_InsertWithOlderTimestamp(t *testing.T) { + changelog := NewChangelog[string](3) now := getTimeFromSec(0) // Insert entries with increasing timestamps - changelog.Set(testString, "first", now) - changelog.Set(testString, "second", now.Add(1*time.Second)) - changelog.Set(testString, "third", now.Add(2*time.Second)) + changelog.Set("first", now) + changelog.Set("second", now.Add(1*time.Second)) + changelog.Set("third", now.Add(2*time.Second)) // Insert an entry with an older timestamp - changelog.Set(testString, "older", now.Add(1*time.Millisecond)) + changelog.Set("older", now.Add(1*time.Millisecond)) // Check the count of entries - assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") + assert.Equal(t, 3, changelog.Count(), "Expected 3 entries") // Verify the order of entries - assert.Equal(t, "older", changelog.Get(testString, now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") - assert.Equal(t, "second", changelog.Get(testString, now.Add(1*time.Second)), "Expected 'second' to be the second entry") - assert.Equal(t, "third", changelog.Get(testString, now.Add(2*time.Second)), "Expected 'third' to be the last entry") + assert.Equal(t, "older", changelog.Get(now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") + assert.Equal(t, "second", changelog.Get(now.Add(1*time.Second)), "Expected 'second' to be the second entry") + assert.Equal(t, "third", changelog.Get(now.Add(2*time.Second)), "Expected 'third' to be the last entry") // Insert an entry with an intermediate timestamp - changelog.Set(testString, "second-third", now.Add(1*time.Second+1*time.Millisecond)) + changelog.Set("second-third", now.Add(1*time.Second+1*time.Millisecond)) // Verify the order of entries - assert.Equal(t, "older", changelog.Get(testString, now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") - assert.Equal(t, "second-third", changelog.Get(testString, now.Add(1*time.Second+1*time.Millisecond)), "Expected 'second-third' to be the second entry") - assert.Equal(t, "third", changelog.Get(testString, now.Add(2*time.Second)), "Expected 'third' to be the last entry") + assert.Equal(t, "older", changelog.Get(now.Add(1*time.Millisecond)), "Expected 'older' to be the first entry") + assert.Equal(t, "second-third", changelog.Get(now.Add(1*time.Second+1*time.Millisecond)), "Expected 'second-third' to be the second entry") + assert.Equal(t, "third", changelog.Get(now.Add(2*time.Second)), "Expected 'third' to be the last entry") // Check the count of entries - assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") + assert.Equal(t, 3, changelog.Count(), "Expected 3 entries") } -func TestChangelogEntries_InsertSameValueWithNewTimestamp(t *testing.T) { - flags := []MaxEntries{ - testString: 3, - } - changelog := NewEntries[string](flags) +func TestChangelog_InsertSameValueWithNewTimestamp(t *testing.T) { + changelog := NewChangelog[string](3) // Insert entries with increasing timestamps - changelog.Set(testString, "same", getTimeFromSec(0)) + changelog.Set("same", getTimeFromSec(0)) // Replace the last entry with the same value but a new timestamp - changelog.Set(testString, "same", getTimeFromSec(1)) + changelog.Set("same", getTimeFromSec(1)) // Verify the order of entries - assert.Equal(t, "same", changelog.Get(testString, getTimeFromSec(1)), "Expected 'same' to be the second entry") + assert.Equal(t, "same", changelog.Get(getTimeFromSec(1)), "Expected 'same' to be the second entry") // Insert entries with sequential timestamps - changelog.Set(testString, "new", getTimeFromSec(2)) - changelog.Set(testString, "other", getTimeFromSec(3)) + changelog.Set("new", getTimeFromSec(2)) + changelog.Set("other", getTimeFromSec(3)) // Replace the last entry with the same value but a new timestamp - changelog.Set(testString, "other", getTimeFromSec(4)) + changelog.Set("other", getTimeFromSec(4)) // Verify the order of entries - assert.Equal(t, "same", changelog.Get(testString, getTimeFromSec(1)), "Expected 'same' to be the first entry") - assert.Equal(t, "new", changelog.Get(testString, getTimeFromSec(2)), "Expected 'new' to be the second entry") - assert.Equal(t, "other", changelog.Get(testString, getTimeFromSec(4)), "Expected 'other' to be the last entry") + assert.Equal(t, "same", changelog.Get(getTimeFromSec(1)), "Expected 'same' to be the first entry") + assert.Equal(t, "new", changelog.Get(getTimeFromSec(2)), "Expected 'new' to be the second entry") + assert.Equal(t, "other", changelog.Get(getTimeFromSec(4)), "Expected 'other' to be the last entry") // Check the count of entries - assert.Equal(t, 3, changelog.Count(testString), "Expected 3 entries") + assert.Equal(t, 3, changelog.Count(), "Expected 3 entries") } -// TestChangelog_PrintSizes prints the sizes of the structs used in the changelog package. +// TestChangelog_PrintSizes prints the sizes of the structs used in the Changelog type. // Run it as DEBUG test to see the output. func TestChangelog_PrintSizes(t *testing.T) { - flags := []MaxEntries{ - testInt0: 1, - } - - changelog1 := NewEntries[int](flags) + changelog1 := NewChangelog[int](1) utils.PrintStructSizes(os.Stdout, changelog1) - entryList1 := entryList[int]{ - maxEntries: flags[testInt0], - entries: []entry[int]{}, - } - utils.PrintStructSizes(os.Stdout, entryList1) - entry1 := entry[int]{} utils.PrintStructSizes(os.Stdout, entry1) // - flags = []MaxEntries{ - testString: 1, - } - - changelog2 := NewEntries[string](flags) + changelog2 := NewChangelog[string](1) utils.PrintStructSizes(os.Stdout, changelog2) - entryList2 := entryList[string]{ - maxEntries: flags[testString], - entries: []entry[string]{}, - } - utils.PrintStructSizes(os.Stdout, entryList2) - entry2 := entry[string]{} utils.PrintStructSizes(os.Stdout, entry2) } diff --git a/pkg/changelog/entry.go b/pkg/changelog/entry.go new file mode 100644 index 000000000000..bf54939c3fdd --- /dev/null +++ b/pkg/changelog/entry.go @@ -0,0 +1,144 @@ +package changelog + +import "time" + +// entry is an internal structure representing a single change in the entryList. +// It includes the timestamp and the value of the change. +type entry[T comparable] struct { + timestamp time.Time // timestamp of the change + value T // value of the change +} + +func newEntry[T comparable](value T, timestamp time.Time) entry[T] { + return entry[T]{ + timestamp: timestamp, + value: value, + } +} + +// entryList is an internal structure that stores a list of changes (entries). +type entryList[T comparable] struct { + maxEntries MaxEntries // maximum number of entries + entries []entry[T] // list of entries +} + +func newEntryList[T comparable](maxEntries MaxEntries) entryList[T] { + return entryList[T]{ + maxEntries: maxEntries, + entries: make([]entry[T], 0), // don't pre-allocate + } +} + +func (el *entryList[T]) set(value T, timestamp time.Time) entryList[T] { + entries := el.entries + length := len(entries) + + // if there are entries, check if the last entry has the same value + + if length > 0 { + lastIdx := length - 1 + if entries[lastIdx].value == value && timestamp.After(entries[lastIdx].timestamp) { + // Only update timestamp and return + entries[lastIdx].timestamp = timestamp + return *el + } + } + + entry := newEntry[T](value, timestamp) + + // if there is space, insert the new entry at the correct position + + if length < int(el.maxEntries) { + insertPos := findInsertIdx(entries, timestamp) + if insertPos == length { + entries = append(entries, entry) + } else { + entries = insertAt(insertPos, entries, entry) + } + el.entries = entries // replace entries with the new list + return *el + } + + // as there is no space, replace an entry + + replaceIdx := length - 1 // default index to replace + if timestamp.After(entries[replaceIdx].timestamp) { + // reallocate values to the left + shiftLeft(entries) + } else { + // find the correct position to store the entry + replaceIdx = findInsertIdx(entries, timestamp) - 1 + if replaceIdx == -1 { + replaceIdx = 0 + } + } + entries[replaceIdx] = entry + + return *el +} + +func (el *entryList[T]) get(timestamp time.Time) T { + entries := el.entries + for i := len(entries) - 1; i >= 0; i-- { + if entries[i].timestamp.Before(timestamp) || entries[i].timestamp.Equal(timestamp) { + return entries[i].value + } + } + + return getZero[T]() +} + +func (el *entryList[T]) getCurrent() T { + entries := el.entries + length := len(entries) + if length == 0 { + return getZero[T]() + } + + return entries[length-1].value +} + +func (el *entryList[T]) getAll() []T { + entries := el.entries + values := make([]T, 0, len(entries)) + for i := len(entries) - 1; i >= 0; i-- { + values = append(values, entries[i].value) + } + + return values +} + +func (el *entryList[T]) noEntries() bool { + return len(el.entries) == 0 +} + +// utility + +// insertAt inserts a new entry at the specified index in the entries list. +func insertAt[T comparable](idx int, entries []entry[T], newEntry entry[T]) []entry[T] { + return append(entries[:idx], append([]entry[T]{newEntry}, entries[idx:]...)...) +} + +// findInsertIdx finds the correct index to insert a new entry based on its timestamp. +func findInsertIdx[T comparable](entries []entry[T], timestamp time.Time) int { + for i := len(entries) - 1; i >= 0; i-- { + if entries[i].timestamp.Before(timestamp) { + return i + 1 + } + } + + return len(entries) +} + +// shiftLeft shifts entries within the given indexes to the left, discarding the oldest entry. +func shiftLeft[T comparable](entries []entry[T]) { + for i := 0; i < len(entries)-1; i++ { + entries[i] = entries[i+1] + } +} + +// getZero returns the zero value for the type `T`. +func getZero[T comparable]() T { + var zero T + return zero +} diff --git a/pkg/proctree/fileinfo.go b/pkg/proctree/fileinfo.go index a9e67fb82b07..5cb605b08027 100644 --- a/pkg/proctree/fileinfo.go +++ b/pkg/proctree/fileinfo.go @@ -52,16 +52,16 @@ var ( // FileInfo represents a file. type FileInfo struct { - mutableStrings *changelog.Entries[string] - mutableInts *changelog.Entries[int] + mutableStrings *changelog.ChangelogKind[string] + mutableInts *changelog.ChangelogKind[int] mutex *sync.RWMutex } // NewFileInfo creates a new file. func NewFileInfo() *FileInfo { return &FileInfo{ - mutableStrings: changelog.NewEntries[string](fileInfoMutableStringsFlags), - mutableInts: changelog.NewEntries[int](fileInfoMutableIntsFlags), + mutableStrings: changelog.NewChangelogKind[string](fileInfoMutableStringsFlags), + mutableInts: changelog.NewChangelogKind[int](fileInfoMutableIntsFlags), mutex: &sync.RWMutex{}, } } diff --git a/pkg/proctree/taskinfo.go b/pkg/proctree/taskinfo.go index 17cdeac7bfc9..a5ca66a1d88e 100644 --- a/pkg/proctree/taskinfo.go +++ b/pkg/proctree/taskinfo.go @@ -57,28 +57,28 @@ var ( // TaskInfo represents a task. type TaskInfo struct { - tid int // immutable - pid int // immutable - nsTid int // immutable - nsPid int // immutable - startTimeNS uint64 // this is a duration, in ns, since boot (immutable) - exitTimeNS uint64 // this is a duration, in ns, since boot (immutable) - mutableStrings *changelog.Entries[string] // string mutable fields - mutableInts *changelog.Entries[int] // int mutable fields + tid int // immutable + pid int // immutable + nsTid int // immutable + nsPid int // immutable + startTimeNS uint64 // this is a duration, in ns, since boot (immutable) + exitTimeNS uint64 // this is a duration, in ns, since boot (immutable) + mutableStrings *changelog.ChangelogKind[string] // string mutable fields + mutableInts *changelog.ChangelogKind[int] // int mutable fields mutex *sync.RWMutex } // NewTaskInfo creates a new task. func NewTaskInfo() *TaskInfo { return &TaskInfo{ - mutableStrings: changelog.NewEntries[string](taskInfoMutableStringsFlags), - mutableInts: changelog.NewEntries[int](taskInfoMutableIntsFlags), + mutableStrings: changelog.NewChangelogKind[string](taskInfoMutableStringsFlags), + mutableInts: changelog.NewChangelogKind[int](taskInfoMutableIntsFlags), mutex: &sync.RWMutex{}, } } // NewTaskInfoFromFeed creates a new task with values from the given feed. -func NewTaskInfoFromFeed(feed TaskInfoFeed) *TaskInfo { +func NewTaskInfoNewFromFeed(feed TaskInfoFeed) *TaskInfo { new := NewTaskInfo() new.setFeed(feed) return new