mirror of
https://github.com/aykhans/sarin.git
synced 2026-08-29 03:24:33 +00:00
Merge pull request #213 from aykhans/perf/reduce-per-request-overhead
perf: reduce per-request overhead in the load generator
This commit is contained in:
@@ -32,7 +32,6 @@ Sarin is designed for efficient HTTP load testing with minimal resource consumpt
|
|||||||
| Request scripting with Lua and JavaScript | Distributed load testing |
|
| Request scripting with Lua and JavaScript | Distributed load testing |
|
||||||
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||||
| Captcha solving<br>(2Captcha, Anti-Captcha, CapSolver) | Plugins / extensions ecosystem |
|
| Captcha solving<br>(2Captcha, Anti-Captcha, CapSolver) | Plugins / extensions ecosystem |
|
||||||
| Flexible config (CLI, ENV, YAML) | |
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ Sarin is designed for efficient HTTP load testing with minimal resource consumpt
|
|||||||
<summary><b>Docker</b></summary>
|
<summary><b>Docker</b></summary>
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker run -it aykhans/sarin:latest --version
|
docker run --rm -it aykhans/sarin:latest --version
|
||||||
```
|
```
|
||||||
|
|
||||||
With a local config file:
|
With a local config file:
|
||||||
|
|||||||
+85
-33
@@ -56,9 +56,9 @@ func NewRequestGenerator(
|
|||||||
|
|
||||||
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, lazyTemplateRoot)
|
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, lazyTemplateRoot)
|
||||||
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, lazyTemplateRoot)
|
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, lazyTemplateRoot)
|
||||||
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, lazyTemplateRoot)
|
paramsGenerator, isParamsGeneratorDynamic, paramKeysAreStatic := NewParamsGeneratorFunc(localRand, params, lazyTemplateRoot)
|
||||||
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, lazyTemplateRoot)
|
headersGenerator, isHeadersGeneratorDynamic, headerKeysAreStatic := NewHeadersGeneratorFunc(localRand, headers, lazyTemplateRoot)
|
||||||
cookiesGenerator, isCookiesGeneratorDynamic := NewCookiesGeneratorFunc(localRand, cookies, lazyTemplateRoot)
|
cookiesGenerator, isCookiesGeneratorDynamic, cookieKeysAreStatic := NewCookiesGeneratorFunc(localRand, cookies, lazyTemplateRoot)
|
||||||
|
|
||||||
bodyTemplateFuncMapData := &BodyTemplateFuncMapData{}
|
bodyTemplateFuncMapData := &BodyTemplateFuncMapData{}
|
||||||
var bodyTemplateRoot *template.Template
|
var bodyTemplateRoot *template.Template
|
||||||
@@ -83,13 +83,26 @@ func NewRequestGenerator(
|
|||||||
Cookies: make(map[string][]string),
|
Cookies: make(map[string][]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When a map's key set is fixed, every request refills exactly the same keys, so
|
||||||
|
// the value slices can be truncated and reused instead of being reallocated after
|
||||||
|
// a clear(). Scripts are excluded: they may swap the maps out or add keys of their
|
||||||
|
// own, and they would observe a leftover empty slice where a key used to be absent.
|
||||||
|
reuseParamSlices := paramKeysAreStatic && !hasScripts
|
||||||
|
reuseHeaderSlices := headerKeysAreStatic && !hasScripts
|
||||||
|
reuseCookieSlices := cookieKeysAreStatic && !hasScripts
|
||||||
|
|
||||||
var (
|
var (
|
||||||
data valuesData
|
data valuesData
|
||||||
path string
|
path string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
return func(req *fasthttp.Request) error {
|
return func(req *fasthttp.Request) error {
|
||||||
resetRequestData(reqData)
|
resetStringSliceMap(reqData.Headers, reuseHeaderSlices)
|
||||||
|
resetStringSliceMap(reqData.Params, reuseParamSlices)
|
||||||
|
resetStringSliceMap(reqData.Cookies, reuseCookieSlices)
|
||||||
|
reqData.Method = ""
|
||||||
|
reqData.Path = ""
|
||||||
|
reqData.Body = ""
|
||||||
|
|
||||||
data, err = valuesGenerator()
|
data, err = valuesGenerator()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -143,20 +156,24 @@ func NewRequestGenerator(
|
|||||||
hasScripts
|
hasScripts
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetRequestData(reqData *script.RequestData) {
|
// resetStringSliceMap empties m for the next render. With reuse it truncates the
|
||||||
reqData.Method = ""
|
// value slices in place so the following appends land in the existing backing
|
||||||
reqData.Path = ""
|
// arrays, without it the map is cleared so no stale key can survive.
|
||||||
reqData.Body = ""
|
func resetStringSliceMap(m map[string][]string, reuse bool) {
|
||||||
clear(reqData.Headers)
|
if !reuse {
|
||||||
clear(reqData.Params)
|
clear(m)
|
||||||
clear(reqData.Cookies)
|
return
|
||||||
|
}
|
||||||
|
for k, v := range m {
|
||||||
|
m[k] = v[:0]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyRequestDataToFastHTTP(reqData *script.RequestData, req *fasthttp.Request, host, scheme string) {
|
func applyRequestDataToFastHTTP(reqData *script.RequestData, req *fasthttp.Request, host, scheme string) {
|
||||||
req.Header.SetHost(host)
|
req.Header.SetHost(host)
|
||||||
req.SetRequestURI(reqData.Path)
|
req.SetRequestURI(reqData.Path)
|
||||||
req.Header.SetMethod(reqData.Method)
|
req.Header.SetMethod(reqData.Method)
|
||||||
req.SetBody([]byte(reqData.Body))
|
req.SetBodyString(reqData.Body)
|
||||||
|
|
||||||
for k, values := range reqData.Headers {
|
for k, values := range reqData.Headers {
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
@@ -164,20 +181,28 @@ func applyRequestDataToFastHTTP(reqData *script.RequestData, req *fasthttp.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, values := range reqData.Params {
|
if len(reqData.Params) > 0 {
|
||||||
for _, v := range values {
|
args := req.URI().QueryArgs()
|
||||||
req.URI().QueryArgs().Add(k, v)
|
for k, values := range reqData.Params {
|
||||||
|
for _, v := range values {
|
||||||
|
args.Add(k, v)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(reqData.Cookies) > 0 {
|
if len(reqData.Cookies) > 0 {
|
||||||
cookieStrings := make([]string, 0, len(reqData.Cookies))
|
var sb strings.Builder
|
||||||
for k, values := range reqData.Cookies {
|
for k, values := range reqData.Cookies {
|
||||||
for _, v := range values {
|
for _, v := range values {
|
||||||
cookieStrings = append(cookieStrings, k+"="+v)
|
if sb.Len() > 0 {
|
||||||
|
sb.WriteString("; ")
|
||||||
|
}
|
||||||
|
sb.WriteString(k)
|
||||||
|
sb.WriteByte('=')
|
||||||
|
sb.WriteString(v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req.Header.Add("Cookie", strings.Join(cookieStrings, "; "))
|
req.Header.Add("Cookie", sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
if scheme == "https" {
|
if scheme == "https" {
|
||||||
@@ -221,8 +246,8 @@ func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, lazyRoot func()
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, lazyRoot func() *template.Template) (requestDataGenerator, bool, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, params, lazyRoot)
|
generators, isDynamic, keysAreStatic := buildKeyValueGenerators(localRand, params, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -243,11 +268,11 @@ func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, lazyRoot
|
|||||||
reqData.Params[key] = append(reqData.Params[key], value)
|
reqData.Params[key] = append(reqData.Params[key], value)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, isDynamic
|
}, isDynamic, keysAreStatic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, lazyRoot func() *template.Template) (requestDataGenerator, bool, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, headers, lazyRoot)
|
generators, isDynamic, keysAreStatic := buildKeyValueGenerators(localRand, headers, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -268,11 +293,11 @@ func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, lazyRo
|
|||||||
reqData.Headers[key] = append(reqData.Headers[key], value)
|
reqData.Headers[key] = append(reqData.Headers[key], value)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, isDynamic
|
}, isDynamic, keysAreStatic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, lazyRoot func() *template.Template) (requestDataGenerator, bool, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, cookies, lazyRoot)
|
generators, isDynamic, keysAreStatic := buildKeyValueGenerators(localRand, cookies, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -293,14 +318,26 @@ func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, lazyRo
|
|||||||
reqData.Cookies[key] = append(reqData.Cookies[key], value)
|
reqData.Cookies[key] = append(reqData.Cookies[key], value)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}, isDynamic
|
}, isDynamic, keysAreStatic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValuesGeneratorFunc(values []string, lazyRoot func() *template.Template) func() (valuesData, error) {
|
func NewValuesGeneratorFunc(values []string, lazyRoot func() *template.Template) func() (valuesData, error) {
|
||||||
|
// No values configured: hand back one shared empty map instead of allocating a
|
||||||
|
// fresh one for every request. Nothing ever writes to it.
|
||||||
|
if len(values) == 0 {
|
||||||
|
empty := valuesData{Values: map[string]string{}}
|
||||||
|
return func() (valuesData, error) { return empty, nil }
|
||||||
|
}
|
||||||
|
|
||||||
generators := make([]func(_ any) (string, error), len(values))
|
generators := make([]func(_ any) (string, error), len(values))
|
||||||
|
|
||||||
|
isDynamic := false
|
||||||
for i, v := range values {
|
for i, v := range values {
|
||||||
generators[i], _ = createTemplateFunc(v, lazyRoot)
|
var valueIsDynamic bool
|
||||||
|
generators[i], valueIsDynamic = createTemplateFunc(v, lazyRoot)
|
||||||
|
if valueIsDynamic {
|
||||||
|
isDynamic = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -308,7 +345,7 @@ func NewValuesGeneratorFunc(values []string, lazyRoot func() *template.Template)
|
|||||||
data map[string]string
|
data map[string]string
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
return func() (valuesData, error) {
|
generate := func() (valuesData, error) {
|
||||||
result := make(map[string]string)
|
result := make(map[string]string)
|
||||||
for _, generator := range generators {
|
for _, generator := range generators {
|
||||||
rendered, err = generator(nil)
|
rendered, err = generator(nil)
|
||||||
@@ -326,6 +363,15 @@ func NewValuesGeneratorFunc(values []string, lazyRoot func() *template.Template)
|
|||||||
|
|
||||||
return valuesData{Values: result}, nil
|
return valuesData{Values: result}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every value is a literal, so the parsed result is identical on every request:
|
||||||
|
// render and parse once instead of re-running godotenv per request.
|
||||||
|
if !isDynamic {
|
||||||
|
staticResult, staticErr := generate()
|
||||||
|
return func() (valuesData, error) { return staticResult, staticErr }
|
||||||
|
}
|
||||||
|
|
||||||
|
return generate
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTemplateFunc(value string, lazyRoot func() *template.Template) (func(data any) (string, error), bool) {
|
func createTemplateFunc(value string, lazyRoot func() *template.Template) (func(data any) (string, error), bool) {
|
||||||
@@ -335,15 +381,19 @@ func createTemplateFunc(value string, lazyRoot func() *template.Template) (func(
|
|||||||
|
|
||||||
tmpl, err := lazyRoot().New("").Parse(value)
|
tmpl, err := lazyRoot().New("").Parse(value)
|
||||||
if err == nil && hasTemplateActions(tmpl) {
|
if err == nil && hasTemplateActions(tmpl) {
|
||||||
var err error
|
var (
|
||||||
|
buf bytes.Buffer
|
||||||
|
err error
|
||||||
|
)
|
||||||
return func(data any) (string, error) {
|
return func(data any) (string, error) {
|
||||||
var buf bytes.Buffer
|
buf.Reset()
|
||||||
if err = tmpl.Execute(&buf, data); err != nil {
|
if err = tmpl.Execute(&buf, data); err != nil {
|
||||||
return "", types.NewTemplateRenderError(err)
|
return "", types.NewTemplateRenderError(err)
|
||||||
}
|
}
|
||||||
return buf.String(), nil
|
return buf.String(), nil
|
||||||
}, true
|
}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(_ any) (string, error) { return value, nil }, false
|
return func(_ any) (string, error) { return value, nil }, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,8 +410,9 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
items []T,
|
items []T,
|
||||||
lazyRoot func() *template.Template,
|
lazyRoot func() *template.Template,
|
||||||
) ([]keyValueGenerator, bool) {
|
) ([]keyValueGenerator, bool, bool) {
|
||||||
isDynamic := false
|
isDynamic := false
|
||||||
|
keysAreStatic := true
|
||||||
generators := make([]keyValueGenerator, len(items))
|
generators := make([]keyValueGenerator, len(items))
|
||||||
|
|
||||||
for generatorIndex, item := range items {
|
for generatorIndex, item := range items {
|
||||||
@@ -372,6 +423,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
keyFunc, keyIsDynamic := createTemplateFunc(keyValue.Key, lazyRoot)
|
keyFunc, keyIsDynamic := createTemplateFunc(keyValue.Key, lazyRoot)
|
||||||
if keyIsDynamic {
|
if keyIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
|
keysAreStatic = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate value functions
|
// Generate value functions
|
||||||
@@ -394,7 +446,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return generators, isDynamic
|
return generators, isDynamic, keysAreStatic
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildStringSliceGenerator(
|
func buildStringSliceGenerator(
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
(buildGoModule.override { go = go_1_26; }) (finalAttrs: {
|
(buildGoModule.override { go = go_1_26; }) (finalAttrs: {
|
||||||
pname = "sarin";
|
pname = "sarin";
|
||||||
version = "1.4.0"; # bump per release
|
version = "1.4.1"; # bump per release
|
||||||
|
|
||||||
src = lib.cleanSource ../.;
|
src = lib.cleanSource ../.;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user