merge main

This commit is contained in:
debugtalk
2022-01-13 18:51:00 +08:00
8 changed files with 177 additions and 39 deletions
+5
View File
@@ -29,6 +29,9 @@ var boomCmd = &cobra.Command{
} }
hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate) hrpBoomer := hrp.NewBoomer(spawnCount, spawnRate)
hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate) hrpBoomer.SetRateLimiter(maxRPS, requestIncreaseRate)
if loopCount > 0 {
hrpBoomer.SetLoopCount(loopCount)
}
if !disableConsoleOutput { if !disableConsoleOutput {
hrpBoomer.AddOutput(boomer.NewConsoleOutput()) hrpBoomer.AddOutput(boomer.NewConsoleOutput())
} }
@@ -45,6 +48,7 @@ var (
spawnCount int spawnCount int
spawnRate float64 spawnRate float64
maxRPS int64 maxRPS int64
loopCount int64
requestIncreaseRate string requestIncreaseRate string
memoryProfile string memoryProfile string
memoryProfileDuration time.Duration memoryProfileDuration time.Duration
@@ -61,6 +65,7 @@ func init() {
boomCmd.Flags().StringVar(&requestIncreaseRate, "request-increase-rate", "-1", "Request increase rate, disabled by default.") boomCmd.Flags().StringVar(&requestIncreaseRate, "request-increase-rate", "-1", "Request increase rate, disabled by default.")
boomCmd.Flags().IntVar(&spawnCount, "spawn-count", 1, "The number of users to spawn for load testing") boomCmd.Flags().IntVar(&spawnCount, "spawn-count", 1, "The number of users to spawn for load testing")
boomCmd.Flags().Float64Var(&spawnRate, "spawn-rate", 1, "The rate for spawning users") boomCmd.Flags().Float64Var(&spawnRate, "spawn-rate", 1, "The rate for spawning users")
boomCmd.Flags().Int64Var(&loopCount, "loop-count", -1, "The specify running cycles for load testing")
boomCmd.Flags().StringVar(&memoryProfile, "mem-profile", "", "Enable memory profiling.") boomCmd.Flags().StringVar(&memoryProfile, "mem-profile", "", "Enable memory profiling.")
boomCmd.Flags().DurationVar(&memoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.") boomCmd.Flags().DurationVar(&memoryProfileDuration, "mem-profile-duration", 30*time.Second, "Memory profile duration.")
boomCmd.Flags().StringVar(&cpuProfile, "cpu-profile", "", "Enable CPU profiling.") boomCmd.Flags().StringVar(&cpuProfile, "cpu-profile", "", "Enable CPU profiling.")
+5
View File
@@ -1,5 +1,10 @@
# Release History # Release History
## v0.5.1 (2022-01-13)
- feat: support specifying running cycles for load testing
- fix: ensure last stats reported when stop running
## v0.5.0 (2022-01-08) ## v0.5.0 (2022-01-08)
- feat: support creating and calling custom functions with [go plugin](https://pkg.go.dev/plugin) - feat: support creating and calling custom functions with [go plugin](https://pkg.go.dev/plugin)
+1
View File
@@ -25,6 +25,7 @@ hrp boom [flags]
--cpu-profile-duration duration CPU profile duration. (default 30s) --cpu-profile-duration duration CPU profile duration. (default 30s)
--disable-console-output Disable console output. --disable-console-output Disable console output.
-h, --help help for boom -h, --help help for boom
--loop-count int The specify running cycles for load testing (default -1)
--max-rps int Max RPS that boomer can generate, disabled by default. --max-rps int Max RPS that boomer can generate, disabled by default.
--mem-profile string Enable memory profiling. --mem-profile string Enable memory profiling.
--mem-profile-duration duration Memory profile duration. (default 30s) --mem-profile-duration duration Memory profile duration. (default 30s)
+5
View File
@@ -52,6 +52,11 @@ func (b *Boomer) SetRateLimiter(maxRPS int64, requestIncreaseRate string) {
} }
} }
// SetLoopCount set loop count for test.
func (b *Boomer) SetLoopCount(loopCount int64) {
b.localRunner.loop = &Loop{loopCount: loopCount}
}
// AddOutput accepts outputs which implements the boomer.Output interface. // AddOutput accepts outputs which implements the boomer.Output interface.
func (b *Boomer) AddOutput(o Output) { func (b *Boomer) AddOutput(o Output) {
b.localRunner.addOutput(o) b.localRunner.addOutput(o)
+46
View File
@@ -197,6 +197,8 @@ func convertData(data map[string]interface{}) (output *dataOutput, err error) {
return nil, fmt.Errorf("stats is not []interface{}") return nil, fmt.Errorf("stats is not []interface{}")
} }
errors := data["errors"].(map[string]map[string]interface{})
transactions, ok := data["transactions"].(map[string]int64) transactions, ok := data["transactions"].(map[string]int64)
if !ok { if !ok {
return nil, fmt.Errorf("transactions is not map[string]int64") return nil, fmt.Errorf("transactions is not map[string]int64")
@@ -223,6 +225,7 @@ func convertData(data map[string]interface{}) (output *dataOutput, err error) {
TotalRPS: getCurrentRps(entryTotalOutput.NumRequests), TotalRPS: getCurrentRps(entryTotalOutput.NumRequests),
TotalFailRatio: getTotalFailRatio(entryTotalOutput.NumRequests, entryTotalOutput.NumFailures), TotalFailRatio: getTotalFailRatio(entryTotalOutput.NumRequests, entryTotalOutput.NumFailures),
Stats: make([]*statsEntryOutput, 0, len(stats)), Stats: make([]*statsEntryOutput, 0, len(stats)),
Errors: errors,
} }
// convert stats // convert stats
@@ -329,6 +332,24 @@ var (
) )
) )
// summary for total
var (
summaryResponseTime = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "response_time",
Help: "The summary of response time",
Objectives: map[float64]float64{
0.5: 0.01,
0.9: 0.01,
0.95: 0.005,
},
AgeBuckets: 1,
MaxAge: 100000 * time.Second,
},
[]string{"method", "name"},
)
)
// gauges for total // gauges for total
var ( var (
gaugeUsers = prometheus.NewGauge( gaugeUsers = prometheus.NewGauge(
@@ -367,6 +388,13 @@ var (
Help: "The accumulated number of failed transactions", Help: "The accumulated number of failed transactions",
}, },
) )
gaugeErrors = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "errors",
Help: "The errors of load testing",
},
[]string{"method", "name", "error"},
)
) )
// NewPrometheusPusherOutput returns a PrometheusPusherOutput. // NewPrometheusPusherOutput returns a PrometheusPusherOutput.
@@ -397,6 +425,9 @@ func (o *PrometheusPusherOutput) OnStart() {
gaugeAverageContentLength, gaugeAverageContentLength,
gaugeCurrentRPS, gaugeCurrentRPS,
gaugeCurrentFailPerSec, gaugeCurrentFailPerSec,
gaugeErrors,
// summary for total
summaryResponseTime,
// gauges for total // gauges for total
gaugeUsers, gaugeUsers,
gaugeState, gaugeState,
@@ -449,6 +480,21 @@ func (o *PrometheusPusherOutput) OnEvent(data map[string]interface{}) {
gaugeAverageContentLength.WithLabelValues(method, name).Set(float64(stat.avgContentLength)) gaugeAverageContentLength.WithLabelValues(method, name).Set(float64(stat.avgContentLength))
gaugeCurrentRPS.WithLabelValues(method, name).Set(stat.currentRps) gaugeCurrentRPS.WithLabelValues(method, name).Set(stat.currentRps)
gaugeCurrentFailPerSec.WithLabelValues(method, name).Set(float64(stat.currentFailPerSec)) gaugeCurrentFailPerSec.WithLabelValues(method, name).Set(float64(stat.currentFailPerSec))
for responseTime, count := range stat.ResponseTimes {
var i int64
for i = 0; i < count; i++ {
summaryResponseTime.WithLabelValues(method, name).Observe(float64(responseTime))
}
}
}
// errors
for _, requestError := range output.Errors {
gaugeErrors.WithLabelValues(
requestError["method"].(string),
requestError["name"].(string),
requestError["error"].(string),
).Set(float64(requestError["occurrences"].(int64)))
} }
if err := o.pusher.Push(); err != nil { if err := o.pusher.Push(); err != nil {
+61 -9
View File
@@ -24,6 +24,31 @@ const (
reportStatsInterval = 3 * time.Second reportStatsInterval = 3 * time.Second
) )
type Loop struct {
loopCount int64 // more than 0
acquiredCount int64 // count acquired of load testing
finishedCount int64 // count finished of load testing
}
func (l *Loop) isFinished() bool {
// return true when there are no remaining loop count to test
return atomic.LoadInt64(&l.finishedCount) == l.loopCount
}
func (l *Loop) acquire() bool {
// get one ticket when there are still remaining loop count to test
// return true when getting ticket successfully
if atomic.LoadInt64(&l.acquiredCount) < l.loopCount {
atomic.AddInt64(&l.acquiredCount, 1)
return true
}
return false
}
func (l *Loop) increaseFinishedCount() {
atomic.AddInt64(&l.finishedCount, 1)
}
type runner struct { type runner struct {
state int32 state int32
@@ -37,6 +62,7 @@ type runner struct {
currentClientsNum int32 // current clients count currentClientsNum int32 // current clients count
spawnCount int // target clients to spawn spawnCount int // target clients to spawn
spawnRate float64 spawnRate float64
loop *Loop // specify running cycles
outputs []Output outputs []Output
} }
@@ -78,7 +104,7 @@ func (r *runner) outputOnStart() {
wg.Wait() wg.Wait()
} }
func (r *runner) outputOnEevent(data map[string]interface{}) { func (r *runner) outputOnEvent(data map[string]interface{}) {
size := len(r.outputs) size := len(r.outputs)
if size == 0 { if size == 0 {
return return
@@ -110,7 +136,14 @@ func (r *runner) outputOnStop() {
wg.Wait() wg.Wait()
} }
func (r *runner) spawnWorkers(spawnCount int, spawnRate float64, quit chan bool, spawnCompleteFunc func()) { func (r *runner) reportStats() {
data := r.stats.collectReportData()
data["user_count"] = atomic.LoadInt32(&r.currentClientsNum)
data["state"] = atomic.LoadInt32(&r.state)
r.outputOnEvent(data)
}
func (r *localRunner) spawnWorkers(spawnCount int, spawnRate float64, quit chan bool, spawnCompleteFunc func()) {
log.Info(). log.Info().
Int("spawnCount", spawnCount). Int("spawnCount", spawnCount).
Float64("spawnRate", spawnRate). Float64("spawnRate", spawnRate).
@@ -135,6 +168,9 @@ func (r *runner) spawnWorkers(spawnCount int, spawnRate float64, quit chan bool,
case <-quit: case <-quit:
return return
default: default:
if r.loop != nil && !r.loop.acquire() {
return
}
if r.rateLimitEnabled { if r.rateLimitEnabled {
blocked := r.rateLimiter.Acquire() blocked := r.rateLimiter.Acquire()
if !blocked { if !blocked {
@@ -145,6 +181,12 @@ func (r *runner) spawnWorkers(spawnCount int, spawnRate float64, quit chan bool,
task := r.getTask() task := r.getTask()
r.safeRun(task.Fn) r.safeRun(task.Fn)
} }
if r.loop != nil {
r.loop.increaseFinishedCount()
if r.loop.isFinished() {
r.stop()
}
}
} }
} }
}() }()
@@ -231,12 +273,15 @@ func (r *localRunner) start() {
// all running workers(goroutines) will select on this channel. // all running workers(goroutines) will select on this channel.
// close this channel will stop all running workers. // close this channel will stop all running workers.
quitChan := make(chan bool) quitChan := make(chan bool)
// when this channel is closed, all statistics are reported successfully
reportedChan := make(chan bool)
go r.spawnWorkers(r.spawnCount, r.spawnRate, quitChan, nil) go r.spawnWorkers(r.spawnCount, r.spawnRate, quitChan, nil)
// output setup // output setup
r.outputOnStart() r.outputOnStart()
// start running // start running
go func() {
var ticker = time.NewTicker(reportStatsInterval) var ticker = time.NewTicker(reportStatsInterval)
for { for {
select { select {
@@ -250,18 +295,27 @@ func (r *localRunner) start() {
r.stats.logError(n.requestType, n.name, n.errMsg) r.stats.logError(n.requestType, n.name, n.errMsg)
// report stats // report stats
case <-ticker.C: case <-ticker.C:
data := r.stats.collectReportData() r.reportStats()
data["user_count"] = atomic.LoadInt32(&r.currentClientsNum) // close reportedChan and return if the last stats is reported successfully
data["state"] = atomic.LoadInt32(&r.state) if atomic.LoadInt32(&r.state) == stateQuitting {
r.outputOnEevent(data) close(reportedChan)
return
}
}
}
}()
// stop // stop
case <-r.stopChan: <-r.stopChan
atomic.StoreInt32(&r.state, stateQuitting) atomic.StoreInt32(&r.state, stateQuitting)
// stop previous goroutines without blocking // stop previous goroutines without blocking
// those goroutines will exit when r.safeRun returns // those goroutines will exit when r.safeRun returns
close(quitChan) close(quitChan)
// wait until all stats are reported successfully
<-reportedChan
// stop rate limiter // stop rate limiter
if r.rateLimitEnabled { if r.rateLimitEnabled {
r.rateLimiter.Stop() r.rateLimiter.Stop()
@@ -272,8 +326,6 @@ func (r *localRunner) start() {
atomic.StoreInt32(&r.state, stateStopped) atomic.StoreInt32(&r.state, stateStopped)
return return
}
}
} }
func (r *localRunner) stop() { func (r *localRunner) stop() {
+26 -2
View File
@@ -1,8 +1,11 @@
package boomer package boomer
import ( import (
"sync/atomic"
"testing" "testing"
"time" "time"
"github.com/stretchr/testify/assert"
) )
type HitOutput struct { type HitOutput struct {
@@ -45,13 +48,13 @@ func TestOutputOnStart(t *testing.T) {
} }
} }
func TestOutputOnEevent(t *testing.T) { func TestOutputOnEvent(t *testing.T) {
hitOutput := &HitOutput{} hitOutput := &HitOutput{}
hitOutput2 := &HitOutput{} hitOutput2 := &HitOutput{}
runner := &runner{} runner := &runner{}
runner.addOutput(hitOutput) runner.addOutput(hitOutput)
runner.addOutput(hitOutput2) runner.addOutput(hitOutput2)
runner.outputOnEevent(nil) runner.outputOnEvent(nil)
if !hitOutput.onEvent { if !hitOutput.onEvent {
t.Error("hitOutput's OnEvent has not been called") t.Error("hitOutput's OnEvent has not been called")
} }
@@ -90,3 +93,24 @@ func TestLocalRunner(t *testing.T) {
time.Sleep(4 * time.Second) time.Sleep(4 * time.Second)
runner.stop() runner.stop()
} }
func TestLoopCount(t *testing.T) {
taskA := &Task{
Weight: 10,
Fn: func() {
time.Sleep(time.Second)
},
Name: "TaskA",
}
tasks := []*Task{taskA}
runner := newLocalRunner(2, 2)
runner.loop = &Loop{loopCount: 4}
runner.setTasks(tasks)
go runner.start()
ticker := time.NewTicker(4 * time.Second)
defer ticker.Stop()
<-ticker.C
if !assert.Equal(t, runner.loop.loopCount, atomic.LoadInt64(&runner.loop.finishedCount)) {
t.Fail()
}
}
+1 -1
View File
@@ -1,3 +1,3 @@
package version package version
const VERSION = "v0.5.0" const VERSION = "v0.5.1"