Skip to content

Commit

Permalink
tools: Implement rw-benchmark line charts
Browse files Browse the repository at this point in the history
Add a new line chart to plot the results from the rw-benchmarks.

Signed-off-by: Ivan Valdes <ivan@vald.es>
  • Loading branch information
ivanvc committed Feb 28, 2025
1 parent 5c147c0 commit 15a1638
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 0 deletions.
14 changes: 14 additions & 0 deletions tools/rw-heatmaps/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var (
ErrMissingInputFileArg = fmt.Errorf("missing input file argument")
// ErrInvalidOutputFormat is returned when the output format is invalid.
ErrInvalidOutputFormat = fmt.Errorf("invalid output format, must be one of png, jpg, jpeg, tiff")
// ErrInvalidChartTYpe is returned when the chart type is invalid.
ErrInvalidChartType = fmt.Errorf("invalid chart type, must be one of line, heatmap")
)

// NewRootCommand returns the root command for the rw-heatmaps tool.
Expand All @@ -56,6 +58,10 @@ func NewRootCommand() *cobra.Command {
}
}

if o.chartType == "line" {
return chart.PlotLineCharts(datasets, o.title, o.outputImageFile, o.outputFormat)
}

return chart.PlotHeatMaps(datasets, o.title, o.outputImageFile, o.outputFormat, o.zeroCentered)
},
}
Expand All @@ -70,13 +76,15 @@ type options struct {
outputImageFile string
outputFormat string
zeroCentered bool
chartType string
}

// newOptions returns a new options for the command with the default values applied.
func newOptions() options {
return options{
outputFormat: "jpg",
zeroCentered: true,
chartType: "heatmap",
}
}

Expand All @@ -86,6 +94,7 @@ func (o *options) AddFlags(fs *pflag.FlagSet) {
fs.StringVarP(&o.outputImageFile, "output-image-file", "o", o.outputImageFile, "output image filename (required)")
fs.StringVarP(&o.outputFormat, "output-format", "f", o.outputFormat, "output image file format")
fs.BoolVar(&o.zeroCentered, "zero-centered", o.zeroCentered, "plot the improvement graph with white color represents 0.0")
fs.StringVarP(&o.chartType, "chart-type", "c", o.chartType, `type of chart to plot ["line", or "heatmap"]`)
}

// Validate returns an error if the options are invalid.
Expand All @@ -101,5 +110,10 @@ func (o *options) Validate() error {
default:
return ErrInvalidOutputFormat
}
switch o.chartType {
case "line", "heatmap":
default:
return ErrInvalidChartType
}
return nil
}
231 changes: 231 additions & 0 deletions tools/rw-heatmaps/pkg/chart/line_chart.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// Copyright 2025 The etcd 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 chart

import (
"cmp"
"fmt"
"image/color"
"slices"
"sort"

"gonum.org/v1/plot"
"gonum.org/v1/plot/font"
"gonum.org/v1/plot/plotter"
"gonum.org/v1/plot/plotutil"
"gonum.org/v1/plot/vg"
"gonum.org/v1/plot/vg/draw"
"gonum.org/v1/plot/vg/vgimg"

"go.etcd.io/etcd/tools/rw-heatmaps/v3/pkg/dataset"
)

// PlotLineCharts creates a new line chart.
func PlotLineCharts(datasets []*dataset.DataSet, title, outputImageFile, outputFormat string) error {
plot.DefaultFont = font.Font{
Typeface: "Liberation",
Variant: "Sans",
}

var canvas *vgimg.Canvas
canvas = plotLineChart(datasets, title)
if err := saveCanvas(canvas, "readwrite", outputImageFile, outputFormat); err != nil {
return err
}

return nil
}

func plotLineChart(datasets []*dataset.DataSet, title string) *vgimg.Canvas {
ratiosLength := func() int {
max := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int {
return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios()))
})
return len(max.GetSortedRatios())
}()

// Make a nx1 grid of line charts.
const cols = 1
rows := ratiosLength

// Set the width and height of the canvas.
width, height := 30*vg.Centimeter, 15*font.Length(ratiosLength)*vg.Centimeter

canvas := vgimg.New(width, height)
dc := draw.New(canvas)

// Create a tiled layout for the plots.
t := draw.Tiles{
Rows: rows,
Cols: cols,
PadX: vg.Millimeter * 4,
PadY: vg.Millimeter * 4,
PadTop: vg.Millimeter * 15,
PadBottom: vg.Millimeter * 2,
PadLeft: vg.Millimeter * 2,
PadRight: vg.Millimeter * 2,
}

plots := make([][]*plot.Plot, rows)
legends := make([]plot.Legend, rows)
for i := range plots {
plots[i] = make([]*plot.Plot, cols)
}

// Load records into the grid.
ratios := slices.MaxFunc(datasets, func(a, b *dataset.DataSet) int {
return cmp.Compare(len(a.GetSortedRatios()), len(b.GetSortedRatios()))
}).GetSortedRatios()

for row, ratio := range ratios {
var records [][]dataset.DataRecord
var fileNames []string
for _, d := range datasets {
records = append(records, d.Records[ratio])
fileNames = append(fileNames, d.FileName)
}
p, l := plotIndividualLineChart(fmt.Sprintf("R/W Ratio %0.04f", ratio), records, fileNames)
plots[row] = []*plot.Plot{p}
legends[row] = l
}

// Fill the canvas with the plots and legends.
canvases := plot.Align(plots, t, dc)
for i := 0; i < rows; i++ {
// Continue if there is no plot in the current cell (incomplete data).
if plots[i][0] == nil {
continue
}

l := legends[i]
r := l.Rectangle(canvases[i][cols])
legendWidth := r.Max.X - r.Min.X
// Adjust the legend down a little.
l.YOffs = plots[i][0].Title.TextStyle.FontExtents().Height * 3
l.Draw(canvases[i][0])

c := draw.Crop(canvases[i][cols], 0, -legendWidth-vg.Millimeter, 0, 0)
plots[i][0].Draw(c)
}

// Add the title and parameter legend.
l := plot.NewLegend()
l.Add(title)
for _, d := range datasets {
l.Add(fmt.Sprintf("%s: %s", d.FileName, d.Param))
}
l.Top = true
l.Left = true
l.Draw(dc)

return canvas
}

func plotIndividualLineChart(title string, records [][]dataset.DataRecord, fileNames []string) (*plot.Plot, plot.Legend) {
p := plot.New()
p.Title.Text = title
p.X.Label.Text = "Connections Amount"
p.X.Scale = plot.LogScale{}
p.X.Tick.Marker = pow2Ticks{}
p.Y.Label.Text = "QPS (Requests/sec)"
p.Y.Scale = plot.LogScale{}
p.Y.Tick.Marker = pow2Ticks{}

legend := plot.NewLegend()

values := getSortedValueSizes(records...)
for i, rs := range records {
rec := make(map[int64][]dataset.DataRecord)
for _, r := range rs {
rec[r.ValueSize] = append(rec[r.ValueSize], r)
}
if len(records) > 1 {
addValues(p, &legend, values, rec, i, fileNames[i])
} else {
addValues(p, &legend, values, rec, i, "")
}
}

return p, legend
}

func getSortedValueSizes(records ...[]dataset.DataRecord) []int {
valueMap := make(map[int64]struct{})
for _, rs := range records {
for _, r := range rs {
valueMap[r.ValueSize] = struct{}{}
}
}

var values []int
for v := range valueMap {
values = append(values, int(v))
}
sort.Ints(values)

return values
}

func addValues(p *plot.Plot, legend *plot.Legend, values []int, rec map[int64][]dataset.DataRecord, index int, fileName string) {
for i, value := range values {
r := rec[int64(value)]
readPts := make(plotter.XYs, len(r))
writePts := make(plotter.XYs, len(r))
for i, record := range r {
writePts[i].X = float64(record.ConnSize)
readPts[i].X = writePts[i].X
readPts[i].Y = record.AvgRead
writePts[i].Y = record.AvgWrite
}

readLine, s, err := plotter.NewLinePoints(readPts)
if err != nil {
panic(err)
}
if index == 0 {
readLine.Color = plotutil.Color(0)
} else {
readLine.Color = plotutil.Color(2)
}
readLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1))
readLine.Dashes = []vg.Length{vg.Points(6), vg.Points(2)}
s.Color = readLine.Color
p.Add(readLine, s)

writeLine, s, err := plotter.NewLinePoints(writePts)
if err != nil {
panic(err)
}
if index == 0 {
writeLine.Color = plotutil.Color(0)
} else {
writeLine.Color = plotutil.Color(2)
}
writeLine.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1))
s.Color = writeLine.Color
p.Add(writeLine, s)

if index == 0 {
l, _, _ := plotter.NewLinePoints(writePts)
l.Color = color.RGBA{0, 0, 0, 255}
l.Width = vg.Length(vg.Millimeter * 0.15 * vg.Length(i+1))
legend.Add(fmt.Sprintf("%d", value), plot.Thumbnailer(l))
}
if i == len(values)-1 {
legend.Add(fmt.Sprintf("read %s", fileName), plot.Thumbnailer(readLine))
legend.Add(fmt.Sprintf("write %s", fileName), plot.Thumbnailer(writeLine))
}
}
}

0 comments on commit 15a1638

Please sign in to comment.