diff --git a/README.md b/README.md index c153ae6..88471e5 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,10 @@ NOTE: Be careful not to re-use the ID's if you care about fetching results at a # get test results ./hperf stat --hosts 1.1.1.{1...100} --id [my_test_id] # save test results -./hperf stat --hosts 1.1.1.{1...100} --id [my_test_id] --output /tmp/file +./hperf stat --hosts 1.1.1.{1...100} --id [my_test_id] --output /tmp/test.out + +# analyze test results +./hperf analyze --file /tmp/test.out # listen in on a running test ./hperf listen --hosts 1.1.1.{1...100} --id [my_test_id] @@ -97,11 +100,18 @@ NOTE: Be careful not to re-use the ID's if you care about fetching results at a ./hperf stop --hosts 1.1.1.{1...100} --id [my_test_id] ``` +## Analysis +The analyze command will print statistics for the 10th and 90th percentiles and all datapoints in between. +The format used is: + - 10th percentile: total, low, avarage, high + - in between: total, low, avarage, high + - 90th percentile: total, low, avarage, high + ## Available Statistics - - Payload Roundtrip (PMS high/low): - - Payload transfer time (Milliseconds) + - Payload Roundtrip (RMS high/low): + - Payload transfer time (Microseconds) - Time to first byte (TTFB high/low): - - This is the amount of time (Milliseconds) it takes between a request being made and the first byte being requested by the receiver + - This is the amount of time (Microseconds) it takes between a request being made and the first byte being requested by the receiver - Transferred bytes (TX): - Bandwidth throughput in KB/s, MB/s, GB/s, etc.. - Request count (#TX): diff --git a/client/client.go b/client/client.go index 2cf24f7..6c675bd 100644 --- a/client/client.go +++ b/client/client.go @@ -18,6 +18,7 @@ package client import ( + "bytes" "context" "encoding/json" "errors" @@ -165,9 +166,14 @@ func handleWSConnection(ctx context.Context, c *shared.Config, host string, id i shared.DEBUG(WarningStyle.Render("Connecting to ", host, ":", c.Port)) + connectString := "wss://" + host + ":" + c.Port + "/ws/" + host + if c.Insecure { + connectString = "ws://" + host + ":" + c.Port + "/ws/" + host + } + con, _, dialErr := dialer.DialContext( ctx, - "ws://"+host+":"+c.Port+"/ws/"+host, + connectString, nil) if dialErr != nil { PrintError(dialErr) @@ -232,18 +238,27 @@ func PrintError(err error) { fmt.Println(ErrorStyle.Render("ERROR: ", err.Error())) } -func receiveJSONDataPoint(data []byte, c *shared.Config) { +func receiveJSONDataPoint(data []byte, _ *shared.Config) { responseLock.Lock() defer responseLock.Unlock() - dp := new(shared.DP) - err := json.Unmarshal(data, &dp) - if err != nil { - PrintError(err) - return + if bytes.Contains(data, []byte("Error")) { + dp := new(shared.TError) + err := json.Unmarshal(data, &dp) + if err != nil { + PrintError(err) + return + } + responseERR = append(responseERR, *dp) + } else { + dp := new(shared.DP) + err := json.Unmarshal(data, &dp) + if err != nil { + PrintError(err) + return + } + responseDPS = append(responseDPS, *dp) } - - responseDPS = append(responseDPS, *dp) } func keepAliveLoop(ctx context.Context, tickerfunc func() (shouldExit bool)) error { @@ -418,10 +433,13 @@ func GetTest(ctx context.Context, c shared.Config) (err error) { _ = keepAliveLoop(ctx, nil) - if len(responseDPS) < 1 { - PrintErrorString("No datapoints found") - return - } + slices.SortFunc(responseERR, func(a shared.TError, b shared.TError) int { + if a.Created.Before(b.Created) { + return -1 + } else { + return 1 + } + }) slices.SortFunc(responseDPS, func(a shared.DP, b shared.DP) int { if a.Created.Before(b.Created) { @@ -437,12 +455,13 @@ func GetTest(ctx context.Context, c shared.Config) (err error) { return err } for i := range responseDPS { - outb, err := json.Marshal(responseDPS[i]) + _, err := shared.WriteStructAndNewLineToFile(f, responseDPS[i]) if err != nil { - PrintError(err) - continue + return err } - _, err = f.Write(append(outb, []byte{10}...)) + } + for i := range responseERR { + _, err := shared.WriteStructAndNewLineToFile(f, responseERR[i]) if err != nil { return err } @@ -460,5 +479,9 @@ func GetTest(ctx context.Context, c shared.Config) (err error) { printTableRow(s1, &dp, dp.Type) } + for i := range responseERR { + PrintTError(responseERR[i]) + } + return nil } diff --git a/client/table.go b/client/table.go index 2177055..aa34ff0 100644 --- a/client/table.go +++ b/client/table.go @@ -65,12 +65,12 @@ func initHeaders() { headerSlice[Created] = header{"Created", 8} headerSlice[Local] = header{"Local", 15} headerSlice[Remote] = header{"Remote", 15} - headerSlice[PMSH] = header{"PMSH", 4} - headerSlice[PMSL] = header{"PMSL", 4} - headerSlice[TTFBH] = header{"TTFBH", 5} - headerSlice[TTFBL] = header{"TTFBL", 5} - headerSlice[TX] = header{"TX", 9} - headerSlice[TXCount] = header{"#TX", 6} + headerSlice[PMSH] = header{"RMSH", 8} + headerSlice[PMSL] = header{"RMSL", 8} + headerSlice[TTFBH] = header{"TTFBH", 8} + headerSlice[TTFBL] = header{"TTFBL", 8} + headerSlice[TX] = header{"TX", 10} + headerSlice[TXCount] = header{"#TX", 10} headerSlice[ErrCount] = header{"#ERR", 6} headerSlice[DroppedPackets] = header{"#Dropped", 9} headerSlice[MemoryUsage] = header{"MemUsed", 7} @@ -148,8 +148,8 @@ func printTableRow(style lipgloss.Style, entry *shared.DP, t shared.TestType) { column{entry.Created.Format("15:04:05"), headerSlice[Created].width}, column{strings.Split(entry.Local, ":")[0], headerSlice[Local].width}, column{strings.Split(entry.Remote, ":")[0], headerSlice[Remote].width}, - column{formatInt(entry.PMSH), headerSlice[PMSH].width}, - column{formatInt(entry.PMSL), headerSlice[PMSL].width}, + column{formatInt(entry.RMSH), headerSlice[PMSH].width}, + column{formatInt(entry.RMSL), headerSlice[PMSL].width}, column{formatUint(entry.TXCount), headerSlice[TXCount].width}, column{formatInt(int64(entry.ErrCount)), headerSlice[ErrCount].width}, column{formatInt(int64(entry.DroppedPackets)), headerSlice[DroppedPackets].width}, @@ -176,8 +176,8 @@ func printTableRow(style lipgloss.Style, entry *shared.DP, t shared.TestType) { column{entry.Created.Format("15:04:05"), headerSlice[Created].width}, column{strings.Split(entry.Local, ":")[0], headerSlice[Local].width}, column{strings.Split(entry.Remote, ":")[0], headerSlice[Remote].width}, - column{formatInt(entry.PMSH), headerSlice[PMSH].width}, - column{formatInt(entry.PMSL), headerSlice[PMSL].width}, + column{formatInt(entry.RMSH), headerSlice[PMSH].width}, + column{formatInt(entry.RMSL), headerSlice[PMSL].width}, column{formatInt(entry.TTFBH), headerSlice[TTFBH].width}, column{formatInt(entry.TTFBL), headerSlice[TTFBH].width}, column{shared.BandwidthBytesToString(entry.TX), headerSlice[TX].width}, diff --git a/cmd/hperf/analyze.go b/cmd/hperf/analyze.go new file mode 100644 index 0000000..25d935f --- /dev/null +++ b/cmd/hperf/analyze.go @@ -0,0 +1,176 @@ +// Copyright (c) 2015-2024 MinIO, Inc. +// +// This file is part of MinIO Object Storage stack +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package main + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "math" + "os" + "slices" + + "github.com/charmbracelet/lipgloss" + "github.com/minio/cli" + "github.com/minio/hperf/client" + "github.com/minio/hperf/shared" +) + +var analyzeCMD = cli.Command{ + Name: "analyze", + Usage: "Analyze the give test", + Action: runAnalyze, + Flags: []cli.Flag{ + dnsServerFlag, + hostsFlag, + portFlag, + fileFlag, + }, + CustomHelpTemplate: `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{.HelpName}} [FLAGS] + +FLAGS: + {{range .VisibleFlags}}{{.}} + {{end}} +EXAMPLES: + 1. Analyze test results in file '/tmp/latency-test-1': + {{.Prompt}} {{.HelpName}} --hosts 10.10.10.1 --file latency-test-1 +`, +} + +func runAnalyze(ctx *cli.Context) error { + config, err := parseConfig(ctx) + if err != nil { + return err + } + return AnalyzeTest(GlobalContext, *config) +} + +func AnalyzeTest(ctx context.Context, c shared.Config) (err error) { + _, cancel := context.WithCancel(ctx) + defer cancel() + + f, err := os.Open(c.File) + if err != nil { + return err + } + + dps := make([]shared.DP, 0) + errors := make([]shared.TError, 0) + + s := bufio.NewScanner(f) + for s.Scan() { + b := s.Bytes() + if !bytes.Contains(b, []byte("Error")) { + dp := new(shared.DP) + err := json.Unmarshal(b, dp) + if err != nil { + return err + } + dps = append(dps, *dp) + } else { + dperr := new(shared.TError) + err := json.Unmarshal(b, dperr) + if err != nil { + return err + } + errors = append(errors, *dperr) + } + } + + // adjust stats + for i := range dps { + // Highest RMSH can never be 0, but it's the default value of golang int64. + // if we find a 0 we just set it to an impossibly high value. + if dps[i].RMSH == 0 { + dps[i].RMSH = 999999999 + } + } + + dps10 := math.Ceil((float64(len(dps)) / 100) * 10) + dps90 := math.Floor((float64(len(dps)) / 100) * 90) + + slices.SortFunc(dps, func(a shared.DP, b shared.DP) int { + if a.RMSH < b.RMSH { + return -1 + } else { + return 1 + } + }) + + dps10s := make([]shared.DP, 0) + dps50s := make([]shared.DP, 0) + dps90s := make([]shared.DP, 0) + + // total, sum, low, mean, high + dps10stats := []int64{0, 0, 999999999, 0, 0} + dps50stats := []int64{0, 0, 999999999, 0, 0} + dps90stats := []int64{0, 0, 999999999, 0, 0} + + for i := range dps { + if i <= int(dps10) { + dps10s = append(dps10s, dps[i]) + updateBracketStats(dps10stats, dps[i]) + } else if i >= int(dps90) { + dps90s = append(dps90s, dps[i]) + updateBracketStats(dps90stats, dps[i]) + } else { + dps50s = append(dps50s, dps[i]) + updateBracketStats(dps50stats, dps[i]) + } + } + + for i := range errors { + client.PrintTError(errors[i]) + } + + printBracker(dps10stats, "? < 10%", client.SuccessStyle) + printBracker(dps50stats, "10% < ? < 90%", client.WarningStyle) + printBracker(dps90stats, "? > 90%", client.ErrorStyle) + + return nil +} + +func printBracker(b []int64, tag string, style lipgloss.Style) { + fmt.Println(style.Render( + fmt.Sprintf(" %s | Total %d | Low %d | Avg %d | High %d | Microseconds ", + tag, + b[0], + b[2], + b[3], + b[4], + ), + )) +} + +func updateBracketStats(b []int64, dp shared.DP) { + b[0]++ + b[1] += dp.RMSH + if dp.RMSH < b[2] { + b[2] = dp.RMSH + } + b[3] = b[1] / b[0] + if dp.RMSH > b[4] { + b[4] = dp.RMSH + } +} diff --git a/cmd/hperf/bandwidth.go b/cmd/hperf/bandwidth.go index ab79260..7339128 100644 --- a/cmd/hperf/bandwidth.go +++ b/cmd/hperf/bandwidth.go @@ -35,7 +35,6 @@ var bandwidthCMD = cli.Command{ testIDFlag, bufferSizeFlag, payloadSizeFlag, - insecureFlag, restartOnErrorFlag, dnsServerFlag, saveTestFlag, diff --git a/cmd/hperf/latency.go b/cmd/hperf/latency.go index a503e60..df8e4f0 100644 --- a/cmd/hperf/latency.go +++ b/cmd/hperf/latency.go @@ -30,7 +30,6 @@ var latencyCMD = cli.Command{ Flags: []cli.Flag{ hostsFlag, portFlag, - insecureFlag, concurrencyFlag, delayFlag, durationFlag, @@ -55,7 +54,7 @@ EXAMPLES: {{.Prompt}} {{.HelpName}} --hosts 10.10.10.1,10.10.10.2 2. Run a slow moving test to probe latency: - {{.Prompt}} {{.HelpName}} --hosts 10.10.10.1,10.10.10.2 --delay 100 + {{.Prompt}} {{.HelpName}} --hosts 10.10.10.1,10.10.10.2 --request-delay 100 --concurrency 1 `, } diff --git a/cmd/hperf/list.go b/cmd/hperf/list.go index 6d497ae..9bb12f9 100644 --- a/cmd/hperf/list.go +++ b/cmd/hperf/list.go @@ -42,7 +42,7 @@ FLAGS: {{range .VisibleFlags}}{{.}} {{end}} EXAMPLES: - 1. List all test on the '10.10.10.1': + 1. List all test on the '10.10.10.1' host: {{.Prompt}} {{.HelpName}} --hosts 10.10.10.1 `, } diff --git a/cmd/hperf/main.go b/cmd/hperf/main.go index 864e0d4..8212708 100644 --- a/cmd/hperf/main.go +++ b/cmd/hperf/main.go @@ -27,7 +27,9 @@ import ( "os/signal" "runtime" "syscall" + "time" + "github.com/google/uuid" "github.com/minio/cli" "github.com/minio/hperf/client" "github.com/minio/hperf/shared" @@ -60,6 +62,7 @@ func InvalidFlagValueError(value interface{}, name string) error { var ( debug = false + insecure = false globalFlags = []cli.Flag{ hostsFlag, portFlag, @@ -138,6 +141,10 @@ var ( Name: "output", Usage: "set output file path/name", } + fileFlag = cli.StringFlag{ + Name: "file", + Usage: "input file path", + } saveTestFlag = cli.BoolTFlag{ Name: "save", EnvVar: "HPERF_SAVE", @@ -153,17 +160,19 @@ var ( var ( baseFlags = []cli.Flag{ debugFlag, + insecureFlag, } Commands = []cli.Command{ - serverCMD, + analyzeCMD, bandwidthCMD, - requestsCMD, + deleteCMD, latencyCMD, listenCMD, listTestsCMD, + requestsCMD, + serverCMD, statTestsCMD, stopCMD, - deleteCMD, } ) @@ -205,6 +214,7 @@ var ( func before(ctx *cli.Context) error { debug = ctx.Bool("debug") + insecure = ctx.Bool("insecure") GlobalContext, GlobalCancelFunc = context.WithCancelCause(context.Background()) go handleOSSignal(GlobalCancelFunc) return nil @@ -225,11 +235,12 @@ func parseConfig(ctx *cli.Context) (*shared.Config, error) { config = &shared.Config{ DialTimeout: 0, Debug: debug, + Hosts: hosts, + Insecure: insecure, TestType: shared.LatencyTest, Duration: ctx.Int(durationFlag.Name), RequestDelay: ctx.Int(delayFlag.Name), Concurrency: ctx.Int(concurrencyFlag.Name), - Insecure: ctx.Bool(insecureFlag.Name), Proc: ctx.Int(concurrencyFlag.Name), PayloadSize: ctx.Int(payloadSizeFlag.Name), BufferKB: ctx.Int(bufferSizeFlag.Name), @@ -237,18 +248,21 @@ func parseConfig(ctx *cli.Context) (*shared.Config, error) { Save: ctx.BoolT(saveTestFlag.Name), TestID: ctx.String(testIDFlag.Name), RestartOnError: ctx.BoolT(restartOnErrorFlag.Name), - Hosts: hosts, Output: ctx.String(outputFlag.Name), + File: ctx.String(fileFlag.Name), } - if ctx.String("id") == "" { - switch ctx.Command.Name { - case "latency", "bandwidth", "http": - err = errors.New("--id is required") - case "get": - err = errors.New("--id is required") - default: + switch ctx.Command.Name { + case "latency", "bandwidth", "http", "get": + if ctx.String("id") == "" { + uid := uuid.NewString() + config.TestID = uid + "-" + time.Now().Format("2006-01-02-15-04-05") + } + case "analyze": + if ctx.String("file") == "" { + err = errors.New("--file is required") } + default: } Error: diff --git a/cmd/hperf/requests.go b/cmd/hperf/requests.go index 5b12048..8cb2725 100644 --- a/cmd/hperf/requests.go +++ b/cmd/hperf/requests.go @@ -30,7 +30,6 @@ var requestsCMD = cli.Command{ Flags: []cli.Flag{ hostsFlag, portFlag, - insecureFlag, concurrencyFlag, delayFlag, durationFlag, diff --git a/go.mod b/go.mod index b9ede86..7eaeda2 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,15 @@ module github.com/minio/hperf go 1.22 require ( - github.com/charmbracelet/lipgloss v0.12.1 + github.com/charmbracelet/lipgloss v0.13.0 github.com/fasthttp/websocket v1.5.10 github.com/gofiber/contrib/websocket v1.3.2 github.com/gofiber/fiber/v2 v2.52.5 + github.com/google/uuid v1.6.0 github.com/minio/cli v1.24.2 github.com/minio/pkg/v3 v3.0.20 github.com/shirou/gopsutil v3.21.11+incompatible - golang.org/x/sys v0.25.0 + golang.org/x/sys v0.26.0 ) require ( @@ -18,7 +19,6 @@ require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/charmbracelet/x/ansi v0.1.4 // indirect github.com/go-ole/go-ole v1.3.0 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.17.9 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 2237691..e433394 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1 github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/charmbracelet/lipgloss v0.12.1 h1:/gmzszl+pedQpjCOH+wFkZr/N90Snz40J/NR7A0zQcs= -github.com/charmbracelet/lipgloss v0.12.1/go.mod h1:V2CiwIuhx9S1S1ZlADfOj9HmxeMAORuz5izHb0zGbB8= +github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= +github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -66,8 +66,8 @@ golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/server/server.go b/server/server.go index 7c1420c..ad25caf 100644 --- a/server/server.go +++ b/server/server.go @@ -30,7 +30,6 @@ import ( "os" "runtime" "runtime/debug" - "slices" "strconv" "strings" "sync" @@ -410,6 +409,7 @@ type netPerfReader struct { buf []byte addr string + ip string client *http.Client TXCount atomic.Uint64 @@ -419,8 +419,8 @@ type netPerfReader struct { TTFBH int64 TTFBL int64 - PMSH int64 - PMSL int64 + RMSH int64 + RMSL int64 lastDataPointTime time.Time } @@ -438,7 +438,7 @@ type asyncReader struct { func (a *asyncReader) Read(b []byte) (n int, err error) { if !a.ttfbRegistered { a.ttfbRegistered = true - since := time.Since(a.start).Milliseconds() + since := time.Since(a.start).Microseconds() a.pr.m.Lock() if since > a.pr.TTFBH { a.pr.TTFBH = since @@ -538,14 +538,14 @@ func sendAndSaveData(t *test) (err error) { wss := new(shared.WebsocketSignal) wss.SType = shared.Stats - dataResponse := new(shared.DataReponseToClient) + wss.DataPoint = new(shared.DataReponseToClient) if t.DataFile == nil && t.Config.Save { newTestFile(t) } for i := range t.DPS { - dataResponse.DPS = append(dataResponse.DPS, t.DPS[i]) + wss.DataPoint.DPS = append(wss.DataPoint.DPS, t.DPS[i]) if t.Config.Save { fileb, err := json.Marshal(t.DPS[i]) if err != nil { @@ -553,28 +553,29 @@ func sendAndSaveData(t *test) (err error) { } t.DataFile.Write(append(fileb, []byte{10}...)) } - t.DPS = slices.Delete(t.DPS, i, i+1) } + t.DPS = make([]shared.DP, 0) - for i := range t.errors { - dataResponse.Errors = append(dataResponse.Errors, t.errors[i]) + t.M.Lock() + errMapClone := make([]shared.TError, 0) + for _, v := range t.errors { + errMapClone = append(errMapClone, v) + } + t.errors = make([]shared.TError, 0) + t.errMap = make(map[string]struct{}) + t.M.Unlock() + + for i := range errMapClone { + wss.DataPoint.Errors = append(wss.DataPoint.Errors, errMapClone[i]) if t.Config.Save { - fileb, err := json.Marshal(t.errors[i]) + fileb, err := json.Marshal(errMapClone[i]) if err != nil { t.AddError(err, "error-marshaling") } t.DataFile.Write(append(fileb, []byte{10}...)) } - t.M.Lock() - t.errors = slices.Delete(t.errors, i, i+1) - t.M.Unlock() } - t.M.Lock() - t.errMap = make(map[string]struct{}) - t.M.Unlock() - - wss.DataPoint = dataResponse for i := range t.cons { if t.cons[i] == nil { continue @@ -616,8 +617,8 @@ func generateDataPoints(t *test) { Remote: r.addr, TTFBL: r.TTFBL, TTFBH: r.TTFBH, - PMSL: r.PMSL, - PMSH: r.PMSH, + RMSL: r.RMSL, + RMSH: r.RMSH, ErrCount: len(t.errors), DroppedPackets: droppedPackets, MemoryUsedPercent: int(currentMemoryStat.UsedPercent), @@ -626,9 +627,9 @@ func generateDataPoints(t *test) { r.m.Lock() r.TTFBH = 0 - r.TTFBL = 999 - r.PMSH = 0 - r.PMSL = 999 + r.TTFBL = 99999999 + r.RMSH = 0 + r.RMSL = 99999999 r.m.Unlock() t.DPS = append(t.DPS, d) @@ -666,8 +667,9 @@ type dialContext func(ctx context.Context, network, address string) (net.Conn, e func newPerformanceReaderForASingleHost(c *shared.Config, host string, port string) (r *netPerfReader) { r = new(netPerfReader) r.addr = net.JoinHostPort(host, port) + r.ip = host r.buf = make([]byte, c.PayloadSize) - r.TTFBL = 999 + r.TTFBL = 99999999 r.client = &http.Client{ Transport: newTransport(c), } @@ -717,9 +719,9 @@ func sendRequestToHost(t *test, r *netPerfReader, cid int) { var resp *http.Response var err error - proto := "http://" - if !t.Config.Insecure { - proto = "https://" + proto := "https://" + if t.Config.Insecure { + proto = "http://" } route := "/404" @@ -768,15 +770,15 @@ func sendRequestToHost(t *test, r *netPerfReader, cid int) { return } - done := time.Since(sent).Milliseconds() + done := time.Since(sent).Microseconds() r.m.Lock() - if done > r.PMSH { - r.PMSH = done + if done > r.RMSH { + r.RMSH = done } - if done < r.PMSL { - r.PMSL = done + if done < r.RMSL { + r.RMSL = done } r.m.Unlock() diff --git a/shared/shared.go b/shared/shared.go index 8361b91..d3f34ef 100644 --- a/shared/shared.go +++ b/shared/shared.go @@ -19,6 +19,7 @@ package shared import ( "bytes" + "encoding/json" "errors" "fmt" "net" @@ -115,8 +116,8 @@ type DP struct { Created time.Time Local string Remote string - PMSH int64 - PMSL int64 + RMSH int64 + RMSL int64 TTFBH int64 TTFBL int64 TX uint64 @@ -127,7 +128,7 @@ type DP struct { CPUUsedPercent int // Client only - Received time.Time + Received time.Time `json:"-"` } type DataReponseToClient struct { @@ -152,6 +153,7 @@ type Config struct { Insecure bool `json:"Insecure"` TestType TestType `json:"TestType"` Output string `json:"Output"` + File string `json:"File"` // AllowLocalInterface bool `json:"AllowLocalInterfaces"` // Client Only @@ -284,3 +286,12 @@ func GetInterfaceAddresses() (list []string, err error) { return } + +func WriteStructAndNewLineToFile(f *os.File, s interface{}) (int, error) { + outb, err := json.Marshal(s) + if err != nil { + return 0, err + } + n, err := f.Write(append(outb, []byte{10}...)) + return n, err +}