func (rs *resultSet) SaveHTML() (string, error) {
	html, err := rs.RenderHTML()
	if err != nil {
		return "", err
	}
	return save(html, "resultSet_*.html")
}
SaveHTML saves an HTML document representing the result set, and returns the file name
package main
import (
	"bytes"
	"flag"
	"fmt"
	"github.com/soteria-dag/soterd/soterutil"
	"html/template"
	"io/ioutil"
	"log"
	"math/big"
	"path/filepath"
	"sort"
	"strings"
	"time"
	"github.com/soteria-dag/soterd/blockdag"
	"github.com/soteria-dag/soterd/chaincfg"
	"github.com/soteria-dag/soterd/integration/rpctest"
	"github.com/wcharczuk/go-chart"
	"github.com/wcharczuk/go-chart/drawing"
)
const (
	defaultMinerCount = 4
	defaultMineTime = time.Second * 30
	defaultWait = time.Minute
)
var (
	defaultSoterdExtraArgs = []string{
	}
)
type nodeMetrics struct {
	blkGenCount int64
	blkGenTimes []float64
	blkHashes []string
	multiParentBlocks int
	meanMultiParentBlockDistance float64
}
type runResult struct {
	profile *runProfile
	nodeStats map[int]*nodeMetrics
	dagSvg []byte
	dagSynced bool
}
type changeResult struct {
	fields  []string
	amounts []int64
	units   []string
	result  *runResult
}
type resultSet struct {
	changes []*changeResult
}
type runProfile struct {
	dagNetParams chaincfg.Params
	dagNetMods []func(*chaincfg.Params) error
	minerCount int
	mineTime time.Duration
	wait time.Duration
	soterdExtraArgs []string
	soterdKeepLogs bool
}
func absInt(x int) int {
	if x < 0 {
		return -x
	} else {
		return x
	}
}
func meanFloat64(x []float64) float64 {
	total := float64(0)
	if len(x) == 0 {
		return total
	}
	for _, v := range x {
		total += v
	}
	mean := total / float64(len(x))
	return mean
}
func pctBigInt(x *big.Int, pct int) (n *big.Int) {
	bigHundred := big.NewInt(100)
	bigPct := big.NewInt(int64(absInt(pct)))
	change := new(big.Int).Mul(
		new(big.Int).Div(x, bigHundred),
		bigPct)
	if pct < 0 {
		n = new(big.Int).Sub(x, change)
	} else {
		n = new(big.Int).Add(x, change)
	}
	return n
}
func save(bytes []byte, pattern string) (string, error) {
	t, err := ioutil.TempFile("", pattern)
	if err != nil {
		return "", err
	}
	_, err = t.Write(bytes)
	if err != nil {
		return "", err
	}
	err = t.Close()
	if err != nil {
		return "", err
	}
	return t.Name(), nil
}
func modPowLimit(params *chaincfg.Params, pct int) {
	params.PowLimit = pctBigInt(params.PowLimit, pct)
	params.PowLimitBits = blockdag.BigToCompact(params.PowLimit)
}
func waitForDAGTick(nodes []*rpctest.Harness, wait time.Duration) {
	threshold := time.Now().Add(wait)
	tick := time.Tick(time.Second * 5)
	stop := make(chan int)
	go func() {
		_ = rpctest.WaitForDAG(nodes, wait)
		close(stop)
	}()
	for {
		select {
			case <-tick:
				if time.Now().After(threshold) {
					fmt.Println("!")
					return
				} else {
					fmt.Print(".")
				}
			case <-stop:
				fmt.Println()
				return
		}
	}
}
func (c *changeResult) amountStrings() []string {
	s := make([]string, 0)
	for _, amount := range c.amounts {
		s = append(s, fmt.Sprintf("%d", amount))
	}
	return s
}
func (c *changeResult) changeStrings() []string {
	s := make([]string, 0)
	for i, field := range c.fields {
		s = append(s, fmt.Sprintf("%s %d%s", field, c.amounts[i], c.units[i]))
	}
	return s
}
func (c *changeResult) filePattern(prefix string) string {
	return fmt.Sprintf("%s_%s_%s_*.svg",
		prefix,
		strings.Join(c.fields, "-"),
		strings.Join(c.amountStrings(), "_"))
}
func (c *changeResult) GraphNodeBlkGen() ([]byte, error) {
	var svg bytes.Buffer
	title := fmt.Sprintf("Blocks Generated Per Node (%s)",
		strings.Join(c.changeStrings(), ", "))
	values := make([]chart.Value, 0)
	for _, m := range c.result.miners() {
		v := chart.Value{
			Label: fmt.Sprintf("node %d", m),
			Value: float64(c.result.minerBlockCount(m)),
		}
		values = append(values, v)
	}
	ch := chart.BarChart{
		Title: title,
		TitleStyle: chart.StyleShow(),
		XAxis: chart.StyleShow(),
		YAxis: chart.YAxis{
			Name: "block count",
			NameStyle: chart.StyleShow(),
			Style: chart.StyleShow(),
		},
		Bars: values,
	}
	err := ch.Render(chart.SVG, &svg)
	if err != nil {
		return svg.Bytes(), err
	}
	return svg.Bytes(), nil
}
func (c *changeResult) Print() {
	fmt.Printf("blocks (total %v):\n", c.result.totalBlockCount())
	for _, m := range c.result.miners() {
		fmt.Printf("\tminer %v\tblkGen count %v\tblkGen mean %v ms\n",
			m, c.result.minerBlockCount(m), c.result.minerMeanGenTime(m))
	}
}
func (c *changeResult) SaveNodeBlkGen() (string, error) {
	svg, err := c.GraphNodeBlkGen()
	if err != nil {
		return "", err
	}
	return save(svg, c.filePattern("nodeBlkGen"))
}
func (c *changeResult) SaveDAG() (string, error) {
	return save(c.result.dagSvg, c.filePattern("dag"))
}
func (c *changeResult) SaveGraphs() (string, string, error) {
	d, err := c.SaveDAG()
	if err != nil {
		return "", "", err
	}
	bg, err := c.SaveNodeBlkGen()
	if err != nil {
		return "", "", err
	}
	return d, bg, nil
}
func newRunResult() runResult {
	return runResult{
		nodeStats: make(map[int]*nodeMetrics),
	}
}
func (rr *runResult) minerBlockCount(m int) int64 {
	return rr.nodeStats[m].blkGenCount
}
func (rr *runResult) minerMeanGenTime(m int) float64 {
	return meanFloat64(rr.nodeStats[m].blkGenTimes)
}
func (rr *runResult) meanMpbDistance() float64 {
	d := make([]float64, 0)
	for _, ns := range rr.nodeStats {
		d = append(d, ns.meanMultiParentBlockDistance)
	}
	return meanFloat64(d)
}
func (rr *runResult) miners() []int {
	var miners []int
	for m := range rr.nodeStats {
		miners = append(miners, m)
	}
	sort.Ints(miners)
	return miners
}
func (rr *runResult) totalBlockCount() int64 {
	total := int64(0)
	for _, ns := range rr.nodeStats {
		total += ns.blkGenCount
	}
	return total
}
func (rr *runResult) totalMpbCount() int {
	total := 0
	for _, ns := range rr.nodeStats {
		total += ns.multiParentBlocks
	}
	return total
}
func newResultSet() resultSet {
	return resultSet{
		changes: make([]*changeResult, 0),
	}
}
func (rs *resultSet) GraphMeanBlkGenTime() ([][]byte, error) {
	if len(rs.changes) == 0 {
		err := fmt.Errorf("No changes in result-set to graph!")
		return [][]byte{}, err
	}
	titles := make([]string, 0)
	xvalues := make([][]float64, 0)
	yvalues := make([][]float64, 0)
	for _, chgString := range rs.changes[0].fields {
		t := fmt.Sprintf("Mean Block-Generation Time in ms (%s)", chgString)
		titles = append(titles, t)
		xvalues = append(xvalues, make([]float64, 0))
		yvalues = append(yvalues, make([]float64, 0))
	}
	for _, cr := range rs.changes {
		for i, v := range cr.amounts {
			for j := 0; j < len(cr.result.miners()); j++ {
				xvalues[i] = append(xvalues[i], float64(v))
			}
			for _, m := range cr.result.miners() {
				yvalues[i] = append(yvalues[i], float64(cr.result.minerMeanGenTime(m)))
			}
		}
	}
	viridisByY := func(xr, yr chart.Range, index int, x, y float64) drawing.Color {
		return chart.Viridis(y, yr.GetMin(), yr.GetMax())
	}
	svgs := make([][]byte, 0)
	for i, t := range titles {
		ch := chart.Chart{
			Title: t,
			TitleStyle: chart.StyleShow(),
			XAxis: chart.XAxis{
				Name: fmt.Sprintf("%s (in %s)", rs.changes[0].fields[i], rs.changes[0].units[i]),
				NameStyle: chart.StyleShow(),
				Style: chart.StyleShow(),
				GridMinorStyle: chart.Style{
					Show: false,
				},
			},
			YAxis: chart.YAxis{
				Name: "block-gen time (in ms)",
				NameStyle: chart.StyleShow(),
				Style: chart.StyleShow(),
			},
			Series: []chart.Series{
				chart.ContinuousSeries{
					Style: chart.Style{
						Show: true,
						StrokeWidth: chart.Disabled,
						DotWidth: 5,
						DotColorProvider: viridisByY,
					},
					XValues: xvalues[i],
					YValues: yvalues[i],
				},
			},
		}
		var svg bytes.Buffer
		err := ch.Render(chart.SVG, &svg)
		if err != nil {
			return [][]byte{}, err
		}
		svgs = append(svgs, svg.Bytes())
	}
	return svgs, nil
}
func saveDagHTML(svg []byte) (string, error) {
	html, err := soterutil.RenderSvgHTML(svg, "dag")
	if err != nil {
		return "", err
	}
	return save(html, "dag_*.html")
}
func (rs *resultSet) RenderHTML() ([]byte, error) {
	var render bytes.Buffer
	tmpl := `
<html> 
	<head>
		<title>{{ .title }}</title>
	</head>
	<body>
		<h2>Mean block-generation times (one graph per value-changed)</h3>
		{{- range .mbgtGraphs -}}
		<figure>
			{{ . }}
		</figure>
		{{- end}}
		<br>
		<br>
		<h2>Per-change graphs</h3>
		{{- range .crGraphs -}}
			<h3>{{ .Title }}</h4>
			<p>dag synced? {{ .DagSynced }}</p>
			<a href="{{ .DagFile }}">dag</a>
			<figure>
				{{ .BlkGenGraph }}
			</figure>
		{{- end}}
	</body>
</html>
`
	t := template.New("dagparam result set")
	t, err := t.Parse(tmpl)
	svgs, err := rs.GraphMeanBlkGenTime()
	if err != nil {
		return []byte{}, err
	}
	htmlSvgs := make([]template.HTML, 0)
	for _, svg := range svgs {
		htmlSvgs = append(htmlSvgs, template.HTML(svg))
	}
	crSvgs := make([]struct{
		Title string
		DagFile string
		BlkGenGraph template.HTML
		DagSynced bool
	}, 0)
	for i, cr := range rs.changes {
		title := fmt.Sprintf("Run %d changes (%s)",
			i, strings.Join(cr.changeStrings(), ", "))
		dagFile, err := saveDagHTML(cr.result.dagSvg)
		if err != nil {
			return []byte{}, err
		}
		fmt.Println("saved dag:", dagFile)
		relDagFile := filepath.Base(dagFile)
		svg, err := cr.GraphNodeBlkGen()
		if err != nil {
			return []byte{}, err
		}
		crSvgs = append(crSvgs, struct{
			Title string
			DagFile string
			BlkGenGraph template.HTML
			DagSynced bool
		}{
			Title: title,
			DagFile: relDagFile,
			BlkGenGraph: template.HTML(svg),
			DagSynced: cr.result.dagSynced,
		})
	}
	data := map[string]interface{}{
		"title": "dagparam result-set",
		"mbgtGraphs": htmlSvgs,
		"crGraphs": crSvgs,
	}
	err = t.Execute(&render, data)
	if err != nil {
		return []byte{}, err
	}
	return render.Bytes(), nil
}