perfdash/parser.go (378 lines of code) (raw):

/* Copyright 2016 The Kubernetes Authors. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package main import ( "encoding/json" "fmt" "math" "regexp" "sort" "strconv" "strings" "time" "k8s.io/klog" "k8s.io/kubernetes/test/e2e/framework/metrics" "k8s.io/kubernetes/test/e2e/perftype" ) func stripCount(data *perftype.DataItem) { delete(data.Labels, "Count") delete(data.Labels, "SlowCount") } func createRequestCountData(data *perftype.DataItem) error { data.Unit = "" data.Data = make(map[string]float64) requestCountString, ok := data.Labels["Count"] if !ok { return fmt.Errorf("no 'Count' label") } requestCount, err := strconv.ParseFloat(requestCountString, 64) if err != nil { return fmt.Errorf("couldn't parse count: %v", err) } data.Data["RequestCount"] = requestCount return nil } type ContainerRestartsInfo struct { Container string Pod string Namespace string RestartCount int } func parseContainerRestarts(data []byte, buildNumber int, testResult *BuildData) { build := fmt.Sprintf("%d", buildNumber) restarts := make([]ContainerRestartsInfo, 0) if err := json.Unmarshal(data, &restarts); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } restartsByContainer := make(map[string]float64) for _, restart := range restarts { restartsByContainer[restart.Container] = restartsByContainer[restart.Container] + float64(restart.RestartCount) } res := perftype.DataItem{Unit: "", Labels: map[string]string{"RestartCount": "RestartCount"}, Data: restartsByContainer} testResult.Builds[build] = append(testResult.Builds[build], res) } func parsePerfData(data []byte, buildNumber int, testResult *BuildData) { build := fmt.Sprintf("%d", buildNumber) obj := perftype.PerfData{} if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } if testResult.Version == "" { testResult.Version = obj.Version } if testResult.Version == obj.Version { for i := range obj.DataItems { stripCount(&obj.DataItems[i]) testResult.Builds[build] = append(testResult.Builds[build], obj.DataItems[i]) } } } type resourceUsagePercentiles map[string][]resourceUsages type resourceUsages struct { Name string `json:"Name"` CPU float64 `json:"CPU"` Memory int `json:"Mem"` } type resourceUsage struct { CPU float64 Memory float64 } type usageAtPercentiles map[string]resourceUsage type podNameToUsage map[string]usageAtPercentiles func parseResourceUsageData(data []byte, buildNumber int, testResult *BuildData) { testResult.Version = "v1" build := fmt.Sprintf("%d", buildNumber) var obj resourceUsagePercentiles if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } usage := make(podNameToUsage) for percentile, items := range obj { for _, item := range items { name := RemoveDisambiguationInfixes(item.Name) if _, ok := usage[name]; !ok { usage[name] = make(usageAtPercentiles) } cpu, memory := float64(item.CPU), float64(item.Memory) if otherUsage, ok := usage[name][percentile]; ok { // Note that we take max of each resource separately, potentially manufacturing a // "franken-sample" which was never seen in the wild. We do this hoping that such result // will be more stable across runs. cpu = math.Max(cpu, otherUsage.CPU) memory = math.Max(memory, otherUsage.Memory) } usage[name][percentile] = resourceUsage{cpu, memory} } } for podName, usageAtPercentiles := range usage { cpu := perftype.DataItem{Unit: "cores", Labels: map[string]string{"PodName": podName, "Resource": "CPU"}, Data: make(map[string]float64)} memory := perftype.DataItem{Unit: "MiB", Labels: map[string]string{"PodName": podName, "Resource": "memory"}, Data: make(map[string]float64)} for percentile, usage := range usageAtPercentiles { cpu.Data[percentile] = usage.CPU memory.Data[percentile] = usage.Memory / (1024 * 1024) } testResult.Builds[build] = append(testResult.Builds[build], cpu) testResult.Builds[build] = append(testResult.Builds[build], memory) } } func parseRequestCountData(data []byte, buildNumber int, testResult *BuildData) { build := fmt.Sprintf("%d", buildNumber) obj := perftype.PerfData{} if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } if testResult.Version == "" { testResult.Version = obj.Version } if testResult.Version == obj.Version { for i := range obj.DataItems { if err := createRequestCountData(&obj.DataItems[i]); err != nil { klog.Errorf("error creating request count data in build %d dataItem %d: %v", buildNumber, i, err) continue } stripCount(&obj.DataItems[i]) testResult.Builds[build] = append(testResult.Builds[build], obj.DataItems[i]) } } } var commitMatcher = regexp.MustCompile("kubernetes/.{7}") var versionMatcher = regexp.MustCompile(`\/v?\d+\.\d+.\d+`) func parseApiserverRequestCount(data []byte, buildNumber int, testResult *BuildData) { testResult.Version = "v1" build := fmt.Sprintf("%d", buildNumber) var obj metrics.Collection if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } if obj.APIServerMetrics == nil { klog.Errorf("no ApiServerMetrics data in build %d", buildNumber) return } metric, ok := obj.APIServerMetrics["apiserver_request_count"] if !ok { klog.Errorf("no apiserver_request_count metric data in build %d", buildNumber) return } resultMap := make(map[string]*perftype.DataItem) for i := range metric { perfData := perftype.DataItem{Unit: "", Data: make(map[string]float64), Labels: make(map[string]string)} for k, v := range metric[i].Metric { perfData.Labels[string(k)] = string(v) } delete(perfData.Labels, "__name__") delete(perfData.Labels, "contentType") dataLabel := "RequestCount" if client, ok := perfData.Labels["client"]; ok { // Client label contains kubernetes version, which is different // in every build. This causes unnecessary creation on multiple different label sets // for one metric. // This fix removes kubernetes version from client label. newClient := commitMatcher.ReplaceAllString(client, "kubernetes") if version := versionMatcher.Find([]byte(newClient)); version != nil { dataLabel = string(version) newClient = strings.Replace(newClient, dataLabel, "", 1) } perfData.Labels["client"] = newClient } perfData.Data[dataLabel] = float64(metric[i].Value) key := createMapID(perfData.Labels) if result, exists := resultMap[key]; exists { result.Data[dataLabel] += perfData.Data[dataLabel] continue } resultMap[key] = &perfData testResult.Builds[build] = append(testResult.Builds[build], perfData) } } func parseApiserverInitEventsCount(data []byte, buildNumber int, testResult *BuildData) { testResult.Version = "v1" build := fmt.Sprintf("%d", buildNumber) var obj metrics.Collection if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } if obj.APIServerMetrics == nil { klog.Errorf("no ApiServerMetrics data in build %d", buildNumber) return } metric, ok := obj.APIServerMetrics["apiserver_init_events_total"] if !ok { klog.Errorf("no apiserver_init_events_total metric data in build %d", buildNumber) return } for i := range metric { perfData := perftype.DataItem{Unit: "", Data: make(map[string]float64), Labels: make(map[string]string)} for k, v := range metric[i].Metric { perfData.Labels[string(k)] = string(v) } delete(perfData.Labels, "__name__") perfData.Data["InitEventsCount"] = float64(metric[i].Value) testResult.Builds[build] = append(testResult.Builds[build], perfData) } } func createMapID(m map[string]string) string { var keys []string for key := range m { keys = append(keys, key) } sort.Strings(keys) var b strings.Builder for _, key := range keys { b.WriteString(fmt.Sprintf("%s:%s|", key, m[key])) } return b.String() } // TODO(krzysied): Copy of structures from kuberentes repository. // At some point latencyMetric and schedulingMetrics should be moved to metric package. type latencyMetric struct { Perc50 time.Duration `json:"Perc50"` Perc90 time.Duration `json:"Perc90"` Perc99 time.Duration `json:"Perc99"` } type schedulingMetrics struct { PreemptionEvaluationLatency latencyMetric `json:"preemptionEvaluationLatency"` E2eSchedulingLatency latencyMetric `json:"e2eSchedulingLatency"` SchedulingLatency latencyMetric `json:"schedulingLatency"` FrameworkExtensionPointDuration map[string]latencyMetric `json:"frameworkExtensionPointDuration"` } func parseOperationLatency(latency latencyMetric, testName string, operationName string) perftype.DataItem { perfData := perftype.DataItem{Unit: "ms", Labels: map[string]string{"TestName": testName, "Operation": operationName}, Data: make(map[string]float64)} perfData.Data["Perc50"] = float64(latency.Perc50) / float64(time.Millisecond) perfData.Data["Perc90"] = float64(latency.Perc90) / float64(time.Millisecond) perfData.Data["Perc99"] = float64(latency.Perc99) / float64(time.Millisecond) return perfData } func parseSchedulingLatency(testName string) func([]byte, int, *BuildData) { return func(data []byte, buildNumber int, testResult *BuildData) { testResult.Version = "v1" build := fmt.Sprintf("%d", buildNumber) var obj schedulingMetrics if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } preemptionEvaluation := parseOperationLatency(obj.PreemptionEvaluationLatency, testName, "preemption_evaluation") testResult.Builds[build] = append(testResult.Builds[build], preemptionEvaluation) e2eScheduling := parseOperationLatency(obj.E2eSchedulingLatency, testName, "e2eScheduling") testResult.Builds[build] = append(testResult.Builds[build], e2eScheduling) scheduling := parseOperationLatency(obj.SchedulingLatency, testName, "scheduling") testResult.Builds[build] = append(testResult.Builds[build], scheduling) for name, metric := range obj.FrameworkExtensionPointDuration { frameworkExtensionPointDuration := parseOperationLatency(metric, testName, name) testResult.Builds[build] = append(testResult.Builds[build], frameworkExtensionPointDuration) } } } type schedulingThroughputMetric struct { Average float64 `json:"average"` Max float64 `json:"max"` Perc50 float64 `json:"perc50"` Perc90 float64 `json:"perc90"` Perc99 float64 `json:"perc99"` } func parseSchedulingThroughputCL(testName string) func([]byte, int, *BuildData) { return func(data []byte, buildNumber int, testResult *BuildData) { testResult.Version = "v1" build := fmt.Sprintf("%d", buildNumber) var obj schedulingThroughputMetric if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } perfData := perftype.DataItem{Unit: "1/s", Labels: map[string]string{"TestName": testName}, Data: make(map[string]float64)} perfData.Data["Perc50"] = obj.Perc50 perfData.Data["Perc90"] = obj.Perc90 perfData.Data["Perc99"] = obj.Perc99 perfData.Data["Average"] = obj.Average perfData.Data["Max"] = obj.Max testResult.Builds[build] = append(testResult.Builds[build], perfData) } } // TODO(krzysied): This structure also should be moved to metric package. type histogram struct { Labels map[string]string `json:"labels"` Buckets map[string]int `json:"buckets"` } type histogramVec []histogram type etcdMetrics struct { BackendCommitDuration histogramVec `json:"backendCommitDuration"` SnapshotSaveTotalDuration histogramVec `json:"snapshotSaveTotalDuration"` PeerRoundTripTime histogramVec `json:"peerRoundTripTime"` WalFsyncDuration histogramVec `json:"walFsyncDuration"` MaxDatabaseSize float64 `json:"maxDatabaseSize"` } func parseHistogramMetric(metricName string) func(data []byte, buildNumber int, testResult *BuildData) { return func(data []byte, buildNumber int, testResult *BuildData) { testResult.Version = "v1" build := fmt.Sprintf("%d", buildNumber) var obj etcdMetrics if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } var histogramVecMetric histogramVec switch metricName { case "backendCommitDuration": histogramVecMetric = obj.BackendCommitDuration case "snapshotSaveTotalDuration": histogramVecMetric = obj.SnapshotSaveTotalDuration case "peerRoundTripTime": histogramVecMetric = obj.PeerRoundTripTime case "walFsyncDuration": histogramVecMetric = obj.WalFsyncDuration default: klog.Errorf("unknown metric name: %s", metricName) } for i := range histogramVecMetric { perfData := perftype.DataItem{Unit: "%", Labels: histogramVecMetric[i].Labels, Data: make(map[string]float64)} delete(perfData.Labels, "__name__") count, exists := histogramVecMetric[i].Buckets["+Inf"] if !exists { klog.Errorf("err in build %d: no +Inf bucket: %s", buildNumber, string(data)) continue } for bucket, buckerVal := range histogramVecMetric[i].Buckets { if bucket != "+Inf" { if count == 0 { perfData.Data["<= "+bucket+"s"] = 0 continue } perfData.Data["<= "+bucket+"s"] = float64(buckerVal) / float64(count) * 100 } } testResult.Builds[build] = append(testResult.Builds[build], perfData) } } } func parseSystemPodMetrics(data []byte, buildNumber int, testResult *BuildData) { type containerMetrics struct { Name string `json:"name"` RestartCount int32 `json:"restartCount"` } type podMetrics struct { Name string `json:"name"` Containers []containerMetrics `json:"containers"` } type systemPodsMetrics struct { Pods []podMetrics `json:"pods"` } build := fmt.Sprintf("%d", buildNumber) var obj systemPodsMetrics if err := json.Unmarshal(data, &obj); err != nil { klog.Errorf("error parsing JSON in build %d: %v %s", buildNumber, err, string(data)) return } restartCounts := make(map[string]float64) for _, pod := range obj.Pods { for _, container := range pod.Containers { cnt := float64(container.RestartCount) if v, ok := restartCounts[container.Name]; ok { restartCounts[container.Name] = math.Max(v, cnt) } else { restartCounts[container.Name] = cnt } } } perfData := perftype.DataItem{ Unit: "", Labels: map[string]string{ "RestartCount": "RestartCount", }, Data: restartCounts, } testResult.Builds[build] = append(testResult.Builds[build], perfData) }