internal/reportgenerator/generators/pipelinebench/generator.go (155 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package benchmark
import (
"encoding/json"
"encoding/xml"
"fmt"
"io/fs"
"math"
"os"
"path/filepath"
"github.com/elastic/elastic-package/internal/benchrunner/runners/pipeline"
"github.com/elastic/elastic-package/internal/reportgenerator"
)
const (
// ReportType defining benchmark reports.
ReportType reportgenerator.ReportType = "benchmark"
)
type Report struct {
Package string
DataStream string
Old float64
New float64
Diff float64
Percentage float64
}
type Reports map[string][]Report
type generator struct {
options reportgenerator.ReportOptions
}
// Type returns the type of benchmark that can be run by this benchmark runner.
func (*generator) Type() reportgenerator.ReportType {
return ReportType
}
// String returns the human-friendly name of the benchmark runner.
func (*generator) String() string {
return "benchmark"
}
// Format returns the format used by the report.
func (*generator) Format() string {
return "md"
}
// Run runs the pipeline benchmarks defined under the given folder
func (g *generator) Generate(options reportgenerator.ReportOptions) ([]byte, error) {
g.options = options
return g.generate()
}
func (g *generator) generate() ([]byte, error) {
// get all results from new
newResults, err := listAllDirResults(g.options.NewPath)
if err != nil {
return nil, fmt.Errorf("listing new results failed: %w", err)
}
// get all results from old
oldResults, err := listAllDirResultsAsMap(g.options.OldPath)
if err != nil {
return nil, fmt.Errorf("listing old results failed: %w", err)
}
// lookup new reports in the old ones and compare
reports := Reports{}
for _, entry := range newResults {
newRes, err := readResult(g.options.NewPath, entry)
if err != nil {
return nil, fmt.Errorf("reading new result: %w", err)
}
pkg, ds := newRes.Package, newRes.DataStream
var oldRes pipeline.BenchmarkResult
if oldEntry, found := oldResults[pkg]; found {
if ds, found := oldEntry[ds]; found {
oldRes, err = readResult(g.options.OldPath, ds)
if err != nil {
return nil, fmt.Errorf("reading old result: %w", err)
}
}
}
report := createReport(newRes, oldRes)
reports[report.Package] = append(reports[report.Package], report)
}
return g.markdownFormat(reports)
}
func createReport(new, old pipeline.BenchmarkResult) Report {
var r Report
r.Package, r.DataStream = new.Package, new.DataStream
// we round all the values to 2 decimals approximations
r.New = roundFloat64(getEPS(new))
r.Old = roundFloat64(getEPS(old))
r.Diff = roundFloat64(r.New - r.Old)
if r.Old > 0 {
r.Percentage = roundFloat64((r.Diff / r.Old) * 100)
}
return r
}
func getEPS(r pipeline.BenchmarkResult) float64 {
for _, test := range r.Tests {
for _, res := range test.Results {
if res.Name == "eps" {
v, _ := res.Value.(float64)
return v
}
}
}
return 0
}
func roundFloat64(v float64) float64 {
return math.Round(v*100) / 100
}
func listAllDirResults(path string) ([]os.DirEntry, error) {
entries, err := os.ReadDir(path)
if err != nil {
return nil, fmt.Errorf("reading directory failed (path: %s): %w", path, err)
}
// only keep results, scan is not recursive
var filtered []os.DirEntry
for _, e := range entries {
if e.IsDir() || !resultExts[filepath.Ext(e.Name())] {
continue
}
filtered = append(filtered, e)
}
return filtered, nil
}
func listAllDirResultsAsMap(path string) (map[string]map[string]fs.DirEntry, error) {
entries, err := listAllDirResults(path)
if err != nil {
return nil, err
}
m := map[string]map[string]fs.DirEntry{}
for _, entry := range entries {
res, err := readResult(path, entry)
if err != nil {
return nil, fmt.Errorf("reading result: %w", err)
}
pkg, ds := res.Package, res.DataStream
if m[pkg] == nil {
m[pkg] = map[string]fs.DirEntry{}
}
m[pkg][ds] = entry
}
return m, nil
}
func readResult(path string, e fs.DirEntry) (pipeline.BenchmarkResult, error) {
fi, err := e.Info()
if err != nil {
return pipeline.BenchmarkResult{}, fmt.Errorf("getting file info failed (file: %s): %w", e.Name(), err)
}
b, err := os.ReadFile(path + string(os.PathSeparator) + fi.Name())
if err != nil {
return pipeline.BenchmarkResult{}, fmt.Errorf("reading result contents (file: %s): %w", fi.Name(), err)
}
var br pipeline.BenchmarkResult
switch ext := filepath.Ext(fi.Name()); ext {
case ".json":
if err := json.Unmarshal(b, &br); err != nil {
return pipeline.BenchmarkResult{}, fmt.Errorf("decoding json (file: %s): %w", fi.Name(), err)
}
case ".xml":
if err := xml.Unmarshal(b, &br); err != nil {
return pipeline.BenchmarkResult{}, fmt.Errorf("decoding xml (file: %s): %w", fi.Name(), err)
}
default:
return pipeline.BenchmarkResult{}, fmt.Errorf("unsupported result format: %v", ext)
}
return br, nil
}
func init() {
reportgenerator.RegisterGenerator(&generator{})
}