Skip to content

Commit

Permalink
Analyze command, automatic ID's, global insecure flag, Microsecond ti…
Browse files Browse the repository at this point in the history
…me and some cleanup (#17)

automatically generating ids and cleanup

Changing timers to Microseconds and adding the analyze command
  • Loading branch information
zveinn authored Oct 12, 2024
1 parent 7c04ee8 commit 0ca547e
Show file tree
Hide file tree
Showing 13 changed files with 323 additions and 90 deletions.
18 changes: 14 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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):
Expand Down
57 changes: 40 additions & 17 deletions client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package client

import (
"bytes"
"context"
"encoding/json"
"errors"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
Expand All @@ -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
}
20 changes: 10 additions & 10 deletions client/table.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down Expand Up @@ -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},
Expand All @@ -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},
Expand Down
176 changes: 176 additions & 0 deletions cmd/hperf/analyze.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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
}
}
1 change: 0 additions & 1 deletion cmd/hperf/bandwidth.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ var bandwidthCMD = cli.Command{
testIDFlag,
bufferSizeFlag,
payloadSizeFlag,
insecureFlag,
restartOnErrorFlag,
dnsServerFlag,
saveTestFlag,
Expand Down
3 changes: 1 addition & 2 deletions cmd/hperf/latency.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ var latencyCMD = cli.Command{
Flags: []cli.Flag{
hostsFlag,
portFlag,
insecureFlag,
concurrencyFlag,
delayFlag,
durationFlag,
Expand All @@ -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
`,
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/hperf/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
`,
}
Expand Down
Loading

0 comments on commit 0ca547e

Please sign in to comment.