LCOV - code coverage report
Current view: top level - pebble/internal/mkbench - ycsb.go (source / functions) Hit Total Coverage
Test: 2024-07-22 08:17Z 72c3f550 - tests + meta.lcov Lines: 167 202 82.7 %
Date: 2024-07-22 08:18:33 Functions: 0 0 -

          Line data    Source code
       1             : // Copyright 2020 The LevelDB-Go and Pebble Authors. All rights reserved. Use
       2             : // of this source code is governed by a BSD-style license that can be found in
       3             : // the LICENSE file.
       4             : 
       5             : package main
       6             : 
       7             : import (
       8             :         "bufio"
       9             :         "bytes"
      10             :         "compress/bzip2"
      11             :         "compress/gzip"
      12             :         "encoding/json"
      13             :         "fmt"
      14             :         "io"
      15             :         "log"
      16             :         "math"
      17             :         "os"
      18             :         "sort"
      19             :         "strings"
      20             : 
      21             :         "github.com/cockroachdb/errors/oserror"
      22             :         "github.com/spf13/cobra"
      23             : )
      24             : 
      25             : const (
      26             :         defaultDir        = "data"
      27             :         defaultCookedFile = "data.js"
      28             : )
      29             : 
      30           1 : func getYCSBCommand() *cobra.Command {
      31           1 :         c := &cobra.Command{
      32           1 :                 Use:   "ycsb",
      33           1 :                 Short: "parse YCSB benchmark data",
      34           1 :                 RunE: func(cmd *cobra.Command, args []string) error {
      35           0 :                         dataDir, err := cmd.Flags().GetString("dir")
      36           0 :                         if err != nil {
      37           0 :                                 return err
      38           0 :                         }
      39             : 
      40           0 :                         inFile, err := cmd.Flags().GetString("in")
      41           0 :                         if err != nil {
      42           0 :                                 return err
      43           0 :                         }
      44             : 
      45           0 :                         outFile, err := cmd.Flags().GetString("out")
      46           0 :                         if err != nil {
      47           0 :                                 return err
      48           0 :                         }
      49             : 
      50           0 :                         parseYCSB(dataDir, inFile, outFile)
      51           0 :                         return nil
      52             :                 },
      53             :         }
      54             : 
      55           1 :         c.Flags().String("dir", defaultDir, "path to data directory")
      56           1 :         c.Flags().String("in", defaultCookedFile, "path to (possibly non-empty) input cooked data file")
      57           1 :         c.Flags().String("out", defaultCookedFile, "path to output data file")
      58           1 :         c.SilenceUsage = true
      59           1 : 
      60           1 :         return c
      61             : }
      62             : 
      63             : type ycsbRun struct {
      64             :         opsSec     float64
      65             :         readBytes  int64
      66             :         writeBytes int64
      67             :         readAmp    float64
      68             :         writeAmp   float64
      69             : }
      70             : 
      71           1 : func (r ycsbRun) formatCSV() string {
      72           1 :         return fmt.Sprintf("%.1f,%d,%d,%.1f,%.1f",
      73           1 :                 r.opsSec, r.readBytes, r.writeBytes, r.readAmp, r.writeAmp)
      74           1 : }
      75             : 
      76             : type ycsbWorkload struct {
      77             :         days map[string][]ycsbRun // data -> runs
      78             : }
      79             : 
      80             : type ycsbLoader struct {
      81             :         cookedDays map[string]bool          // set of already cooked days
      82             :         data       map[string]*ycsbWorkload // workload name -> workload data
      83             : }
      84             : 
      85           1 : func newYCSBLoader() *ycsbLoader {
      86           1 :         return &ycsbLoader{
      87           1 :                 cookedDays: make(map[string]bool),
      88           1 :                 data:       make(map[string]*ycsbWorkload),
      89           1 :         }
      90           1 : }
      91             : 
      92           1 : func (l *ycsbLoader) addRun(name, day string, r ycsbRun) {
      93           1 :         w := l.data[name]
      94           1 :         if w == nil {
      95           1 :                 w = &ycsbWorkload{days: make(map[string][]ycsbRun)}
      96           1 :                 l.data[name] = w
      97           1 :         }
      98           1 :         w.days[day] = append(w.days[day], r)
      99             : }
     100             : 
     101           1 : func (l *ycsbLoader) loadCooked(path string) {
     102           1 :         data, err := os.ReadFile(path)
     103           1 :         if oserror.IsNotExist(err) {
     104           1 :                 return
     105           1 :         }
     106           1 :         if err != nil {
     107           0 :                 log.Fatal(err)
     108           0 :         }
     109             : 
     110           1 :         data = bytes.TrimSpace(data)
     111           1 : 
     112           1 :         prefix := []byte("data = ")
     113           1 :         if !bytes.HasPrefix(data, prefix) {
     114           0 :                 log.Fatalf("missing '%s' prefix", prefix)
     115           0 :         }
     116           1 :         data = bytes.TrimPrefix(data, prefix)
     117           1 : 
     118           1 :         suffix := []byte(";")
     119           1 :         if !bytes.HasSuffix(data, suffix) {
     120           0 :                 log.Fatalf("missing '%s' suffix", suffix)
     121           0 :         }
     122           1 :         data = bytes.TrimSuffix(data, suffix)
     123           1 : 
     124           1 :         m := make(map[string]string)
     125           1 :         if err := json.Unmarshal(data, &m); err != nil {
     126           0 :                 log.Fatal(err)
     127           0 :         }
     128             : 
     129           1 :         for name, data := range m {
     130           1 :                 s := bufio.NewScanner(strings.NewReader(data))
     131           1 :                 for s.Scan() {
     132           1 :                         line := s.Text()
     133           1 :                         line = strings.Replace(line, ",", " ", -1)
     134           1 : 
     135           1 :                         var r ycsbRun
     136           1 :                         var day string
     137           1 :                         n, err := fmt.Sscanf(line, "%s %f %d %d %f %f",
     138           1 :                                 &day, &r.opsSec, &r.readBytes, &r.writeBytes, &r.readAmp, &r.writeAmp)
     139           1 :                         if err != nil || n != 6 {
     140           0 :                                 log.Fatalf("%s: %+v", line, err)
     141           0 :                         }
     142           1 :                         l.cookedDays[day] = true
     143           1 :                         l.addRun(name, day, r)
     144             :                 }
     145             :         }
     146             : }
     147             : 
     148           1 : func (l *ycsbLoader) loadRaw(dir string) {
     149           1 :         walkFn := func(path, pathRel string, info os.FileInfo) error {
     150           1 :                 // The directory structure is of the form:
     151           1 :                 //   $date/pebble/ycsb/$name/$run/$file
     152           1 :                 parts := strings.Split(pathRel, string(os.PathSeparator))
     153           1 :                 if len(parts) < 6 {
     154           1 :                         return nil // stumble forward on invalid paths
     155           1 :                 }
     156             : 
     157             :                 // We're only interested in YCSB benchmark data.
     158           1 :                 if parts[2] != "ycsb" {
     159           1 :                         return nil
     160           1 :                 }
     161             : 
     162           1 :                 day := parts[0]
     163           1 :                 if l.cookedDays[day] {
     164           1 :                         return nil
     165           1 :                 }
     166             : 
     167           1 :                 f, err := os.Open(path)
     168           1 :                 if err != nil {
     169           0 :                         fmt.Fprintf(os.Stderr, "%+v\n", err)
     170           0 :                         return nil // stumble forward on error
     171           0 :                 }
     172           1 :                 defer f.Close()
     173           1 : 
     174           1 :                 r := io.Reader(f)
     175           1 :                 if strings.HasSuffix(path, ".bz2") {
     176           1 :                         r = bzip2.NewReader(f)
     177           1 :                 } else if strings.HasSuffix(path, ".gz") {
     178           1 :                         var err error
     179           1 :                         r, err = gzip.NewReader(f)
     180           1 :                         if err != nil {
     181           0 :                                 fmt.Fprintf(os.Stderr, "%+v\n", err)
     182           0 :                                 return nil // stumble forward on error
     183           0 :                         }
     184             :                 }
     185             : 
     186           1 :                 s := bufio.NewScanner(r)
     187           1 :                 for s.Scan() {
     188           1 :                         line := s.Text()
     189           1 :                         if !strings.HasPrefix(line, "Benchmark") {
     190           1 :                                 continue
     191             :                         }
     192             : 
     193           1 :                         var r ycsbRun
     194           1 :                         var name string
     195           1 :                         var ops int64
     196           1 :                         n, err := fmt.Sscanf(line,
     197           1 :                                 "Benchmark%s %d %f ops/sec %d read %d write %f r-amp %f w-amp",
     198           1 :                                 &name, &ops, &r.opsSec, &r.readBytes, &r.writeBytes, &r.readAmp, &r.writeAmp)
     199           1 :                         if err != nil || n != 7 {
     200           0 :                                 fmt.Fprintf(os.Stderr, "%s: %v\n", s.Text(), err)
     201           0 :                                 // Stumble forward on error.
     202           0 :                                 continue
     203             :                         }
     204             : 
     205           1 :                         fmt.Fprintf(os.Stderr, "%s: adding %s\n", day, name)
     206           1 :                         l.addRun(name, day, r)
     207             :                 }
     208           1 :                 return nil
     209             :         }
     210             : 
     211           1 :         _ = walkDir(dir, walkFn)
     212             : }
     213             : 
     214           1 : func (l *ycsbLoader) cook(path string) {
     215           1 :         m := make(map[string]string)
     216           1 :         for name, workload := range l.data {
     217           1 :                 m[name] = l.cookWorkload(workload)
     218           1 :         }
     219             : 
     220           1 :         out := []byte("data = ")
     221           1 :         out = append(out, prettyJSON(m)...)
     222           1 :         out = append(out, []byte(";\n")...)
     223           1 :         if err := os.WriteFile(path, out, 0644); err != nil {
     224           0 :                 log.Fatal(err)
     225           0 :         }
     226             : }
     227             : 
     228           1 : func (l *ycsbLoader) cookWorkload(w *ycsbWorkload) string {
     229           1 :         days := make([]string, 0, len(w.days))
     230           1 :         for day := range w.days {
     231           1 :                 days = append(days, day)
     232           1 :         }
     233           1 :         sort.Strings(days)
     234           1 : 
     235           1 :         var buf bytes.Buffer
     236           1 :         for _, day := range days {
     237           1 :                 fmt.Fprintf(&buf, "%s,%s\n", day, l.cookDay(w.days[day]))
     238           1 :         }
     239           1 :         return buf.String()
     240             : }
     241             : 
     242           1 : func (l *ycsbLoader) cookDay(runs []ycsbRun) string {
     243           1 :         if len(runs) == 1 {
     244           1 :                 return runs[0].formatCSV()
     245           1 :         }
     246             : 
     247             :         // The benchmarks show significant run-to-run variance due to
     248             :         // instance-to-instance performance variability on AWS. We attempt to smooth
     249             :         // out this variance by excluding outliers: any run that is more than one
     250             :         // stddev from the average, and then taking the average of the remaining
     251             :         // runs. Note that the runs on a given day are all from the same SHA, so this
     252             :         // smoothing will not affect exceptional day-to-day performance changes.
     253             : 
     254           1 :         var sum float64
     255           1 :         for i := range runs {
     256           1 :                 sum += runs[i].opsSec
     257           1 :         }
     258           1 :         mean := sum / float64(len(runs))
     259           1 : 
     260           1 :         var sum2 float64
     261           1 :         for i := range runs {
     262           1 :                 v := runs[i].opsSec - mean
     263           1 :                 sum2 += v * v
     264           1 :         }
     265             : 
     266           1 :         stddev := math.Sqrt(sum2 / float64(len(runs)))
     267           1 :         lo := mean - stddev
     268           1 :         hi := mean + stddev
     269           1 : 
     270           1 :         var avg ycsbRun
     271           1 :         var count int
     272           1 :         for i := range runs {
     273           1 :                 r := &runs[i]
     274           1 :                 if r.opsSec < lo || r.opsSec > hi {
     275           1 :                         continue
     276             :                 }
     277           1 :                 count++
     278           1 :                 avg.opsSec += r.opsSec
     279           1 :                 avg.readBytes += r.readBytes
     280           1 :                 avg.writeBytes += r.writeBytes
     281           1 :                 avg.readAmp += r.readAmp
     282           1 :                 avg.writeAmp += r.writeAmp
     283             :         }
     284             : 
     285           1 :         avg.opsSec /= float64(count)
     286           1 :         avg.readBytes /= int64(count)
     287           1 :         avg.writeBytes /= int64(count)
     288           1 :         avg.readAmp /= float64(count)
     289           1 :         avg.writeAmp /= float64(count)
     290           1 :         return avg.formatCSV()
     291             : }
     292             : 
     293             : // parseYCSB coalesces YCSB benchmark data.
     294           1 : func parseYCSB(dataDir, inFile, outFile string) {
     295           1 :         log.SetFlags(log.Lshortfile)
     296           1 : 
     297           1 :         l := newYCSBLoader()
     298           1 :         l.loadCooked(inFile)
     299           1 :         l.loadRaw(dataDir)
     300           1 :         l.cook(outFile)
     301           1 : }

Generated by: LCOV version 1.14