Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
aos
GitHub Repository: aos/grafana-agent
Path: blob/main/pkg/runner/hash_map_test.go
4093 views
1
package runner
2
3
import (
4
"fmt"
5
"testing"
6
7
"github.com/stretchr/testify/require"
8
)
9
10
func Test_hashMap(t *testing.T) {
11
t.Run("tasks can be added", func(t *testing.T) {
12
hm := newHashMap(0)
13
14
// Try adding the task twice. The first time we add it, Add should return
15
// true, since it's brand new to the hash map. The second time we add it,
16
// Add should return false, since the task was previously added.
17
task := newBasicMockTask(1)
18
require.True(t, hm.Add(task), "Add should have returned true on the first call")
19
require.False(t, hm.Add(task), "Add should have returned false on the second call of the same task")
20
21
// Try adding a new task twice.
22
otherTask := newBasicMockTask(2)
23
require.True(t, hm.Add(otherTask), "Add should have returned true on the first call")
24
require.False(t, hm.Add(otherTask), "Add should have returned false on the second call of the same task")
25
26
// Make sure that both of our added tasks can be returned.
27
tasks := collectMockTasks(hm)
28
require.ElementsMatch(t, tasks, []*mockTask{task, otherTask})
29
})
30
31
t.Run("tasks can be searched", func(t *testing.T) {
32
hm := newHashMap(0)
33
34
var (
35
task1 = newBasicMockTask(1)
36
task2 = newBasicMockTask(2)
37
)
38
39
// Add task1 into the hashMap, but not task2.
40
require.True(t, hm.Add(task1))
41
42
require.True(t, hm.Has(task1), "task1 should be in the hashMap")
43
require.False(t, hm.Has(task2), "task2 should not be in the hashMap")
44
})
45
46
t.Run("tasks can be deleted", func(t *testing.T) {
47
hm := newHashMap(0)
48
49
// Create two sets of tasks, where each set has exactly one hash collision
50
// with the other set. This helps us test that deletes work when there's
51
// collisions too.
52
var tasks []*mockTask
53
for i := 0; i < 10; i++ {
54
tasks = append(tasks, newBasicMockTask(uint64(i)))
55
}
56
for i := 0; i < 10; i++ {
57
tasks = append(tasks, newBasicMockTask(uint64(i)))
58
}
59
60
// Add tasks all at once.
61
for _, task := range tasks {
62
require.True(t, hm.Add(task))
63
}
64
65
// Start removing every task until there's none left.
66
expectCount := 20
67
for _, task := range tasks {
68
require.Len(t, collectMockTasks(hm), expectCount)
69
expectCount--
70
71
require.True(t, hm.Delete(task))
72
}
73
74
require.Len(t, collectMockTasks(hm), 0)
75
})
76
77
t.Run("tasks are deduplicated", func(t *testing.T) {
78
hm := newHashMap(0)
79
80
// Create two tasks with the same hash and that report they're equal to
81
// each other, even though they're separate pointers.
82
//
83
// The hashMap should deduplicate these tasks and only store one.
84
var (
85
task1 = &mockTask{
86
HashFunc: func() uint64 { return 1 },
87
EqualsFunc: func(t Task) bool { return true },
88
}
89
task2 = &mockTask{
90
HashFunc: func() uint64 { return 1 },
91
EqualsFunc: func(t Task) bool { return true },
92
}
93
)
94
95
require.True(t, hm.Add(task1))
96
require.False(t, hm.Add(task2))
97
98
// The hashMap should say that both task1 and task2 exist because they are
99
// considered duplicates.
100
require.True(t, hm.Has(task1))
101
require.True(t, hm.Has(task2))
102
103
// Make sure that both of our added tasks can be returned.
104
tasks := collectMockTasks(hm)
105
require.ElementsMatch(t, tasks, []*mockTask{task1})
106
})
107
108
t.Run("hash collisions are handled", func(t *testing.T) {
109
hm := newHashMap(0)
110
111
// Create two tasks with the same hash but that aren't equal to each other
112
// (because mockTask.Equals will check for pointer equality).
113
//
114
// It should be possible to store both tasks in the hashMap, despite having
115
// the same hash.
116
var (
117
task1 = newBasicMockTask(1)
118
task2 = newBasicMockTask(1)
119
)
120
121
require.True(t, hm.Add(task1))
122
require.True(t, hm.Add(task2))
123
124
require.True(t, hm.Has(task1))
125
require.True(t, hm.Has(task2))
126
127
// Make sure that both of our added tasks can be returned.
128
tasks := collectMockTasks(hm)
129
require.ElementsMatch(t, tasks, []*mockTask{task1, task2})
130
})
131
}
132
133
func Benchmark_hashMap(b *testing.B) {
134
b.Run("Add element", func(b *testing.B) {
135
task := newBasicMockTask(0)
136
137
for i := 0; i < b.N; i++ {
138
hm := newHashMap(1)
139
hm.Add(task)
140
}
141
})
142
143
b.Run("Remove element", func(b *testing.B) {
144
task := newBasicMockTask(0)
145
146
// Precreate the hash maps to only measure deletes.
147
var hashMaps []*hashMap
148
for i := 0; i < b.N; i++ {
149
hm := newHashMap(1)
150
hm.Add(task)
151
hashMaps = append(hashMaps, hm)
152
}
153
154
b.ResetTimer()
155
for i := 0; i < b.N; i++ {
156
hashMaps[i].Delete(task)
157
}
158
})
159
160
b.Run("Lookup element", func(b *testing.B) {
161
task := newBasicMockTask(0)
162
163
// Pre-add the task into the hash map so it's not counted as part of the
164
// performance benchmark.
165
hm := newHashMap(0)
166
hm.Add(task)
167
168
b.ResetTimer()
169
170
for i := 0; i < b.N; i++ {
171
hm.Has(task)
172
}
173
})
174
175
b.Run("Add existing element", func(b *testing.B) {
176
task := newBasicMockTask(0)
177
178
// Pre-add the task into the hash map so it's not counted as part of the
179
// performance benchmark.
180
hm := newHashMap(0)
181
hm.Add(task)
182
183
b.ResetTimer()
184
185
for i := 0; i < b.N; i++ {
186
hm.Add(task)
187
}
188
})
189
}
190
191
func Benchmark_hashMap_collisions(b *testing.B) {
192
collisionChecks := []int{1, 2, 5, 10, 100}
193
for _, collisions := range collisionChecks {
194
b.Run(fmt.Sprintf("%d collisions", collisions), func(b *testing.B) {
195
// Precreate the set of tasks. Each task should have the hash 0 to check
196
// for the hash collision handling performance.
197
var tasks []*mockTask
198
for i := 0; i < collisions+1; i++ {
199
tasks = append(tasks, newBasicMockTask(0))
200
}
201
202
b.Run("Add", func(b *testing.B) {
203
for i := 0; i < b.N; i++ {
204
hm := newHashMap(0)
205
for _, task := range tasks {
206
hm.Add(task)
207
}
208
}
209
})
210
211
b.Run("Remove", func(b *testing.B) {
212
hm := newHashMap(0)
213
for _, task := range tasks {
214
hm.Add(task)
215
}
216
b.ResetTimer()
217
218
for i := 0; i < b.N; i++ {
219
// Iterate in reverse to get worse-case performance.
220
for i := len(tasks) - 1; i >= 0; i-- {
221
hm.Delete(tasks[i])
222
}
223
}
224
})
225
226
b.Run("Lookup", func(b *testing.B) {
227
hm := newHashMap(0)
228
for _, task := range tasks {
229
hm.Add(task)
230
}
231
b.ResetTimer()
232
233
for i := 0; i < b.N; i++ {
234
for _, task := range tasks {
235
hm.Has(task)
236
}
237
}
238
})
239
})
240
}
241
}
242
243
func collectMockTasks(hm *hashMap) []*mockTask {
244
var res []*mockTask
245
for t := range hm.Iterate() {
246
res = append(res, t.(*mockTask))
247
}
248
return res
249
}
250
251
type mockTask struct {
252
HashFunc func() uint64
253
EqualsFunc func(t Task) bool
254
}
255
256
func newBasicMockTask(hash uint64) *mockTask {
257
return &mockTask{
258
HashFunc: func() uint64 { return hash },
259
}
260
}
261
262
var _ Task = (*mockTask)(nil)
263
264
func (mt *mockTask) Hash() uint64 {
265
if mt.HashFunc == nil {
266
return 0
267
}
268
return mt.HashFunc()
269
}
270
271
func (mt *mockTask) Equals(t Task) bool {
272
if mt.EqualsFunc == nil {
273
// Default to pointer comparison.
274
return mt == t.(*mockTask)
275
}
276
return mt.EqualsFunc(t)
277
}
278
279