Files
nuclei/pkg/protocols/common/generators/maps.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

95 lines
2.1 KiB
Go

package generators
import (
maps0 "maps"
"reflect"
)
// MergeMapsMany merges many maps into a new map
func MergeMapsMany(maps ...interface{}) map[string][]string {
m := make(map[string][]string)
for _, gotMap := range maps {
val := reflect.ValueOf(gotMap)
if val.Kind() != reflect.Map {
continue
}
appendToSlice := func(key, value string) {
if values, ok := m[key]; !ok {
m[key] = []string{value}
} else {
m[key] = append(values, value)
}
}
for _, e := range val.MapKeys() {
v := val.MapIndex(e)
switch v.Kind() {
case reflect.Slice, reflect.Array:
for i := 0; i < v.Len(); i++ {
appendToSlice(e.String(), v.Index(i).String())
}
case reflect.String:
appendToSlice(e.String(), v.String())
case reflect.Interface:
switch data := v.Interface().(type) {
case string:
appendToSlice(e.String(), data)
case []string:
for _, value := range data {
appendToSlice(e.String(), value)
}
}
}
}
}
return m
}
// MergeMaps merges multiple maps into a new map.
//
// Use [CopyMap] if you need to copy a single map.
// Use [MergeMapsInto] to merge into an existing map.
func MergeMaps(maps ...map[string]interface{}) map[string]interface{} {
mapsLen := 0
for _, m := range maps {
mapsLen += len(m)
}
merged := make(map[string]interface{}, mapsLen)
for _, m := range maps {
maps0.Copy(merged, m)
}
return merged
}
// CopyMap creates a shallow copy of a single map.
func CopyMap(m map[string]interface{}) map[string]interface{} {
if m == nil {
return nil
}
result := make(map[string]interface{}, len(m))
maps0.Copy(result, m)
return result
}
// MergeMapsInto copies all entries from src maps into dst (mutating dst).
//
// Use when dst is a fresh map the caller owns and wants to avoid allocation.
func MergeMapsInto(dst map[string]interface{}, srcs ...map[string]interface{}) {
for _, src := range srcs {
maps0.Copy(dst, src)
}
}
// ExpandMapValues converts values from flat string to string slice
func ExpandMapValues(m map[string]string) map[string][]string {
m1 := make(map[string][]string, len(m))
for k, v := range m {
m1[k] = []string{v}
}
return m1
}