Skip to content

Commit

Permalink
plot: Revert back to Dygraphs
Browse files Browse the repository at this point in the history
  • Loading branch information
tsenart committed Jul 21, 2018
1 parent d21bc64 commit f9995df
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 108 deletions.
242 changes: 164 additions & 78 deletions lib/plot.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,45 @@ import (
"encoding/json"
"html/template"
"io"

tsz "github.com/dgryski/go-tsz"
"math"
"strconv"
"time"
)

// An HTMLPlot represents an interactive HTML time series
// plot of Result latencies over time.
type HTMLPlot struct {
title string
threshold int
series map[string]map[string]*timeSeries
series map[string]*attackSeries
}

// attackSeries groups the two timeSeries an attack results in:
// OK and Error data points
type attackSeries struct{ ok, err *timeSeries }

// add adds the given result to the OK timeSeries if the Result
// has no error, or to the Error timeSeries otherwise.
func (as *attackSeries) add(r *Result) {
var (
s **timeSeries
label string
)

if r.Error == "" {
s, label = &as.ok, "OK"
} else {
s, label = &as.err, "Error"
}

if *s == nil {
*s = newTimeSeries(r.Attack, label, r.Timestamp)
}

(*s).add(
uint32(r.Timestamp.Sub((*s).began)/(100*time.Microsecond)),
r.Latency.Seconds()*1000,
)
}

// NewHTMLPlot returns an HTMLPlot with the given title,
Expand All @@ -22,117 +51,142 @@ func NewHTMLPlot(title string, threshold int) *HTMLPlot {
return &HTMLPlot{
title: title,
threshold: threshold,
series: map[string]map[string]*timeSeries{},
series: map[string]*attackSeries{},
}
}

// Add adds the given Result to the HTMLPlot time series.
func (p *HTMLPlot) Add(r *Result) {
attack, ok := p.series[r.Attack]
if !ok {
attack = make(map[string]*timeSeries, 2)
p.series[r.Attack] = attack
}

var label string
if r.Error == "" {
label = "OK"
} else {
label = "Error"
}

s, ok := attack[label]
s, ok := p.series[r.Attack]
if !ok {
s = &timeSeries{
attack: r.Attack,
label: label,
began: r.Timestamp,
data: tsz.New(0),
}
attack[label] = s
s = &attackSeries{}
p.series[r.Attack] = s
}

s.add(r)
}

func (p *HTMLPlot) Close() {
for _, labels := range p.series {
for _, s := range labels {
s.data.Finish()
for _, as := range p.series {
for _, ts := range []*timeSeries{as.ok, as.err} {
if ts != nil {
ts.data.Finish()
}
}
}
}

// WriteTo writes the HTML plot to the give io.Writer.
func (p HTMLPlot) WriteTo(w io.Writer) (n int64, err error) {
type chart struct {
Type string `json:"type"`
RenderTo string `json:"renderTo"`
}

type title struct {
Text string `json:"text"`
type dygraphsOpts struct {
Title string `json:"title"`
Labels []string `json:"labels,omitempty"`
YLabel string `json:"ylabel"`
XLabel string `json:"xlabel"`
Colors []string `json:"colors,omitempty"`
Legend string `json:"legend"`
ShowRoller bool `json:"showRoller"`
LogScale bool `json:"logScale"`
StrokeWidth float64 `json:"strokeWidth"`
}

type axis struct {
Type string `json:"type"`
Title title `json:"title"`
type plotData struct {
Title string
HTML2CanvasJS template.JS
DygraphsJS template.JS
Data template.JS
Opts template.JS
}

type data struct {
Name string `json:"name"`
Data []point `json:"data"`
dp, labels, err := p.data()
if err != nil {
return 0, err
}

type highChartOpts struct {
Chart chart `json:"chart"`
Title title `json:"title"`
XAxis axis `json:"xAxis"`
YAxis axis `json:"yAxis"`
Series []data `json:"series"`
var sz int
if len(dp) > 0 {
sz = len(dp) * len(dp[0]) * 12 // heuristic
}

type templateData struct {
Title string
HTML2CanvasJS string
HighChartOptsJSON string
}
data := dp.Append(make([]byte, 0, sz))

opts := highChartOpts{
Chart: chart{Type: "line", RenderTo: "latencies"},
Title: title{Text: p.title},
XAxis: axis{Title: title{Text: "Time elapsed (s)"}},
YAxis: axis{
Title: title{Text: "Latency (ms)"},
Type: "logarithmic",
},
}
// TODO: Improve colors to be more intutive
// Green pallette for OK series
// Red pallette for Error series

for attack, labels := range p.series {
for label, s := range labels {
d := data{Name: attack + ": " + label}
if d.Data, err = s.lttb(p.threshold); err != nil {
return 0, err
}
opts.Series = append(opts.Series, d)
}
opts := dygraphsOpts{
Title: p.title,
Labels: labels,
YLabel: "Latency (ms)",
XLabel: "Seconds elapsed",
Legend: "always",
ShowRoller: true,
LogScale: true,
StrokeWidth: 1.3,
}

bs, err := json.Marshal(&opts)
optsJSON, err := json.MarshalIndent(&opts, " ", " ")
if err != nil {
return 0, err
}

cw := countingWriter{w: w}
err = plotTemplate.Execute(&cw, &templateData{
Title: p.title,
HTML2CanvasJS: string(asset(html2canvas)),
HighChartOptsJSON: string(bs),
err = plotTemplate.Execute(&cw, &plotData{
Title: p.title,
HTML2CanvasJS: template.JS(asset(html2canvas)),
DygraphsJS: template.JS(asset(dygraphs)),
Data: template.JS(data),
Opts: template.JS(optsJSON),
})

return cw.n, err
}

// See http://dygraphs.com/data.html
func (p *HTMLPlot) data() (dataPoints, []string, error) {
var (
series []*timeSeries
count int
)

for _, as := range p.series {
for _, s := range [...]*timeSeries{as.ok, as.err} {
if s != nil {
series = append(series, s)
count += s.len
}
}
}

var (
size = 1 + len(series)
nan = math.NaN()
labels = make([]string, size)
data = make(dataPoints, 0, count)
)

labels[0] = "Seconds"

for i, s := range series {
points, err := s.lttb(p.threshold)
if err != nil {
return nil, nil, err
}

for _, p := range points {
point := make([]float64, size)
for j := range point {
point[j] = nan
}
point[0], point[i+1] = p[0], p[1]
data = append(data, point)
}

labels[i+1] = s.attack + ": " + s.label
}

return data, labels, nil
}

type countingWriter struct {
n int64
w io.Writer
Expand All @@ -144,6 +198,34 @@ func (cw *countingWriter) Write(p []byte) (int, error) {
return n, err
}

type dataPoints [][]float64

func (ps dataPoints) Append(buf []byte) []byte {
buf = append(buf, "[\n "...)

for i, p := range ps {
buf = append(buf, " ["...)

for j, f := range p {
if math.IsNaN(f) {
buf = append(buf, "NaN"...)
} else {
buf = strconv.AppendFloat(buf, f, 'f', -1, 64)
}

if j < len(p)-1 {
buf = append(buf, ',')
}
}

if buf = append(buf, "]"...); i < len(ps)-1 {
buf = append(buf, ",\n "...)
}
}

return append(buf, " ]"...)
}

var plotTemplate = template.Must(template.New("plot").Parse(`
<!doctype html>
<html>
Expand All @@ -154,10 +236,9 @@ var plotTemplate = template.Must(template.New("plot").Parse(`
<body>
<div id="latencies" style="font-family: Courier; width: 100%%; height: 600px"></div>
<button id="download">Download as PNG</button>
<script src="https://code.highcharts.com/highcharts.src.js"></script>
<script>{{.HTML2CanvasJS}}</script>
<script>{{.HTML2CanvasJS}}</script>
<script>{{.DygraphsJS}}</script>
<script>
Highcharts.chart(JSON.parse("{{.HighChartOptsJSON}}"));
document.getElementById("download").addEventListener("click", function(e) {
html2canvas(document.body, {background: "#fff"}).then(function(canvas) {
var url = canvas.toDataURL('image/png').replace(/^data:image\/[^;]/, 'data:application/octet-stream');
Expand All @@ -167,6 +248,11 @@ var plotTemplate = template.Must(template.New("plot").Parse(`
a.click();
});
});
var container = document.getElementById("latencies");
var opts = {{.Opts}};
var data = {{.Data}};
var plot = new Dygraph(container, data, opts);
</script>
</body>
</html>`))
56 changes: 35 additions & 21 deletions lib/plot_test.go
Original file line number Diff line number Diff line change
@@ -1,23 +1,37 @@
package vegeta

// func BenchmarkHTMLPlot(b *testing.B) {
// b.StopTimer()
// // Build result set
// rs := make(Results, 50000)
// for began, i := time.Now(), 0; i < 50000; i++ {
// rs[i] = Result{
// Code: uint16(i % 600),
// Latency: 50 * time.Millisecond,
// Timestamp: began.Add(time.Duration(i) * 50 * time.Millisecond),
// }
// if i%5 == 0 {
// rs[i].Error = "Error"
// }
// }
// // Start benchmark
// b.ReportAllocs()
// b.StartTimer()
// for i := 0; i < b.N; i++ {
// NewHTMLPlot("Vegeta Plot", 5000, rs).WriteTo(ioutil.Discard)
// }
// }
import (
"io/ioutil"
"testing"
"time"
)

func BenchmarkHTMLPlot(b *testing.B) {
b.StopTimer()
// Build result set
rs := make(Results, 50000000)
for began, i := time.Now(), 0; i < cap(rs); i++ {
rs[i] = Result{
Attack: "foo",
Code: uint16(i % 600),
Latency: 50 * time.Millisecond,
Timestamp: began.Add(time.Duration(i) * 50 * time.Millisecond),
}
if i%5 == 0 {
rs[i].Error = "Error"
}
}

plot := NewHTMLPlot("Vegeta Plot", 5000)
b.Run("Add", func(b *testing.B) {
for i := 0; i < b.N; i++ {
plot.Add(&rs[i%len(rs)])
}
})

b.Run("WriteTo", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = plot.WriteTo(ioutil.Discard)
}
})
}
Loading

0 comments on commit f9995df

Please sign in to comment.