Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate CPU & PGO profiles #6058

Open
wants to merge 17 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/generate-pgo.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: 👤 Generate PGO

on:
push:
branches: ["dev"]
paths:
- '**.go'
- '**.mod'
workflow_dispatch:

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

# TODO(dwisiswant0): https://go.dev/doc/pgo#merging-profiles

jobs:
pgo:
strategy:
matrix:
targets: [150]
runs-on: ubuntu-latest-16-cores
if: github.repository == 'projectdiscovery/nuclei'
permissions:
contents: write
env:
PGO_FILE: "cmd/nuclei/default.pgo"
LIST_FILE: "/tmp/targets-${{ matrix.targets }}.txt"
PROFILE_MEM: "/tmp/nuclei-profile-${{ matrix.targets }}-targets"
steps:
- uses: actions/checkout@v4
- uses: projectdiscovery/actions/setup/git@v1
- uses: projectdiscovery/actions/setup/go@v1
- name: Generate list
run: for i in {1..${{ matrix.targets }}}; do echo "https://honey.scanme.sh/?_=${i}" >> "${LIST_FILE}"; done
# NOTE(dwisiswant0): use `-no-mhe` flag to get better samples.
- run: go run . -l "${LIST_FILE}" -profile-mem="${PROFILE_MEM}" -no-mhe
working-directory: cmd/nuclei/
- run: mv "${PROFILE_MEM}.cpu" ${PGO_FILE}
# NOTE(dwisiswant0): shall we prune $PGO_FILE git history?
# if we prune it, this won't be linear since it requires a force-push.
# if we don't, the git objects will just keep growing bigger.
#
# Ref:
# - https://go.dev/blog/pgo#:~:text=We%20recommend%20committing%20default.pgo%20files%20to%20your%20repository
# - https://gist.github.com/nottrobin/5758221
- uses: projectdiscovery/actions/commit@v1
with:
files: "${PGO_FILE}"
message: "build: update PGO profile :robot:"
- run: git push origin $GITHUB_REF
- uses: actions/upload-artifact@v4
with:
name: "pgo"
path: "${{ env.PGO_FILE }}"
36 changes: 36 additions & 0 deletions .github/workflows/perf-regression.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: 🔨 Performance Regression

on:
workflow_call:
workflow_dispatch:

jobs:
perf-regression:
runs-on: ubuntu-latest-16-cores
if: github.repository == 'projectdiscovery/nuclei'
env:
BENCH_OUT: "/tmp/bench.out"
steps:
- uses: actions/checkout@v4
- uses: projectdiscovery/actions/setup/go@v1
- run: make build-test
- run: ./bin/nuclei.test -test.run - -test.bench=. -test.benchmem ./cmd/nuclei/ | tee $BENCH_OUT
- uses: actions/cache/restore@v4
with:
path: ./cache
key: ${{ runner.os }}-benchmark
- uses: benchmark-action/github-action-benchmark@v1
with:
name: 'RunEnumeration Benchmark'
tool: 'go'
output-file-path: ${{ env.BENCH_OUT }}
external-data-json-path: ./cache/benchmark-data.json
fail-on-alert: false
github-token: ${{ secrets.GITHUB_TOKEN }}
comment-on-alert: true
summary-always: true
- uses: actions/cache/save@v4
if: github.event_name == 'push'
with:
path: ./cache
key: ${{ runner.os }}-benchmark
26 changes: 17 additions & 9 deletions .github/workflows/perf-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@ jobs:
- uses: projectdiscovery/actions/setup/go@v1
- run: make verify
- name: Generate list
run: for i in {1..${{ matrix.count }}}; do echo "https://scanme.sh/?_=${i}" >> "${LIST_FILE}"; done
- run: NUCLEI_ARGS=host-error-stats go run . -l "${LIST_FILE}" -profile-mem="${PROFILE_MEM}"
run: for i in {1..${{ matrix.count }}}; do echo "https://honey.scanme.sh/?_=${i}" >> "${LIST_FILE}"; done
- run: go run . -l "${LIST_FILE}" -profile-mem="${PROFILE_MEM}"
env:
NUCLEI_ARGS: host-error-stats
working-directory: cmd/nuclei/
- uses: projectdiscovery/actions/flamegraph@v1
id: flamegraph
id: flamegraph-cpu
with:
profile: "${{ env.PROFILE_MEM }}.prof"
name: "nuclei-perf-test-${{ matrix.count }}"
profile: "${{ env.PROFILE_MEM }}.cpu"
name: "${{ env.FLAMEGRAPH_NAME }} CPU profiles"
continue-on-error: true
- if: ${{ steps.flamegraph.outputs.message == '' }}
run: echo "::notice::${FLAMEGRAPH_URL}"
env:
FLAMEGRAPH_URL: ${{ steps.flamegraph.outputs.url }}
- uses: projectdiscovery/actions/flamegraph@v1
id: flamegraph-mem
with:
profile: "${{ env.PROFILE_MEM }}.mem"
name: "${{ env.FLAMEGRAPH_NAME }} memory profiles"
continue-on-error: true
- if: ${{ steps.flamegraph-mem.outputs.message == '' }}
run: |
echo "::notice::CPU flamegraph: ${{ steps.flamegraph-cpu.outputs.url }}"
echo "::notice::Memory (heap) flamegraph: ${{ steps.flamegraph-mem.outputs.url }}"
26 changes: 18 additions & 8 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,23 @@ jobs:
echo "FLAMEGRAPH_NAME=nuclei (PR #${{ github.event.number }})" >> $GITHUB_ENV
- run: ./bin/nuclei -silent -update-templates
- run: ./bin/nuclei -silent -u "${TARGET_URL}" -profile-mem="${PROFILE_MEM}"
- uses: projectdiscovery/actions/flamegraph@master
id: flamegraph
- uses: projectdiscovery/actions/flamegraph@v1
id: flamegraph-cpu
with:
profile: "${{ env.PROFILE_MEM }}.prof"
name: "${{ env.FLAMEGRAPH_NAME }}"
profile: "${{ env.PROFILE_MEM }}.cpu"
name: "${{ env.FLAMEGRAPH_NAME }} CPU profiles"
continue-on-error: true
- if: ${{ steps.flamegraph.outputs.message == '' }}
run: echo "::notice::${FLAMEGRAPH_URL}"
env:
FLAMEGRAPH_URL: ${{ steps.flamegraph.outputs.url }}
- uses: projectdiscovery/actions/flamegraph@v1
id: flamegraph-mem
with:
profile: "${{ env.PROFILE_MEM }}.mem"
name: "${{ env.FLAMEGRAPH_NAME }} memory profiles"
continue-on-error: true
- if: ${{ steps.flamegraph-mem.outputs.message == '' }}
run: |
echo "::notice::CPU flamegraph: ${{ steps.flamegraph-cpu.outputs.url }}"
echo "::notice::Memory (heap) flamegraph: ${{ steps.flamegraph-mem.outputs.url }}"

perf-regression:
needs: ["tests"]
uses: ./.github/workflows/perf-regression.yaml
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,6 @@ vendor
# Profiling & tracing
*.prof
*.pprof
*.trace
*.trace
*.mem
*.cpu
50 changes: 26 additions & 24 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
before:
hooks:
- go mod tidy
- go mod download
- go mod verify

builds:
- main: cmd/nuclei/main.go
binary: nuclei
id: nuclei-cli

env:
- CGO_ENABLED=0

goos: [windows,linux,darwin]
goarch: [amd64,386,arm,arm64]
ignore:
- goos: darwin
goarch: 386
- goos: windows
goarch: arm
- goos: windows
goarch: arm64

flags:
- -trimpath
- main: cmd/nuclei/main.go
binary: nuclei
id: nuclei-cli
env:
- CGO_ENABLED=0
goos: [windows,linux,darwin]
goarch: [amd64,'386',arm,arm64]
ignore:
- goos: darwin
goarch: '386'
- goos: windows
goarch: arm
- goos: windows
goarch: arm64
flags:
- -trimpath
- -pgo=auto
ldflags:
- -s
- -w

#- main: cmd/tmc/main.go
# binary: tmc
Expand All @@ -34,10 +36,10 @@ builds:
# goarch: [amd64]

archives:
- format: zip
id: nuclei
builds: [nuclei-cli]
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}'
- format: zip
id: nuclei
builds: [nuclei-cli]
name_template: '{{ .ProjectName }}_{{ .Version }}_{{ if eq .Os "darwin" }}macOS{{ else }}{{ .Os }}{{ end }}_{{ .Arch }}'

checksum:
algorithm: sha256
Expand Down
27 changes: 17 additions & 10 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,35 +459,42 @@ That's it, you've added a new protocol to Nuclei. The next good step would be to

## Profiling and Tracing

To analyze Nuclei's performance and resource usage, you can generate memory profiles and trace files using the `-profile-mem` flag:
To analyze Nuclei's performance and resource usage, you can generate CPU & memory profiles and trace files using the `-profile-mem` flag:

```bash
nuclei -t nuclei-templates/ -u https://example.com -profile-mem=nuclei-$(git describe --tags)
```

This command creates two files:
This command creates three files:

* `nuclei.prof`: Memory (heap) profile
* `nuclei.cpu`: CPU profile
* `nuclei.mem`: Memory (heap) profile
* `nuclei.trace`: Execution trace

### Analyzing the Memory Profile
### Analyzing the CPU/Memory Profiles

1. View the profile in the terminal:
* View the profile in the terminal:

```bash
go tool pprof nuclei.prof
go tool pprof nuclei.{cpu,mem}
```

2. Display top memory consumers:
* Display overall CPU time for processing $$N$$ targets:

```
go tool pprof -top nuclei.cpu | grep "Total samples"
```

* Display top memory consumers:

```bash
go tool pprof -top nuclei.prof | grep "$(go list -m)" | head -10
go tool pprof -top nuclei.mem | grep "$(go list -m)" | head -10
```

3. Visualize the profile in a web browser:
* Visualize the profile in a web browser:

```bash
go tool pprof -http=:$(shuf -i 1000-99999 -n 1) nuclei.prof
go tool pprof -http=:$(shuf -i 1000-99999 -n 1) nuclei.{cpu,mem}
```

### Analyzing the Trace File
Expand Down
13 changes: 11 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GOFLAGS := -v
LDFLAGS := -s -w

ifneq ($(shell go env GOOS),darwin)
LDFLAGS = -extldflags "-static"
LDFLAGS += -extldflags "-static"
endif

.PHONY: all build build-stats clean devtools-all devtools-bindgen devtools-scrapefuncs
Expand All @@ -26,13 +26,22 @@ clean:

go-build: clean
go-build:
$(GOBUILD) $(GOFLAGS) -ldflags '${LDFLAGS}' $(GOBUILD_ADDITIONAL_ARGS) \
CGO_ENABLED=0 $(GOBUILD) -trimpath $(GOFLAGS) -ldflags '${LDFLAGS}' $(GOBUILD_ADDITIONAL_ARGS) \
-o '${GOBUILD_OUTPUT}' $(GOBUILD_PACKAGES)

build: GOFLAGS = -v -pgo=auto
build: GOBUILD_OUTPUT = ./bin/nuclei
build: GOBUILD_PACKAGES = cmd/nuclei/main.go
build: go-build

build-test: GOFLAGS = -v -pgo=auto
build-test: GOBUILD_OUTPUT = ./bin/nuclei.test
build-test: GOBUILD_PACKAGES = ./cmd/nuclei/
build-test: clean
build-test:
CGO_ENABLED=0 $(GOCMD) test -c -trimpath $(GOFLAGS) -ldflags '${LDFLAGS}' $(GOBUILD_ADDITIONAL_ARGS) \
-o '${GOBUILD_OUTPUT}' ${GOBUILD_PACKAGES}

build-stats: GOBUILD_OUTPUT = ./bin/nuclei-stats
build-stats: GOBUILD_PACKAGES = cmd/nuclei/main.go
build-stats: GOBUILD_ADDITIONAL_ARGS = -tags=stats
Expand Down
37 changes: 24 additions & 13 deletions cmd/nuclei/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,18 +106,20 @@ func main() {

// Profiling & tracing related code
if memProfile != "" {
memProfile = strings.TrimSuffix(memProfile, filepath.Ext(memProfile)) + ".prof"
memProfileFile, err := os.Create(memProfile)
if err != nil {
gologger.Fatal().Msgf("profile: could not create memory profile %q file: %v", memProfile, err)
}
memProfile = strings.TrimSuffix(memProfile, filepath.Ext(memProfile))

traceFilepath := strings.TrimSuffix(memProfile, filepath.Ext(memProfile)) + ".trace"
traceFile, err := os.Create(traceFilepath)
if err != nil {
gologger.Fatal().Msgf("profile: could not create trace %q file: %v", traceFilepath, err)
createProfileFile := func(ext, profileType string) *os.File {
f, err := os.Create(memProfile + ext)
if err != nil {
gologger.Fatal().Msgf("profile: could not create %s profile %q file: %v", profileType, f.Name(), err)
}
return f
}

memProfileFile := createProfileFile(".mem", "memory")
cpuProfileFile := createProfileFile(".cpu", "CPU")
traceFile := createProfileFile(".trace", "trace")

oldMemProfileRate := runtime.MemProfileRate
runtime.MemProfileRate = 4096

Expand All @@ -126,18 +128,27 @@ func main() {
gologger.Fatal().Msgf("profile: could not start trace: %v", err)
}

// Start CPU profiling
if err := pprof.StartCPUProfile(cpuProfileFile); err != nil {
gologger.Fatal().Msgf("profile: could not start CPU profile: %v", err)
}

defer func() {
// Start CPU profiling
// Start heap memory snapshot
if err := pprof.WriteHeapProfile(memProfileFile); err != nil {
gologger.Fatal().Msgf("profile: could not start CPU profile: %v", err)
gologger.Fatal().Msgf("profile: could not write memory profile: %v", err)
}

pprof.StopCPUProfile()
memProfileFile.Close()
traceFile.Close()
trace.Stop()

runtime.MemProfileRate = oldMemProfileRate

gologger.Info().Msgf("Memory profile saved at %q", memProfile)
gologger.Info().Msgf("Traced at %q", traceFilepath)
gologger.Info().Msgf("CPU profile saved at %q", cpuProfileFile.Name())
gologger.Info().Msgf("Memory usage snapshot saved at %q", memProfileFile.Name())
gologger.Info().Msgf("Traced at %q", traceFile.Name())
}()
}

Expand Down
Loading
Loading