Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Analyze command, automatic ID's, global insecure flag, Microsecond time and some cleanup #17

Merged
merged 4 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
zveinn marked this conversation as resolved.
Show resolved Hide resolved
}
}

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}
zveinn marked this conversation as resolved.
Show resolved Hide resolved

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]
zveinn marked this conversation as resolved.
Show resolved Hide resolved
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