1package main
2
3import (
4	"flag"
5	"fmt"
6	"io"
7	"os"
8	"os/signal"
9
10	vegeta "github.com/tsenart/vegeta/v12/lib"
11	"github.com/tsenart/vegeta/v12/lib/plot"
12)
13
14const plotUsage = `Usage: vegeta plot [options] [<file>...]
15
16Outputs an HTML time series plot of request latencies over time.
17The X axis represents elapsed time in seconds from the beginning
18of the earliest attack in all input files. The Y axis represents
19request latency in milliseconds.
20
21Click and drag to select a region to zoom into. Double click to zoom out.
22Choose a different number on the bottom left corner input field
23to change the moving average window size (in data points).
24
25Arguments:
26  <file>  A file with vegeta attack results encoded with one of
27          the supported encodings (gob | json | csv) [default: stdin]
28
29Options:
30  --title      Title and header of the resulting HTML page.
31               [default: Vegeta Plot]
32  --threshold  Threshold of data points to downsample series to.
33               Series with less than --threshold number of data
34               points are not downsampled. [default: 4000]
35
36Examples:
37  echo "GET http://:80" | vegeta attack -name=50qps -rate=50 -duration=5s > results.50qps.bin
38  cat results.50qps.bin | vegeta plot > plot.50qps.html
39  echo "GET http://:80" | vegeta attack -name=100qps -rate=100 -duration=5s > results.100qps.bin
40  vegeta plot results.50qps.bin results.100qps.bin > plot.html
41`
42
43func plotCmd() command {
44	fs := flag.NewFlagSet("vegeta plot", flag.ExitOnError)
45	title := fs.String("title", "Vegeta Plot", "Title and header of the resulting HTML page")
46	threshold := fs.Int("threshold", 4000, "Threshold of data points above which series are downsampled.")
47	output := fs.String("output", "stdout", "Output file")
48
49	fs.Usage = func() {
50		fmt.Fprintln(os.Stderr, plotUsage)
51	}
52
53	return command{fs, func(args []string) error {
54		fs.Parse(args)
55		files := fs.Args()
56		if len(files) == 0 {
57			files = append(files, "stdin")
58		}
59		return plotRun(files, *threshold, *title, *output)
60	}}
61}
62
63func plotRun(files []string, threshold int, title, output string) error {
64	dec, mc, err := decoder(files)
65	defer mc.Close()
66	if err != nil {
67		return err
68	}
69
70	out, err := file(output, true)
71	if err != nil {
72		return err
73	}
74	defer out.Close()
75
76	sigch := make(chan os.Signal, 1)
77	signal.Notify(sigch, os.Interrupt)
78
79	p := plot.New(
80		plot.Title(title),
81		plot.Downsample(threshold),
82		plot.Label(plot.ErrorLabeler),
83	)
84
85decode:
86	for {
87		select {
88		case <-sigch:
89			break decode
90		default:
91			var r vegeta.Result
92			if err = dec.Decode(&r); err != nil {
93				if err == io.EOF {
94					break decode
95				}
96				return err
97			}
98
99			if err = p.Add(&r); err != nil {
100				return err
101			}
102		}
103	}
104
105	p.Close()
106
107	_, err = p.WriteTo(out)
108	return err
109}
110