289 lines
6.3 KiB
Go
289 lines
6.3 KiB
Go
/*
|
|
* Copyright 2020 The Dragonfly Authors
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"os/signal"
|
|
"sort"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/go-echarts/statsview"
|
|
"github.com/go-echarts/statsview/viewer"
|
|
"github.com/montanaflynn/stats"
|
|
|
|
"d7y.io/dragonfly/v2/client/config"
|
|
"d7y.io/dragonfly/v2/pkg/net/ip"
|
|
"d7y.io/dragonfly/v2/pkg/unit"
|
|
)
|
|
|
|
var (
|
|
target string
|
|
output string
|
|
proxy string
|
|
con int
|
|
duration *time.Duration
|
|
)
|
|
|
|
func init() {
|
|
flag.StringVar(&target, "url", "", "target url for stress testing, example: http://localhost")
|
|
flag.StringVar(&output, "output", "/tmp/statistics.txt", "all request statistics")
|
|
flag.StringVar(&proxy, "proxy", "", "target proxy for downloading, example: http://127.0.0.1:65001")
|
|
flag.IntVar(&con, "connections", 100, "concurrency count of connections")
|
|
duration = flag.Duration("duration", 100*time.Second, "testing duration")
|
|
}
|
|
|
|
type Result struct {
|
|
StatusCode int
|
|
StartTime time.Time
|
|
EndTime time.Time
|
|
Cost time.Duration
|
|
TaskID string
|
|
PeerID string
|
|
Size int64
|
|
Message string
|
|
}
|
|
|
|
func main() {
|
|
go debug()
|
|
|
|
flag.Parse()
|
|
|
|
var (
|
|
wgProcess = &sync.WaitGroup{}
|
|
wgCollect = &sync.WaitGroup{}
|
|
)
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
resultCh := make(chan *Result, 1024)
|
|
|
|
if proxy != "" {
|
|
pu, err := url.Parse(proxy)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
http.DefaultClient.Transport = &http.Transport{
|
|
Proxy: http.ProxyURL(pu),
|
|
DialContext: (&net.Dialer{
|
|
Timeout: 30 * time.Second,
|
|
KeepAlive: 30 * time.Second,
|
|
}).DialContext,
|
|
ForceAttemptHTTP2: true,
|
|
MaxIdleConns: 100,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
TLSHandshakeTimeout: 10 * time.Second,
|
|
ExpectContinueTimeout: 1 * time.Second,
|
|
}
|
|
}
|
|
|
|
wgCollect.Add(1)
|
|
go collect(wgCollect, resultCh)
|
|
|
|
for i := 0; i < con; i++ {
|
|
wgProcess.Add(1)
|
|
go process(ctx, wgProcess, resultCh)
|
|
}
|
|
|
|
signals := make(chan os.Signal, 1)
|
|
signal.Notify(signals, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
|
|
go forceExit(signals)
|
|
|
|
loop:
|
|
for {
|
|
select {
|
|
case <-time.After(*duration):
|
|
break loop
|
|
case sig := <-signals:
|
|
log.Printf("receive signal: %v", sig)
|
|
break loop
|
|
}
|
|
}
|
|
cancel()
|
|
wgProcess.Wait()
|
|
close(resultCh)
|
|
wgCollect.Wait()
|
|
}
|
|
|
|
func debug() {
|
|
debugAddr := fmt.Sprintf("%s:%d", ip.IPv4.String(), 18066)
|
|
viewer.SetConfiguration(viewer.WithAddr(debugAddr))
|
|
if err := statsview.New().Start(); err != nil {
|
|
log.Println("stat view start failed", err)
|
|
}
|
|
}
|
|
|
|
func forceExit(signals chan os.Signal) {
|
|
var count int
|
|
for {
|
|
select {
|
|
case <-signals:
|
|
count++
|
|
if count > 2 {
|
|
log.Printf("force exit")
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func process(ctx context.Context, wg *sync.WaitGroup, result chan *Result) {
|
|
defer wg.Done()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
}
|
|
|
|
start := time.Now()
|
|
resp, err := http.Get(target)
|
|
if err != nil {
|
|
log.Printf("connect target error: %s", err)
|
|
continue
|
|
}
|
|
var msg string
|
|
n, err := io.Copy(io.Discard, resp.Body)
|
|
if err != nil {
|
|
msg = err.Error()
|
|
log.Printf("discard data error: %s", err)
|
|
}
|
|
end := time.Now()
|
|
result <- &Result{
|
|
StatusCode: resp.StatusCode,
|
|
StartTime: start,
|
|
EndTime: end,
|
|
Cost: end.Sub(start),
|
|
Size: n,
|
|
TaskID: resp.Header.Get(config.HeaderDragonflyTask),
|
|
PeerID: resp.Header.Get(config.HeaderDragonflyPeer),
|
|
Message: msg,
|
|
}
|
|
resp.Body.Close()
|
|
}
|
|
}
|
|
|
|
func collect(wg *sync.WaitGroup, resultCh chan *Result) {
|
|
defer wg.Done()
|
|
var results = make([]*Result, 0, 1000)
|
|
loop:
|
|
for {
|
|
select {
|
|
case result, ok := <-resultCh:
|
|
if !ok {
|
|
break loop
|
|
}
|
|
results = append(results, result)
|
|
}
|
|
}
|
|
|
|
printStatistics(results)
|
|
saveToOutput(results)
|
|
}
|
|
|
|
func printStatistics(results []*Result) {
|
|
sort.Slice(results, func(i, j int) bool {
|
|
return results[i].Cost < results[j].Cost
|
|
})
|
|
printLatency(results)
|
|
printStatus(results)
|
|
printThroughput(results)
|
|
}
|
|
|
|
func printStatus(results []*Result) {
|
|
var status = make(map[int]int)
|
|
for _, v := range results {
|
|
status[v.StatusCode]++
|
|
}
|
|
|
|
fmt.Printf("HTTP codes\n")
|
|
for code, count := range status {
|
|
fmt.Printf("\t%d\t %d\n", code, count)
|
|
}
|
|
}
|
|
|
|
func printLatency(results []*Result) {
|
|
var dur []int64
|
|
|
|
for _, v := range results {
|
|
if v.StatusCode == 200 {
|
|
dur = append(dur, v.EndTime.Sub(v.StartTime).Nanoseconds())
|
|
}
|
|
}
|
|
if len(dur) == 0 {
|
|
log.Printf("empty result with 200 status")
|
|
return
|
|
}
|
|
|
|
d := stats.LoadRawData(dur)
|
|
|
|
min, _ := stats.Min(d)
|
|
max, _ := stats.Max(d)
|
|
mean, _ := stats.Mean(d)
|
|
fmt.Printf("Latency\n")
|
|
fmt.Printf("\tavg\t %v\n", time.Duration(int64(mean)))
|
|
fmt.Printf("\tmin\t %v\n", time.Duration(int64(min)))
|
|
fmt.Printf("\tmax\t %v\n", time.Duration(int64(max)))
|
|
|
|
fmt.Printf("Latency Distribution\n")
|
|
for _, p := range []float64{50, 75, 90, 95, 99} {
|
|
percentile, err := stats.Percentile(d, p)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
fmt.Printf("\t%.0f%%\t%v\n", p, time.Duration(int64(percentile)))
|
|
}
|
|
}
|
|
|
|
func printThroughput(results []*Result) {
|
|
var total int64
|
|
for _, v := range results {
|
|
total += v.Size
|
|
}
|
|
fmt.Printf("Throughput\t%v\n", unit.Bytes(total/int64(*duration/time.Second)))
|
|
fmt.Printf("Request\t\t%d/s\n", len(results)/int(*duration/time.Second))
|
|
}
|
|
|
|
func saveToOutput(results []*Result) {
|
|
out, err := os.OpenFile(output, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0644)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer out.Close()
|
|
for _, v := range results {
|
|
if v.TaskID == "" {
|
|
v.TaskID = "unknown"
|
|
}
|
|
if v.PeerID == "" {
|
|
v.PeerID = "unknown"
|
|
}
|
|
if _, err := out.WriteString(fmt.Sprintf("%s %s %d %v %d %d %s\n",
|
|
v.TaskID, v.PeerID, v.StatusCode, v.Cost,
|
|
v.StartTime.UnixNano()/100, v.EndTime.UnixNano()/100, v.Message)); err != nil {
|
|
log.Panicln("write string failed", err)
|
|
}
|
|
}
|
|
}
|