Skip to content

Commit efee395

Browse files
authored
improve histogram plot and plot error propagation (#308)
1 parent 5b84f50 commit efee395

File tree

5 files changed

+207
-54
lines changed

5 files changed

+207
-54
lines changed

pkg/reporter/plot.go

+31-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package reporter
33
import (
44
"fmt"
55
"image/color"
6+
"math"
67
"os"
78
"sort"
89
"time"
@@ -11,6 +12,7 @@ import (
1112
"github.com/montanaflynn/stats"
1213
"github.com/tantalor93/dnspyre/v3/pkg/dnsbench"
1314
"go-hep.org/x/hep/hplot"
15+
"gonum.org/v1/gonum/stat"
1416
"gonum.org/v1/plot"
1517
"gonum.org/v1/plot/plotter"
1618
"gonum.org/v1/plot/plotutil"
@@ -30,14 +32,14 @@ func plotHistogramLatency(file string, times []dnsbench.Datapoint) {
3032
p := plot.New()
3133
p.Title.Text = "Latencies distribution"
3234

33-
hist, err := plotter.NewHist(values, 16)
35+
hist, err := plotter.NewHist(values, numBins(values))
3436
if err != nil {
3537
panic(err)
3638
}
3739
p.X.Label.Text = "Latencies (ms)"
38-
p.X.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"}
40+
p.X.Tick.Marker = hplot.Ticks{N: 5, Format: "%.0f"}
3941
p.Y.Label.Text = "Number of requests"
40-
p.Y.Tick.Marker = hplot.Ticks{N: 3, Format: "%.0f"}
42+
p.Y.Tick.Marker = hplot.Ticks{N: 5, Format: "%.0f"}
4143
hist.FillColor = color.RGBA{R: 175, G: 238, B: 238, A: 255}
4244
p.Add(hist)
4345

@@ -46,6 +48,32 @@ func plotHistogramLatency(file string, times []dnsbench.Datapoint) {
4648
}
4749
}
4850

51+
// numBins calculates number of bins for histogram.
52+
func numBins(values plotter.Values) int {
53+
n := float64(len(values))
54+
55+
// small dataset
56+
if n < 100 {
57+
sqrt := math.Sqrt(n)
58+
return int(math.Min(15, sqrt))
59+
}
60+
61+
// medium dataset - use Rice's rule
62+
if n < 1000 {
63+
rice := 2 * math.Cbrt(n)
64+
return int(math.Min(30, rice))
65+
}
66+
67+
// large dataset - use Doane's rule
68+
// Calculate skewness
69+
skewness := stat.Skew(values, nil)
70+
71+
// Calculate standard error of skewness
72+
sigmaG := math.Sqrt(6 * (n - 2) / ((n + 1) * (n + 3)))
73+
doane := 1 + math.Log2(n) + math.Log2(1+math.Abs(skewness)/sigmaG)
74+
return int(math.Min(50, doane))
75+
}
76+
4977
func plotBoxPlotLatency(file, server string, times []dnsbench.Datapoint) {
5078
if len(times) == 0 {
5179
// nothing to plot

pkg/reporter/plot_test.go

+44
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"github.com/stretchr/testify/assert"
99
"github.com/stretchr/testify/require"
1010
"github.com/tantalor93/dnspyre/v3/pkg/dnsbench"
11+
"gonum.org/v1/plot/plotter"
1112
)
1213

1314
var testStart = time.Now()
@@ -130,3 +131,46 @@ func Test_plotErrorRate(t *testing.T) {
130131

131132
assert.Equal(t, expected, actual, "generated error rate plot does not equal to expected 'test-errorrate-lineplot.png")
132133
}
134+
135+
func Test_numBins(t *testing.T) {
136+
tests := []struct {
137+
name string
138+
values plotter.Values
139+
want int
140+
}{
141+
{
142+
name: "small dataset",
143+
values: dataset(25),
144+
want: 5,
145+
},
146+
{
147+
name: "medium dataset",
148+
values: dataset(500),
149+
want: 15,
150+
},
151+
{
152+
name: "large dataset",
153+
values: dataset(2000),
154+
want: 11,
155+
},
156+
{
157+
name: "single item dataset",
158+
values: dataset(1),
159+
want: 1,
160+
},
161+
}
162+
for _, tt := range tests {
163+
t.Run(tt.name, func(t *testing.T) {
164+
assert.Equal(t, tt.want, numBins(tt.values))
165+
})
166+
}
167+
}
168+
169+
// dataset generates uniformorly distributed dataset.
170+
func dataset(len int) plotter.Values {
171+
values := make(plotter.Values, len)
172+
for i := range values {
173+
values[i] = float64(i)
174+
}
175+
return values
176+
}

pkg/reporter/report.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,14 @@ func PrintReport(b *dnsbench.Benchmark, stats []*dnsbench.ResultStats, benchStar
5656
}
5757

5858
if len(b.PlotDir) != 0 {
59+
if err := directoryExists(b.PlotDir); err != nil {
60+
return fmt.Errorf("unable to plot results: %w", err)
61+
}
62+
5963
now := time.Now().Format(time.RFC3339)
6064
dir := fmt.Sprintf("%s/graphs-%s", b.PlotDir, now)
6165
if err := os.Mkdir(dir, os.ModePerm); err != nil {
62-
panic(err)
66+
return fmt.Errorf("unable to plot results: %w", err)
6367
}
6468
plotHistogramLatency(fileName(b, dir, "latency-histogram"), totals.Timings)
6569
plotBoxPlotLatency(fileName(b, dir, "latency-boxplot"), b.Server, totals.Timings)
@@ -108,6 +112,19 @@ func PrintReport(b *dnsbench.Benchmark, stats []*dnsbench.ResultStats, benchStar
108112
return printer(b).print(params)
109113
}
110114

115+
func directoryExists(plotDir string) error {
116+
stat, err := os.Stat(plotDir)
117+
if err != nil {
118+
if os.IsNotExist(err) {
119+
return fmt.Errorf("'%s' path does not point to an existing directory", plotDir)
120+
}
121+
return err
122+
} else if !stat.IsDir() {
123+
return fmt.Errorf("'%s' is not a path to a directory", plotDir)
124+
}
125+
return nil
126+
}
127+
111128
func printer(b *dnsbench.Benchmark) reportPrinter {
112129
switch {
113130
case b.JSON:

pkg/reporter/report_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"net"
88
"os"
9+
"path/filepath"
10+
"strings"
911
"testing"
1012
"time"
1113

@@ -101,6 +103,55 @@ func Test_PrintReport_errors(t *testing.T) {
101103
assert.Equal(t, readResource("errorReport"), buffer.String())
102104
}
103105

106+
func Test_PrintReport_plot(t *testing.T) {
107+
dir := t.TempDir()
108+
109+
buffer := bytes.Buffer{}
110+
b, rs := testReportData(&buffer)
111+
b.PlotDir = dir
112+
b.PlotFormat = dnsbench.DefaultPlotFormat
113+
114+
err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second)
115+
116+
require.NoError(t, err)
117+
118+
testDir, err := os.ReadDir(dir)
119+
120+
require.NoError(t, err)
121+
require.Len(t, testDir, 1)
122+
123+
graphsDir := testDir[0].Name()
124+
assert.True(t, strings.HasPrefix(graphsDir, "graphs-"))
125+
126+
graphsDirContent, err := os.ReadDir(filepath.Join(dir, graphsDir))
127+
require.NoError(t, err)
128+
129+
var graphFiles []string
130+
for _, v := range graphsDirContent {
131+
graphFiles = append(graphFiles, v.Name())
132+
}
133+
134+
assert.ElementsMatch(t, graphFiles,
135+
[]string{
136+
"errorrate-lineplot.svg", "latency-boxplot.svg", "latency-histogram.svg", "latency-lineplot.svg",
137+
"responses-barchart.svg", "throughput-lineplot.svg",
138+
},
139+
)
140+
}
141+
142+
func Test_PrintReport_plot_error(t *testing.T) {
143+
dir := t.TempDir()
144+
145+
buffer := bytes.Buffer{}
146+
b, rs := testReportData(&buffer)
147+
b.PlotDir = dir + "/non-existing-directory"
148+
b.PlotFormat = dnsbench.DefaultPlotFormat
149+
150+
err := reporter.PrintReport(&b, []*dnsbench.ResultStats{&rs}, time.Now(), time.Second)
151+
152+
require.Error(t, err)
153+
}
154+
104155
func testReportData(testOutputWriter io.Writer) (dnsbench.Benchmark, dnsbench.ResultStats) {
105156
b := dnsbench.Benchmark{
106157
HistPre: 1,

pkg/reporter/testdata/test-histogram-latency.svg

+63-50
Loading

0 commit comments

Comments
 (0)