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 : }
|