minikube/hack/benchmark/image-build/generate-chart.go

300 lines
7.5 KiB
Go

/*
Copyright 2023 The Kubernetes Authors All rights reserved.
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/csv"
"encoding/json"
"flag"
"fmt"
"image/color"
"io"
"log"
"math"
"os"
"path/filepath"
"strconv"
"time"
"gonum.org/v1/plot"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/plotutil"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
)
var Images = []string{
"buildpacksFewLargeFiles",
// to simplify the output, the following images are omitted
// "buildpacksFewSmallFiles",
// "buildpacksManyLargeFiles",
// "buildpacksManySmallFiles",
}
var Environments = []string{
"MinikubeImageLoadDocker",
"MinikubeImageBuild",
"MinikubeDockerEnvDocker",
"MinikubeAddonRegistryDocker",
"MinikubeImageLoadContainerd",
"MinikubeImageContainerd",
"MinikubeDockerEnvContainerd",
"MinikubeAddonRegistryContainerd",
"MinikubeImageLoadCrio",
"MinikubeImageCrio",
"MinikubeAddonRegistryCrio",
"Kind",
"K3d",
"Microk8s",
}
var RuntimeMethods = map[string][]string{
"docker": {
"MinikubeImageLoadDocker",
"MinikubeImageBuild",
"MinikubeDockerEnvDocker",
"MinikubeAddonRegistryDocker",
},
"containerd": {
"MinikubeImageLoadContainerd",
"MinikubeImageContainerd",
"MinikubeDockerEnvContainerd",
"MinikubeAddonRegistryContainerd",
},
}
const (
INTERATIVE = "Iterative"
NONINTERATIVE = "NonIterative"
)
var Itrs = []string{
INTERATIVE,
// to simplify the output, non-interative is omitted
// NONINTERATIVE,
}
// method name-> test result
type TestResult map[string]float64
func NewTestResult(values []float64) TestResult {
res := make(TestResult)
for index, v := range values {
res[Environments[index]] = v
}
return res
}
// imageName->TestResult
type ImageTestResults map[string]TestResult
type ItrTestResults struct {
Date time.Time
// itr name -> results
Results map[string]ImageTestResults
}
type Records struct {
Records []ItrTestResults
}
func main() {
latestTestResultPath := flag.String("csv", "", "path to the CSV file containing the latest benchmark result")
pastTestRecordsPath := flag.String("past-runs", "", "path to the JSON file containing the past benchmark results")
chartsPath := flag.String("charts", "", "path to the folder to write the daily charts to")
flag.Parse()
latestBenchmark := readInLatestTestResult(*latestTestResultPath)
latestBenchmark.Date = time.Now()
pastBenchmarks := readInPastTestResults(*pastTestRecordsPath)
pastBenchmarks.Records = append(pastBenchmarks.Records, latestBenchmark)
updatePastTestResults(pastBenchmarks, *pastTestRecordsPath)
createDailyChart(pastBenchmarks, *chartsPath)
}
// readInLatestTestResult reads in the latest benchmark result from a CSV file
// and return the MethodTestResults object
func readInLatestTestResult(latestBenchmarkPath string) ItrTestResults {
var res = ItrTestResults{
Results: make(map[string]ImageTestResults),
}
res.Results[INTERATIVE] = make(ImageTestResults)
res.Results[NONINTERATIVE] = make(ImageTestResults)
f, err := os.Open(latestBenchmarkPath)
if err != nil {
log.Fatal(err)
}
r := csv.NewReader(f)
for {
line, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
// skip the first line of the CSV file
if line[0] == "image" {
continue
}
valuesInterative := []float64{}
valuesNonInterative := []float64{}
// interative test results of each env are stored in the following columns
indicesInterative := []int{1, 5, 9, 13, 17, 21, 25, 29, 33, 37, 41, 45, 49, 53}
// non-interative test results of each env are stored in the following columns
indicesNonInterative := []int{3, 7, 11, 15, 19, 23, 27, 31, 35, 39, 43, 47, 51, 55}
for _, i := range indicesInterative {
if line[i] == "NaN" {
// we use -1 as invalid value
valuesInterative = append(valuesInterative, -1)
continue
}
v, err := strconv.ParseFloat(line[i], 64)
if err != nil {
log.Fatal(err)
}
valuesInterative = append(valuesInterative, v)
}
for _, i := range indicesNonInterative {
if line[i] == "NaN" {
// we use -1 as invalid value
valuesNonInterative = append(valuesNonInterative, -1)
continue
}
v, err := strconv.ParseFloat(line[i], 64)
if err != nil {
log.Fatal(err)
}
valuesNonInterative = append(valuesNonInterative, v)
}
imageName := line[0]
res.Results[INTERATIVE][imageName] = NewTestResult(valuesInterative)
res.Results[NONINTERATIVE][imageName] = NewTestResult(valuesNonInterative)
}
return res
}
// readInPastTestResults reads in the past benchmark results from a JSON file
func readInPastTestResults(pastTestRecordPath string) Records {
record := Records{}
data, err := os.ReadFile(pastTestRecordPath)
if os.IsNotExist(err) {
return record
}
if err != nil {
log.Fatal(err)
}
if err := json.Unmarshal(data, &record); err != nil {
log.Fatal(err)
}
return record
}
// updatePastTestResults overwrites the run file with the updated benchmarks list
func updatePastTestResults(h Records, pastTestRecordPath string) {
b, err := json.Marshal(h)
if err != nil {
log.Fatal(err)
}
if err := os.WriteFile(pastTestRecordPath, b, 0600); err != nil {
log.Fatal(err)
}
}
func createDailyChart(record Records, outputFolder string) {
for _, itr := range Itrs {
for _, image := range Images {
createChart(record, itr, image, "docker", outputFolder)
createChart(record, itr, image, "containerd", outputFolder)
}
}
}
func createChart(record Records, itr string, imageName string, runtime string, chartOutputPath string) {
p := plot.New()
p.Add(plotter.NewGrid())
p.Legend.Top = true
p.Title.Text = fmt.Sprintf("%s-%s-%s-performance", itr, imageName, runtime)
p.X.Label.Text = "date"
p.X.Tick.Marker = plot.TimeTicks{Format: "2006-01-02"}
p.Y.Label.Text = "time (seconds)"
yMaxTotal := float64(0)
// gonum plot do not have enough default colors in any group
// so we combine different group of default colors
colors := append([]color.Color{}, plotutil.SoftColors...)
colors = append(colors, plotutil.DarkColors...)
pointGroup := make(map[string]plotter.XYs)
for _, name := range RuntimeMethods[runtime] {
pointGroup[name] = make(plotter.XYs, 0)
}
for i := 0; i < len(record.Records); i++ {
for _, method := range RuntimeMethods[runtime] {
// for invalid values(<0) this point is dropped
if v, ok := record.Records[i].Results[itr][imageName][method]; ok && v > 0 {
point := plotter.XY{
X: float64(record.Records[i].Date.Unix()),
Y: record.Records[i].Results[itr][imageName][method],
}
pointGroup[method] = append(pointGroup[method], point)
yMaxTotal = math.Max(yMaxTotal, point.Y)
}
}
}
p.Y.Max = yMaxTotal
i := 0
for method, xys := range pointGroup {
line, points, err := plotter.NewLinePoints(xys)
if err != nil {
log.Fatal(err)
}
line.Color = colors[i]
points.Color = colors[i]
points.Shape = draw.CircleGlyph{}
i++
p.Add(line, points)
p.Legend.Add(method, line)
}
filename := filepath.Join(chartOutputPath, fmt.Sprintf("%s_%s_%s_chart.png", itr, imageName, runtime))
if err := p.Save(12*vg.Inch, 8*vg.Inch, filename); err != nil {
log.Fatalf("failed creating png: %v", err)
}
}