Files
nuclei/pkg/protocols/common/generators/options_test.go
Dwi Siswanto bb79a061bc perf(generators): optimize MergeMaps to reduce allocs
`MergeMaps` accounts for 11.41% of allocs (13.8
GB) in clusterbomb mode. With 1,305 combinations
per target, this function is called millions of
times in the hot path.

RCA:
* Request generator calls `MergeMaps` with single
  arg on every payload combination, incurring
  variadic overhead.
* Build request merges same maps multiple times
  per request.
* `BuildPayloadFromOptions` recomputes static CLI
  options on every call.
* Variables calls `MergeMaps` $$2×N$$ times per
  variable evaluation (once in loop, once in
  `evaluateVariableValue`)

Changes:

Core optimizations in maps.go:
* Pre-size merged map to avoid rehashing (30-40%
  reduction)
* Add `CopyMap` for efficient single-map copy
  without variadic overhead.
* Add `MergeMapsInto` for in-place mutation when
  caller owns destination.

Hot path fixes:
* Replace `MergeMaps(r.currentPayloads)` with
  `CopyMap(r.currentPayloads)` to eliminates
  allocation on every combination iteration.
* Pre-allocate combined map once, extend in-place
  during `ForEach` loop instead of creating new
  map per variable (eliminates $$2×N$$ allocations
  per request).

Caching with concurrency safety:
* Cache `BuildPayloadFromOptions` computation in
  `sync.Map` keyed by `types.Options` ptr, but
  return copy to prevent concurrent modification.
* Cost: shallow copy of ~10-20 entries vs. full
  merge of vars + env (85-90% savings in typical
  case)
* Clear cache in `closeInternal()` to prevent
  memory leaks when SDK instances are created or
  destroyed.

Estimated impact: 40-60% reduction in `MergeMaps`
allocations (5.5-8.3 GB savings from original
13.8 GB). Safe for concurrent execution and SDK
usage with multiple instances.

Signed-off-by: Dwi Siswanto <git@dw1.io>
2025-12-19 20:19:43 +07:00

93 lines
2.1 KiB
Go

package generators
import (
"sync"
"testing"
"github.com/projectdiscovery/goflags"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
"github.com/stretchr/testify/require"
)
func TestBuildPayloadFromOptionsConcurrency(t *testing.T) {
// Test that BuildPayloadFromOptions is safe for concurrent use
// and returns independent copies that can be modified without races
vars := goflags.RuntimeMap{}
_ = vars.Set("key=value")
opts := &types.Options{
Vars: vars,
}
const numGoroutines = 100
var wg sync.WaitGroup
wg.Add(numGoroutines)
// Each goroutine gets a map and modifies it
for i := 0; i < numGoroutines; i++ {
go func(id int) {
defer wg.Done()
// Get the map (should be a copy of cached data)
m := BuildPayloadFromOptions(opts)
// Modify it - this should not cause races
m["goroutine_id"] = id
m["test_key"] = "test_value"
// Verify original cached value is present
require.Equal(t, "value", m["key"])
}(i)
}
wg.Wait()
}
func TestBuildPayloadFromOptionsCaching(t *testing.T) {
// Test that caching actually works
vars := goflags.RuntimeMap{}
_ = vars.Set("cached=yes")
opts := &types.Options{
Vars: vars,
EnvironmentVariables: false,
}
// First call - builds and caches
m1 := BuildPayloadFromOptions(opts)
require.Equal(t, "yes", m1["cached"])
// Second call - should return copy of cached result
m2 := BuildPayloadFromOptions(opts)
require.Equal(t, "yes", m2["cached"])
// Modify m1 - should not affect m2 since they're copies
m1["modified"] = "in_m1"
require.NotContains(t, m2, "modified")
// Modify m2 - should not affect future calls
m2["modified"] = "in_m2"
m3 := BuildPayloadFromOptions(opts)
require.NotContains(t, m3, "modified")
}
func TestClearOptionsPayloadMap(t *testing.T) {
vars := goflags.RuntimeMap{}
_ = vars.Set("temp=data")
opts := &types.Options{
Vars: vars,
}
// Build and cache
m1 := BuildPayloadFromOptions(opts)
require.Equal(t, "data", m1["temp"])
// Clear the cache
ClearOptionsPayloadMap(opts)
// Verify it still works (rebuilds)
m2 := BuildPayloadFromOptions(opts)
require.Equal(t, "data", m2["temp"])
}