• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubevirt / hyperconverged-cluster-operator / 20659155662

02 Jan 2026 01:45PM UTC coverage: 76.133% (+0.2%) from 75.973%
20659155662

Pull #3944

github

web-flow
Merge 5d2791ec5 into 9e75b320c
Pull Request #3944: Report recommended CPU models for heterogeneous clusters

126 of 142 new or added lines in 3 files covered. (88.73%)

28 existing lines in 1 file now uncovered.

8600 of 11296 relevant lines covered (76.13%)

1.79 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.18
/pkg/internal/nodeinfo/cpu_models.go
1
package nodeinfo
2

3
import (
4
        "sort"
5
        "strings"
6
        "sync"
7

8
        corev1 "k8s.io/api/core/v1"
9
        "k8s.io/apimachinery/pkg/api/resource"
10
        "k8s.io/apimachinery/pkg/util/sets"
11

12
        "github.com/kubevirt/hyperconverged-cluster-operator/api/v1beta1"
13
)
14

15
const (
16
        // CPU model labels prefix used by KubeVirt
17
        CpuModelLabelPrefix = "cpu-model.node.kubevirt.io/"
18

19
        // Weights for CPU model recommendation scoring
20
        benchmarkWeight = 0.50
21
        cpuWeight       = 0.20
22
        memoryWeight    = 0.15
23
        nodeWeight      = 0.15
24
)
25

26
// cpuModelPassMarkScores maps libvirt CPU model names to approximate PassMark scores.
27
// Keys must match exactly the cpu-model.node.kubevirt.io/* label values.
28
// The precise values do not matter much, this is intended only as a rough heuristic.
29
var cpuModelPassMarkScores = map[string]int{
30
        // Intel Broadwell
31
        "Broadwell":            7800,
32
        "Broadwell-IBRS":       7800,
33
        "Broadwell-noTSX":      7800,
34
        "Broadwell-noTSX-IBRS": 7800,
35
        "Broadwell-v1":         7800,
36
        "Broadwell-v2":         7800,
37
        "Broadwell-v3":         7800,
38
        "Broadwell-v4":         7800,
39

40
        // Intel Cascadelake
41
        "Cascadelake-Server":       22000,
42
        "Cascadelake-Server-noTSX": 22000,
43
        "Cascadelake-Server-v2":    22000,
44
        "Cascadelake-Server-v3":    22000,
45
        "Cascadelake-Server-v4":    22000,
46
        "Cascadelake-Server-v5":    22000,
47

48
        // Intel Cooperlake
49
        "Cooperlake":    24000,
50
        "Cooperlake-v1": 24000,
51
        "Cooperlake-v2": 24000,
52

53
        // Intel Denverton
54
        "Denverton":    3500,
55
        "Denverton-v1": 3500,
56
        "Denverton-v2": 3500,
57
        "Denverton-v3": 3500,
58

59
        // Intel Haswell
60
        "Haswell":            7200,
61
        "Haswell-IBRS":       7200,
62
        "Haswell-noTSX":      7200,
63
        "Haswell-noTSX-IBRS": 7200,
64
        "Haswell-v1":         7200,
65
        "Haswell-v2":         7200,
66
        "Haswell-v3":         7200,
67
        "Haswell-v4":         7200,
68

69
        // Intel Icelake
70
        "Icelake-Server":       25000,
71
        "Icelake-Server-noTSX": 25000,
72
        "Icelake-Server-v1":    25000,
73
        "Icelake-Server-v2":    25000,
74
        "Icelake-Server-v3":    25000,
75
        "Icelake-Server-v4":    25000,
76
        "Icelake-Server-v5":    25000,
77
        "Icelake-Server-v6":    25000,
78

79
        // Intel IvyBridge
80
        "IvyBridge":      6400,
81
        "IvyBridge-IBRS": 6400,
82
        "IvyBridge-v1":   6400,
83
        "IvyBridge-v2":   6400,
84

85
        // Intel Nehalem
86
        "Nehalem":      3800,
87
        "Nehalem-IBRS": 3800,
88
        "Nehalem-v1":   3800,
89
        "Nehalem-v2":   3800,
90

91
        // Intel Penryn
92
        "Penryn":    2400,
93
        "Penryn-v1": 2400,
94

95
        // Intel SandyBridge
96
        "SandyBridge":      5600,
97
        "SandyBridge-IBRS": 5600,
98
        "SandyBridge-v1":   5600,
99
        "SandyBridge-v2":   5600,
100

101
        // Intel SapphireRapids
102
        "SapphireRapids":    35000,
103
        "SapphireRapids-v1": 35000,
104
        "SapphireRapids-v2": 35000,
105

106
        // Intel Skylake-Client
107
        "Skylake-Client":            8900,
108
        "Skylake-Client-IBRS":       8900,
109
        "Skylake-Client-noTSX-IBRS": 8900,
110
        "Skylake-Client-v1":         8900,
111
        "Skylake-Client-v2":         8900,
112
        "Skylake-Client-v3":         8900,
113
        "Skylake-Client-v4":         8900,
114

115
        // Intel Skylake-Server
116
        "Skylake-Server":            15000,
117
        "Skylake-Server-IBRS":       15000,
118
        "Skylake-Server-noTSX-IBRS": 15000,
119
        "Skylake-Server-v1":         15000,
120
        "Skylake-Server-v2":         15000,
121
        "Skylake-Server-v3":         15000,
122
        "Skylake-Server-v4":         15000,
123
        "Skylake-Server-v5":         15000,
124

125
        // Intel Snowridge
126
        "Snowridge":    4500,
127
        "Snowridge-v1": 4500,
128
        "Snowridge-v2": 4500,
129
        "Snowridge-v3": 4500,
130
        "Snowridge-v4": 4500,
131

132
        // Intel Westmere
133
        "Westmere":      4200,
134
        "Westmere-IBRS": 4200,
135
        "Westmere-v1":   4200,
136
        "Westmere-v2":   4200,
137

138
        // Intel older
139
        "Conroe":    1800,
140
        "Conroe-v1": 1800,
141

142
        // AMD EPYC
143
        "EPYC":          25000,
144
        "EPYC-IBPB":     25000,
145
        "EPYC-v1":       25000,
146
        "EPYC-v2":       25000,
147
        "EPYC-v3":       25000,
148
        "EPYC-v4":       25000,
149
        "EPYC-Rome":     35000,
150
        "EPYC-Rome-v1":  35000,
151
        "EPYC-Rome-v2":  35000,
152
        "EPYC-Rome-v3":  35000,
153
        "EPYC-Rome-v4":  35000,
154
        "EPYC-Milan":    45000,
155
        "EPYC-Milan-v1": 45000,
156
        "EPYC-Milan-v2": 45000,
157
        "EPYC-Genoa":    55000,
158
        "EPYC-Genoa-v1": 55000,
159

160
        // AMD Opteron
161
        "Opteron_G1":    800,
162
        "Opteron_G1-v1": 800,
163
        "Opteron_G2":    1000,
164
        "Opteron_G2-v1": 1000,
165
        "Opteron_G3":    1400,
166
        "Opteron_G3-v1": 1400,
167
        "Opteron_G4":    4200,
168
        "Opteron_G4-v1": 4200,
169
        "Opteron_G5":    6800,
170
        "Opteron_G5-v1": 6800,
171

172
        // AMD Dhyana (Hygon)
173
        "Dhyana":    20000,
174
        "Dhyana-v1": 20000,
175
        "Dhyana-v2": 20000,
176

177
        // AMD phenom
178
        "phenom":    2800,
179
        "phenom-v1": 2800,
180
}
181

182
var (
183
        cpuModelInfo = &CpuModelCache{
184
                lock: &sync.RWMutex{},
185
        }
186

187
        maxPassMark = func() int {
21✔
188
                m := 0
21✔
189
                for _, score := range cpuModelPassMarkScores {
42✔
190
                        if score > m {
42✔
191
                                m = score
21✔
192
                        }
21✔
193
                }
194
                return m
21✔
195
        }()
196
)
197

198
type CpuModelCache struct {
199
        lock *sync.RWMutex
200
        // Pre-computed sorted list of top recommended models
201
        recommendedModels []v1beta1.CpuModelInfo
202
}
203

204
func GetRecommendedCpuModels() []v1beta1.CpuModelInfo {
2✔
205
        cpuModelInfo.lock.RLock()
2✔
206
        defer cpuModelInfo.lock.RUnlock()
2✔
207

2✔
208
        // Return a copy of the pre-computed result
2✔
209
        result := make([]v1beta1.CpuModelInfo, len(cpuModelInfo.recommendedModels))
2✔
210
        copy(result, cpuModelInfo.recommendedModels)
2✔
211
        return result
2✔
212
}
2✔
213

214
// calculateWeightedScore computes a weighted recommendation score considering:
215
// - PassMark performance - CPU performance indicator
216
// - Available CPU cores - fraction of cluster CPU
217
// - Memory - fraction of cluster memory
218
// - Number of nodes - fraction of cluster nodes
219
func calculateWeightedScore(model v1beta1.CpuModelInfo, totalNodes int, totalCpu, totalMemory float64) float64 {
1✔
220
        // PassMark score - normalized to 0-100 scale
1✔
221
        passMark := float64(model.Benchmark)
1✔
222
        passMarkScore := (passMark / float64(maxPassMark)) * 100.0 * benchmarkWeight
1✔
223

1✔
224
        // CPU - fraction of total cluster CPU
1✔
225
        cpuScore := 0.0
1✔
226
        if totalCpu > 0 && model.CPU != nil {
2✔
227
                cpuScore = (model.CPU.AsApproximateFloat64() / totalCpu) * 100.0 * cpuWeight
1✔
228
        }
1✔
229

230
        // Memory - fraction of total cluster memory
231
        memoryScore := 0.0
1✔
232
        if totalMemory > 0 && model.Memory != nil {
2✔
233
                memoryGB := model.Memory.AsApproximateFloat64() / (1024 * 1024 * 1024)
1✔
234
                memoryScore = (memoryGB / totalMemory) * 100.0 * memoryWeight
1✔
235
        }
1✔
236

237
        // Number of nodes - fraction of total cluster nodes
238
        nodeScore := 0.0
1✔
239
        if totalNodes > 0 {
2✔
240
                nodeScore = (float64(model.Nodes) / float64(totalNodes)) * 100.0 * nodeWeight
1✔
241
        }
1✔
242

243
        return passMarkScore + cpuScore + memoryScore + nodeScore
1✔
244
}
245

246
func processCpuModels(nodes []corev1.Node) bool {
1✔
247
        cpuModelCounts := make(map[string]int)
1✔
248
        cpuModelCores := make(map[string]*resource.Quantity)
1✔
249
        cpuModelMemory := make(map[string]*resource.Quantity)
1✔
250

1✔
251
        // Calculate cluster totals for normalization
1✔
252
        totalNodes := len(nodes)
1✔
253
        var totalCpu float64
1✔
254
        var totalMemory float64
1✔
255
        for _, node := range nodes {
2✔
256
                if cpu := node.Status.Capacity.Cpu(); cpu != nil {
2✔
257
                        totalCpu += cpu.AsApproximateFloat64()
1✔
258
                }
1✔
259
                if mem := node.Status.Capacity.Memory(); mem != nil {
2✔
260
                        totalMemory += mem.AsApproximateFloat64() / (1024 * 1024 * 1024) // Convert to GB
1✔
261
                }
1✔
262
        }
263

264
        for _, node := range nodes {
2✔
265
                cpuModels := extractCpuModels(node.Labels)
1✔
266
                memoryCapacity := node.Status.Capacity.Memory()
1✔
267
                cpuCapacity := node.Status.Capacity.Cpu()
1✔
268

1✔
269
                for _, cpuModel := range cpuModels {
2✔
270
                        cpuModelCounts[cpuModel]++
1✔
271

1✔
272
                        if cpuCapacity != nil {
2✔
273
                                if cpuModelCores[cpuModel] == nil {
2✔
274
                                        cpuModelCores[cpuModel] = resource.NewQuantity(0, resource.DecimalSI)
1✔
275
                                }
1✔
276
                                cpuModelCores[cpuModel].Add(*cpuCapacity)
1✔
277
                        }
278

279
                        if memoryCapacity != nil {
2✔
280
                                if cpuModelMemory[cpuModel] == nil {
2✔
281
                                        cpuModelMemory[cpuModel] = resource.NewQuantity(0, resource.BinarySI)
1✔
282
                                }
1✔
283
                                cpuModelMemory[cpuModel].Add(*memoryCapacity)
1✔
284
                        }
285
                }
286
        }
287

288
        models := make([]v1beta1.CpuModelInfo, 0, len(cpuModelCounts))
1✔
289
        for cpuModel, count := range cpuModelCounts {
2✔
290
                models = append(models, v1beta1.CpuModelInfo{
1✔
291
                        Name:      cpuModel,
1✔
292
                        Benchmark: getCpuModelPassMarkScore(cpuModel),
1✔
293
                        Nodes:     count,
1✔
294
                        CPU:       cpuModelCores[cpuModel],
1✔
295
                        Memory:    cpuModelMemory[cpuModel],
1✔
296
                })
1✔
297
        }
1✔
298

299
        // Sort by weighted score (highest first), then by name for stability
300
        sort.Slice(models, func(i, j int) bool {
2✔
301
                scoreI := calculateWeightedScore(models[i], totalNodes, totalCpu, totalMemory)
1✔
302
                scoreJ := calculateWeightedScore(models[j], totalNodes, totalCpu, totalMemory)
1✔
303
                if scoreI != scoreJ {
2✔
304
                        return scoreI > scoreJ
1✔
305
                }
1✔
NEW
306
                return models[i].Name < models[j].Name
×
307
        })
308

309
        // Keep top 4 models
310
        if len(models) > 4 {
2✔
311
                models = models[:4]
1✔
312
        }
1✔
313

314
        cpuModelInfo.lock.Lock()
1✔
315
        defer cpuModelInfo.lock.Unlock()
1✔
316

1✔
317
        changed := !recommendedModelsEqual(cpuModelInfo.recommendedModels, models)
1✔
318
        cpuModelInfo.recommendedModels = models
1✔
319
        return changed
1✔
320
}
321

322
func recommendedModelsEqual(a, b []v1beta1.CpuModelInfo) bool {
1✔
323
        if len(a) != len(b) {
2✔
324
                return false
1✔
325
        }
1✔
326
        for i := range a {
2✔
327
                if a[i].Name != b[i].Name || a[i].Benchmark != b[i].Benchmark || a[i].Nodes != b[i].Nodes {
1✔
NEW
328
                        return false
×
NEW
329
                }
×
330
                if !quantityEqual(a[i].CPU, b[i].CPU) || !quantityEqual(a[i].Memory, b[i].Memory) {
1✔
NEW
331
                        return false
×
NEW
332
                }
×
333
        }
334
        return true
1✔
335
}
336

337
func quantityEqual(a, b *resource.Quantity) bool {
1✔
338
        if a == nil && b == nil {
1✔
NEW
339
                return true
×
NEW
340
        }
×
341
        if a == nil || b == nil {
1✔
NEW
342
                return false
×
NEW
343
        }
×
344
        return a.Equal(*b)
1✔
345
}
346

347
func extractCpuModels(labels map[string]string) []string {
1✔
348
        cpuModels := sets.New[string]()
1✔
349

1✔
350
        for key, value := range labels {
2✔
351
                if strings.HasPrefix(key, CpuModelLabelPrefix) {
2✔
352
                        cpuModel := strings.TrimPrefix(key, CpuModelLabelPrefix)
1✔
353
                        // Only include if the value is "true" (meaning the node supports this model)
1✔
354
                        if value == "true" {
2✔
355
                                cpuModels.Insert(cpuModel)
1✔
356
                        }
1✔
357
                }
358
        }
359

360
        return cpuModels.UnsortedList()
1✔
361
}
362

363
func getCpuModelPassMarkScore(cpuModel string) int {
1✔
364
        if score, exists := cpuModelPassMarkScores[cpuModel]; exists {
2✔
365
                return score
1✔
366
        }
1✔
367
        return 0
1✔
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc