Skip to content

Commit 5bf313d

Browse files
tmshortclaude
andcommitted
✨ Add e2e profiling toolchain for heap and CPU analysis
Introduces automated memory and CPU profiling for e2e tests with: - Automatic port-forwarding to Kubernetes deployment pprof endpoints - Configurable periodic heap and CPU profile collection with differential timing - Analysis report generation with growth metrics and top allocators - Makefile targets: start-profiling, stop-profiling, analyze-profiles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Todd Short <[email protected]>
1 parent 9530175 commit 5bf313d

29 files changed

+4221
-2
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,6 @@ site
5050

5151
# Temporary files and directories
5252
/test/regression/convert/testdata/tmp/*
53+
54+
# Test profiling artifacts
55+
test-profiles/

Makefile

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,11 +106,11 @@ CATALOGS_MANIFEST := $(MANIFEST_HOME)/default-catalogs.yaml
106106

107107
.PHONY: help
108108
help: #HELP Display essential help.
109-
@awk 'BEGIN {FS = ":[^#]*#HELP"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\n"} /^[a-zA-Z_0-9-]+:.*#HELP / { printf " \033[36m%-21s\033[0m %s\n", $$1, $$2 } ' $(MAKEFILE_LIST)
109+
@awk 'BEGIN {FS = ":[^#]*#HELP"; printf "\nUsage:\n make \033[36m<target>\033[0m\n\n"} /^[a-zA-Z_0-9\/%-]+:.*#HELP / { printf " \033[36m%-21s\033[0m %s\n", $$1, $$2 } ' $(MAKEFILE_LIST)
110110

111111
.PHONY: help-extended
112112
help-extended: #HELP Display extended help.
113-
@awk 'BEGIN {FS = ":.*#(EX)?HELP"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*#(EX)?HELP / { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^#SECTION / { printf "\n\033[1m%s\033[0m\n", substr($$0, 10) } ' $(MAKEFILE_LIST)
113+
@awk 'BEGIN {FS = ":.*#(EX)?HELP"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9\/%-]+:.*#(EX)?HELP / { printf " \033[36m%-25s\033[0m %s\n", $$1, $$2 } /^#SECTION / { printf "\n\033[1m%s\033[0m\n", substr($$0, 10) } ' $(MAKEFILE_LIST)
114114

115115
#SECTION Development
116116

@@ -335,6 +335,27 @@ test-upgrade-experimental-e2e: $(TEST_UPGRADE_E2E_TASKS) #HELP Run upgrade e2e t
335335
e2e-coverage:
336336
COVERAGE_NAME=$(COVERAGE_NAME) ./hack/test/e2e-coverage.sh
337337

338+
TEST_PROFILE_BIN := bin/test-profile
339+
.PHONY: build-test-profiler
340+
build-test-profiler: #EXHELP Build the test profiling tool
341+
cd hack/tools/test-profiling && go build -o ../../../$(TEST_PROFILE_BIN) ./cmd/test-profile
342+
343+
.PHONY: test-test-profiler
344+
test-test-profiler: #EXHELP Run unit tests for the test profiling tool
345+
cd hack/tools/test-profiling && go test -v ./...
346+
347+
.PHONY: start-profiling
348+
start-profiling: build-test-profiler #EXHELP Start profiling in background with auto-generated name (timestamp). Use start-profiling/<name> for custom name.
349+
$(TEST_PROFILE_BIN) start
350+
351+
.PHONY: start-profiling/%
352+
start-profiling/%: build-test-profiler #EXHELP Start profiling in background with specified name. Usage: make start-profiling/<name>
353+
$(TEST_PROFILE_BIN) start $*
354+
355+
.PHONY: stop-profiling
356+
stop-profiling: build-test-profiler #EXHELP Stop profiling and generate analysis report
357+
$(TEST_PROFILE_BIN) stop
358+
338359
#SECTION KIND Cluster Operations
339360

340361
.PHONY: kind-load
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Test Profiling Tools
2+
3+
Collect and analyze heap/CPU profiles during operator-controller tests.
4+
5+
## Quick Start
6+
7+
```bash
8+
# Start profiling
9+
make start-profiling/baseline
10+
11+
# Run tests
12+
make test-e2e
13+
14+
# Stop and analyze
15+
make stop-profiling
16+
17+
# View report
18+
cat test-profiles/baseline/analysis.md
19+
20+
# Compare runs
21+
./bin/test-profile compare baseline optimized
22+
cat test-profiles/comparisons/baseline-vs-optimized.md
23+
```
24+
25+
## Commands
26+
27+
```bash
28+
# Build
29+
make build-test-profiler
30+
31+
# Run test with profiling
32+
./bin/test-profile run <name> [test-target]
33+
34+
# Start/stop daemon
35+
./bin/test-profile start [name] # Daemonizes automatically
36+
./bin/test-profile stop
37+
38+
# Analyze/compare
39+
./bin/test-profile analyze <name>
40+
./bin/test-profile compare <baseline> <optimized>
41+
./bin/test-profile collect # Single snapshot
42+
```
43+
44+
## Configuration
45+
46+
```bash
47+
# Define components to profile (optional - defaults to operator-controller and catalogd)
48+
# Format: "name:namespace:deployment:port;name2:namespace2:deployment2:port2"
49+
export TEST_PROFILE_COMPONENTS="operator-controller:olmv1-system:operator-controller-controller-manager:6060;catalogd:olmv1-system:catalogd-controller-manager:6060"
50+
51+
# Profile custom applications
52+
export TEST_PROFILE_COMPONENTS="my-app:my-ns:my-deployment:8080;api-server:api-ns:api-deployment:9090"
53+
54+
# Other settings
55+
export TEST_PROFILE_INTERVAL=10 # seconds between collections
56+
export TEST_PROFILE_CPU_DURATION=10 # CPU profiling duration in seconds
57+
export TEST_PROFILE_MODE=both # both|heap|cpu
58+
export TEST_PROFILE_DIR=./test-profiles # output directory
59+
export TEST_PROFILE_TEST_TARGET=test-e2e # make target to run
60+
```
61+
62+
**Component Configuration:**
63+
- Each component needs a `/debug/pprof` endpoint (standard Go pprof)
64+
- Local ports are automatically assigned to avoid conflicts
65+
- Default: operator-controller and catalogd in olmv1-system namespace
66+
67+
## Output
68+
69+
```
70+
test-profiles/
71+
├── <name>/
72+
│ ├── operator-controller/{heap,cpu}*.pprof
73+
│ ├── catalogd/{heap,cpu}*.pprof
74+
│ ├── profiler.log
75+
│ └── analysis.md
76+
└── comparisons/<name>-vs-<name>.md
77+
```
78+
79+
## Interactive Analysis
80+
81+
```bash
82+
cd test-profiles/<name>/operator-controller
83+
go tool pprof -top heap23.pprof
84+
go tool pprof -base=heap0.pprof -top heap23.pprof
85+
go tool pprof -text heap23.pprof | grep -i openapi
86+
```
87+
88+
## Requirements
89+
90+
**Runtime:**
91+
- Kubernetes cluster access (via kubeconfig)
92+
93+
**Build:**
94+
- go 1.24+
95+
- make
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/operator-framework/operator-controller/hack/tools/test-profiling/pkg/analyzer"
7+
"github.com/operator-framework/operator-controller/hack/tools/test-profiling/pkg/config"
8+
"github.com/spf13/cobra"
9+
)
10+
11+
var analyzeCmd = &cobra.Command{
12+
Use: "analyze <name>",
13+
Short: "Analyze collected profiles",
14+
Long: `Generate an analysis report from previously collected profiles.
15+
16+
The report includes:
17+
- Memory growth analysis
18+
- Top memory allocators
19+
- CPU profiling results
20+
- OpenAPI and JSON deserialization analysis
21+
22+
Example:
23+
test-profile analyze baseline`,
24+
Args: cobra.ExactArgs(1),
25+
RunE: runAnalyze,
26+
}
27+
28+
func runAnalyze(cmd *cobra.Command, args []string) error {
29+
cfg := config.DefaultConfig()
30+
cfg.Name = args[0]
31+
32+
if err := cfg.Validate(); err != nil {
33+
return err
34+
}
35+
36+
fmt.Printf("📊 Analyzing profiles in: %s\n", cfg.ProfileDir())
37+
38+
a := analyzer.NewAnalyzer(cfg)
39+
return a.Analyze()
40+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"time"
6+
7+
"github.com/operator-framework/operator-controller/hack/tools/test-profiling/pkg/collector"
8+
"github.com/operator-framework/operator-controller/hack/tools/test-profiling/pkg/config"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var collectCmd = &cobra.Command{
13+
Use: "collect",
14+
Short: "Collect a single profile snapshot",
15+
Long: `Collect a single snapshot of heap and CPU profiles from all components.
16+
17+
This is useful for quick spot checks without running the full daemon.
18+
19+
Example:
20+
test-profile collect`,
21+
RunE: runCollect,
22+
}
23+
24+
func runCollect(cmd *cobra.Command, args []string) error {
25+
cfg := config.DefaultConfig()
26+
cfg.Name = time.Now().Format("snapshot-20060102-150405")
27+
28+
if err := cfg.Validate(); err != nil {
29+
return err
30+
}
31+
32+
ctx := context.Background()
33+
34+
c := collector.NewCollector(cfg)
35+
return c.CollectOnce(ctx)
36+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"path/filepath"
6+
7+
"github.com/operator-framework/operator-controller/hack/tools/test-profiling/pkg/comparator"
8+
"github.com/operator-framework/operator-controller/hack/tools/test-profiling/pkg/config"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
var compareCmd = &cobra.Command{
13+
Use: "compare <baseline> <optimized>",
14+
Short: "Compare two profile runs",
15+
Long: `Generate a comparison report between two profile runs.
16+
17+
This helps identify improvements or regressions between different
18+
versions or configurations.
19+
20+
Example:
21+
test-profile compare baseline optimized`,
22+
Args: cobra.ExactArgs(2),
23+
RunE: runCompare,
24+
}
25+
26+
func runCompare(cmd *cobra.Command, args []string) error {
27+
cfg := config.DefaultConfig()
28+
29+
baselineName := args[0]
30+
optimizedName := args[1]
31+
32+
baselineDir := filepath.Join(cfg.OutputDir, baselineName)
33+
optimizedDir := filepath.Join(cfg.OutputDir, optimizedName)
34+
outputDir := filepath.Join(cfg.OutputDir, "comparisons")
35+
36+
fmt.Printf("📊 Comparing %s vs %s\n", baselineName, optimizedName)
37+
38+
c := comparator.NewComparator(baselineDir, optimizedDir, outputDir)
39+
return c.Compare(baselineName, optimizedName)
40+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package main
2+
3+
import (
4+
"os"
5+
)
6+
7+
func main() {
8+
if err := rootCmd.Execute(); err != nil {
9+
os.Exit(1)
10+
}
11+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"github.com/spf13/cobra"
5+
)
6+
7+
var rootCmd = &cobra.Command{
8+
Use: "test-profile",
9+
Short: "Test profiling tool for operator-controller",
10+
Long: `Test profiling tool for collecting, analyzing, and comparing
11+
heap and CPU profiles during operator-controller tests.
12+
13+
Examples:
14+
# Run test with profiling
15+
test-profile run baseline
16+
17+
# Start profiling daemon
18+
test-profile start my-test
19+
20+
# Stop profiling daemon
21+
test-profile stop
22+
23+
# Analyze collected profiles
24+
test-profile analyze baseline
25+
26+
# Compare two runs
27+
test-profile compare baseline optimized
28+
29+
# Collect single snapshot
30+
test-profile collect`,
31+
}
32+
33+
func init() {
34+
rootCmd.AddCommand(runCmd)
35+
rootCmd.AddCommand(startCmd)
36+
rootCmd.AddCommand(stopCmd)
37+
rootCmd.AddCommand(collectCmd)
38+
rootCmd.AddCommand(analyzeCmd)
39+
rootCmd.AddCommand(compareCmd)
40+
}

0 commit comments

Comments
 (0)