@@ -17,15 +17,34 @@ limitations under the License.
17
17
package benchmark
18
18
19
19
import (
20
+ "encoding/json"
21
+ "flag"
22
+ "fmt"
23
+ "io/ioutil"
24
+ "math"
25
+ "path"
26
+ "sort"
27
+ "time"
28
+
29
+ dto "github.com/prometheus/client_model/go"
20
30
v1 "k8s.io/api/core/v1"
21
31
"k8s.io/apimachinery/pkg/labels"
22
32
"k8s.io/apimachinery/pkg/runtime/schema"
23
33
coreinformers "k8s.io/client-go/informers/core/v1"
24
34
clientset "k8s.io/client-go/kubernetes"
25
35
restclient "k8s.io/client-go/rest"
36
+ "k8s.io/component-base/metrics/legacyregistry"
37
+ "k8s.io/klog"
26
38
"k8s.io/kubernetes/test/integration/util"
27
39
)
28
40
41
+ const (
42
+ dateFormat = "2006-01-02T15:04:05Z"
43
+ throughputSampleFrequency = time .Second
44
+ )
45
+
46
+ var dataItemsDir = flag .String ("data-items-dir" , "" , "destination directory for storing generated data items for perf dashboard" )
47
+
29
48
// mustSetupScheduler starts the following components:
30
49
// - k8s api server (a.k.a. master)
31
50
// - scheduler
@@ -66,3 +85,250 @@ func getScheduledPods(podInformer coreinformers.PodInformer) ([]*v1.Pod, error)
66
85
}
67
86
return scheduled , nil
68
87
}
88
+
89
+ // DataItem is the data point.
90
+ type DataItem struct {
91
+ // Data is a map from bucket to real data point (e.g. "Perc90" -> 23.5). Notice
92
+ // that all data items with the same label combination should have the same buckets.
93
+ Data map [string ]float64 `json:"data"`
94
+ // Unit is the data unit. Notice that all data items with the same label combination
95
+ // should have the same unit.
96
+ Unit string `json:"unit"`
97
+ // Labels is the labels of the data item.
98
+ Labels map [string ]string `json:"labels,omitempty"`
99
+ }
100
+
101
+ // DataItems is the data point set. It is the struct that perf dashboard expects.
102
+ type DataItems struct {
103
+ Version string `json:"version"`
104
+ DataItems []DataItem `json:"dataItems"`
105
+ }
106
+
107
+ func dataItems2JSONFile (dataItems DataItems , namePrefix string ) error {
108
+ b , err := json .Marshal (dataItems )
109
+ if err != nil {
110
+ return err
111
+ }
112
+
113
+ destFile := fmt .Sprintf ("%v_%v.json" , namePrefix , time .Now ().Format (dateFormat ))
114
+ if * dataItemsDir != "" {
115
+ destFile = path .Join (* dataItemsDir , destFile )
116
+ }
117
+
118
+ return ioutil .WriteFile (destFile , b , 0644 )
119
+ }
120
+
121
+ // prometheusCollector collects metrics from legacyregistry.DefaultGatherer.Gather() endpoint.
122
+ // Currently only Histrogram metrics are supported.
123
+ type prometheusCollector struct {
124
+ metric string
125
+ cache * dto.MetricFamily
126
+ }
127
+
128
+ func newPrometheusCollector (metric string ) * prometheusCollector {
129
+ return & prometheusCollector {
130
+ metric : metric ,
131
+ }
132
+ }
133
+
134
+ func (pc * prometheusCollector ) collect () * DataItem {
135
+ var metricFamily * dto.MetricFamily
136
+ m , err := legacyregistry .DefaultGatherer .Gather ()
137
+ if err != nil {
138
+ klog .Error (err )
139
+ return nil
140
+ }
141
+ for _ , mFamily := range m {
142
+ if mFamily .Name != nil && * mFamily .Name == pc .metric {
143
+ metricFamily = mFamily
144
+ break
145
+ }
146
+ }
147
+
148
+ if metricFamily == nil {
149
+ klog .Infof ("Metric %q not found" , pc .metric )
150
+ return nil
151
+ }
152
+
153
+ if metricFamily .GetMetric () == nil {
154
+ klog .Infof ("Metric %q is empty" , pc .metric )
155
+ return nil
156
+ }
157
+
158
+ if len (metricFamily .GetMetric ()) == 0 {
159
+ klog .Infof ("Metric %q is empty" , pc .metric )
160
+ return nil
161
+ }
162
+
163
+ // Histograms are stored under the first index (based on observation).
164
+ // Given there's only one histogram registered per each metric name, accessaing
165
+ // the first index is sufficient.
166
+ dataItem := pc .promHist2Summary (metricFamily .GetMetric ()[0 ].GetHistogram ())
167
+ if dataItem .Data == nil {
168
+ return nil
169
+ }
170
+
171
+ // clear the metrics so that next test always starts with empty prometheus
172
+ // metrics (since the metrics are shared among all tests run inside the same binary)
173
+ clearPromHistogram (metricFamily .GetMetric ()[0 ].GetHistogram ())
174
+
175
+ return dataItem
176
+ }
177
+
178
+ // Bucket of a histogram
179
+ type bucket struct {
180
+ upperBound float64
181
+ count float64
182
+ }
183
+
184
+ func bucketQuantile (q float64 , buckets []bucket ) float64 {
185
+ if q < 0 {
186
+ return math .Inf (- 1 )
187
+ }
188
+ if q > 1 {
189
+ return math .Inf (+ 1 )
190
+ }
191
+
192
+ if len (buckets ) < 2 {
193
+ return math .NaN ()
194
+ }
195
+
196
+ rank := q * buckets [len (buckets )- 1 ].count
197
+ b := sort .Search (len (buckets )- 1 , func (i int ) bool { return buckets [i ].count >= rank })
198
+
199
+ if b == 0 {
200
+ return buckets [0 ].upperBound * (rank / buckets [0 ].count )
201
+ }
202
+
203
+ // linear approximation of b-th bucket
204
+ brank := rank - buckets [b - 1 ].count
205
+ bSize := buckets [b ].upperBound - buckets [b - 1 ].upperBound
206
+ bCount := buckets [b ].count - buckets [b - 1 ].count
207
+
208
+ return buckets [b - 1 ].upperBound + bSize * (brank / bCount )
209
+ }
210
+
211
+ func (pc * prometheusCollector ) promHist2Summary (hist * dto.Histogram ) * DataItem {
212
+ buckets := []bucket {}
213
+
214
+ if hist .SampleCount == nil || * hist .SampleCount == 0 {
215
+ return & DataItem {}
216
+ }
217
+
218
+ if hist .SampleSum == nil || * hist .SampleSum == 0 {
219
+ return & DataItem {}
220
+ }
221
+
222
+ for _ , bckt := range hist .Bucket {
223
+ if bckt == nil {
224
+ return & DataItem {}
225
+ }
226
+ if bckt .UpperBound == nil || * bckt .UpperBound < 0 {
227
+ return & DataItem {}
228
+ }
229
+ buckets = append (buckets , bucket {
230
+ count : float64 (* bckt .CumulativeCount ),
231
+ upperBound : * bckt .UpperBound ,
232
+ })
233
+ }
234
+
235
+ // bucketQuantile expects the upper bound of the last bucket to be +inf
236
+ buckets [len (buckets )- 1 ].upperBound = math .Inf (+ 1 )
237
+
238
+ q50 := bucketQuantile (0.50 , buckets )
239
+ q90 := bucketQuantile (0.90 , buckets )
240
+ q99 := bucketQuantile (0.95 , buckets )
241
+
242
+ msFactor := float64 (time .Second ) / float64 (time .Millisecond )
243
+
244
+ return & DataItem {
245
+ Labels : map [string ]string {
246
+ "Metric" : pc .metric ,
247
+ },
248
+ Data : map [string ]float64 {
249
+ "Perc50" : q50 * msFactor ,
250
+ "Perc90" : q90 * msFactor ,
251
+ "Perc99" : q99 * msFactor ,
252
+ "Average" : (* hist .SampleSum / float64 (* hist .SampleCount )) * msFactor ,
253
+ },
254
+ Unit : "ms" ,
255
+ }
256
+ }
257
+
258
+ func clearPromHistogram (hist * dto.Histogram ) {
259
+ if hist .SampleCount != nil {
260
+ * hist .SampleCount = 0
261
+ }
262
+ if hist .SampleSum != nil {
263
+ * hist .SampleSum = 0
264
+ }
265
+ for _ , b := range hist .Bucket {
266
+ if b .CumulativeCount != nil {
267
+ * b .CumulativeCount = 0
268
+ }
269
+ if b .UpperBound != nil {
270
+ * b .UpperBound = 0
271
+ }
272
+ }
273
+ }
274
+
275
+ type throughputCollector struct {
276
+ podInformer coreinformers.PodInformer
277
+ schedulingThroughputs []float64
278
+ }
279
+
280
+ func newThroughputCollector (podInformer coreinformers.PodInformer ) * throughputCollector {
281
+ return & throughputCollector {
282
+ podInformer : podInformer ,
283
+ }
284
+ }
285
+
286
+ func (tc * throughputCollector ) run (stopCh chan struct {}) {
287
+ podsScheduled , err := getScheduledPods (tc .podInformer )
288
+ if err != nil {
289
+ klog .Fatalf ("%v" , err )
290
+ }
291
+ lastScheduledCount := len (podsScheduled )
292
+ for {
293
+ select {
294
+ case <- stopCh :
295
+ return
296
+ case <- time .After (throughputSampleFrequency ):
297
+ podsScheduled , err := getScheduledPods (tc .podInformer )
298
+ if err != nil {
299
+ klog .Fatalf ("%v" , err )
300
+ }
301
+
302
+ scheduled := len (podsScheduled )
303
+ samplingRatioSeconds := float64 (throughputSampleFrequency ) / float64 (time .Second )
304
+ throughput := float64 (scheduled - lastScheduledCount ) / samplingRatioSeconds
305
+ tc .schedulingThroughputs = append (tc .schedulingThroughputs , throughput )
306
+ lastScheduledCount = scheduled
307
+
308
+ klog .Infof ("%d pods scheduled" , lastScheduledCount )
309
+ }
310
+ }
311
+ }
312
+
313
+ func (tc * throughputCollector ) collect () * DataItem {
314
+ throughputSummary := & DataItem {}
315
+ if length := len (tc .schedulingThroughputs ); length > 0 {
316
+ sort .Float64s (tc .schedulingThroughputs )
317
+ sum := 0.0
318
+ for i := range tc .schedulingThroughputs {
319
+ sum += tc .schedulingThroughputs [i ]
320
+ }
321
+
322
+ throughputSummary .Labels = map [string ]string {
323
+ "Metric" : "SchedulingThroughput" ,
324
+ }
325
+ throughputSummary .Data = map [string ]float64 {
326
+ "Average" : sum / float64 (length ),
327
+ "Perc50" : tc .schedulingThroughputs [int (math .Ceil (float64 (length * 50 )/ 100 ))- 1 ],
328
+ "Perc90" : tc .schedulingThroughputs [int (math .Ceil (float64 (length * 90 )/ 100 ))- 1 ],
329
+ "Perc99" : tc .schedulingThroughputs [int (math .Ceil (float64 (length * 99 )/ 100 ))- 1 ],
330
+ }
331
+ throughputSummary .Unit = "pods/s"
332
+ }
333
+ return throughputSummary
334
+ }
0 commit comments