Files
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

54 lines
1.6 KiB
Go

package generators
import (
"sync"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
)
// optionsPayloadMap caches the result of BuildPayloadFromOptions per options
// pointer. This supports multiple SDK instances with different options running
// concurrently.
var optionsPayloadMap sync.Map // map[*types.Options]map[string]interface{}
// BuildPayloadFromOptions returns a map with the payloads provided via CLI.
//
// The result is cached per options pointer since options don't change during a run.
// Returns a copy of the cached map to prevent concurrent modification issues.
// Safe for concurrent use with multiple SDK instances.
func BuildPayloadFromOptions(options *types.Options) map[string]interface{} {
if options == nil {
return make(map[string]interface{})
}
if cached, ok := optionsPayloadMap.Load(options); ok {
return CopyMap(cached.(map[string]interface{}))
}
m := make(map[string]interface{})
// merge with vars
if !options.Vars.IsEmpty() {
m = MergeMaps(m, options.Vars.AsMap())
}
// merge with env vars
if options.EnvironmentVariables {
m = MergeMaps(EnvVars(), m)
}
actual, _ := optionsPayloadMap.LoadOrStore(options, m)
// Return a copy to prevent concurrent writes to the cached map
return CopyMap(actual.(map[string]interface{}))
}
// ClearOptionsPayloadMap clears the cached options payload.
// SDK users should call this when disposing of a NucleiEngine instance
// to prevent memory leaks if creating many short-lived instances.
func ClearOptionsPayloadMap(options *types.Options) {
if options != nil {
optionsPayloadMap.Delete(options)
}
}