diff --git a/Makefile b/Makefile
index 3b03c1af..51ce2e10 100644
--- a/Makefile
+++ b/Makefile
@@ -58,6 +58,7 @@ endif
test:
@echo "Running unit tests..."
go test -v ./...
+ cd tools/stackcollapse-perf && go test -v ./...
.PHONY: update-deps
update-deps:
@@ -103,6 +104,25 @@ check_lint:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
golangci-lint run
+.PHONY: check_vuln
+check_vuln:
+ @echo "Running govulncheck to check for vulnerabilities..."
+ go install golang.org/x/vuln/cmd/govulncheck@latest
+ govulncheck ./...
+
+.PHONY: check_sec
+check_sec:
+ @echo "Running gosec to check for security issues..."
+ go install github.com/securego/gosec/v2/cmd/gosec@latest
+ gosec ./...
+
+.PHONY: check_semgrep
+check_semgrep:
+ @echo "Running semgrep to check for security issues..."
+ @echo "Please install semgrep from https://semgrep.dev/docs/getting-started/installation/ if not already installed."
+ @echo "Running semgrep..."
+ semgrep scan
+
.PHONY: check_modernize
check_modernize:
@echo "Running go-modernize to check for modernization opportunities..."
@@ -114,7 +134,7 @@ modernize:
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...
.PHONY: check
-check: check_format check_vet check_static check_license check_lint check_modernize
+check: check_format check_vet check_static check_license check_lint check_vuln check_modernize
.PHONY: sweep
sweep:
diff --git a/THIRD_PARTY_PROGRAMS b/THIRD_PARTY_PROGRAMS
index a0bf8b81..5138ef76 100644
--- a/THIRD_PARTY_PROGRAMS
+++ b/THIRD_PARTY_PROGRAMS
@@ -567,29 +567,6 @@ NO WARRANTY
END OF TERMS AND CONDITIONS
-------------------------------------------------------------
-stackcollapse-perf.pl
-# Copyright 2012 Joyent, Inc. All rights reserved.
-# Copyright 2012 Brendan Gregg. All rights reserved.
-#
-# CDDL HEADER START
-#
-# The contents of this file are subject to the terms of the
-# Common Development and Distribution License (the "License").
-# You may not use this file except in compliance with the License.
-#
-# You can obtain a copy of the license at docs/cddl1.txt or
-# http://opensource.org/licenses/CDDL-1.0.
-# See the License for the specific language governing permissions
-# and limitations under the License.
-#
-# When distributing Covered Code, include this CDDL HEADER in each
-# file and include the License file at docs/cddl1.txt.
-# If applicable, add the following below this CDDL HEADER, with the
-# fields enclosed by brackets "[]" replaced with your own identifying
-# information: Portions Copyright [yyyy] [name of copyright owner]
-#
-# CDDL HEADER END
--------------------------------------------------------------
stress-ng
Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
diff --git a/cmd/flame/flame.go b/cmd/flame/flame.go
index 3695c1da..75f45635 100644
--- a/cmd/flame/flame.go
+++ b/cmd/flame/flame.go
@@ -9,6 +9,7 @@ import (
"os"
"perfspect/internal/common"
"perfspect/internal/report"
+ "perfspect/internal/util"
"slices"
"strconv"
"strings"
@@ -40,15 +41,17 @@ var Cmd = &cobra.Command{
var (
flagDuration int
flagFrequency int
- flagPid int
+ flagPids []int
flagNoSystemSummary bool
+ flagMaxDepth int
)
const (
flagDurationName = "duration"
flagFrequencyName = "frequency"
- flagPidName = "pid"
+ flagPidsName = "pids"
flagNoSystemSummaryName = "no-summary"
+ flagMaxDepthName = "max-depth"
)
func init() {
@@ -56,8 +59,9 @@ func init() {
Cmd.Flags().StringSliceVar(&common.FlagFormat, common.FlagFormatName, []string{report.FormatAll}, "")
Cmd.Flags().IntVar(&flagDuration, flagDurationName, 30, "")
Cmd.Flags().IntVar(&flagFrequency, flagFrequencyName, 11, "")
- Cmd.Flags().IntVar(&flagPid, flagPidName, 0, "")
+ Cmd.Flags().IntSliceVar(&flagPids, flagPidsName, nil, "")
Cmd.Flags().BoolVar(&flagNoSystemSummary, flagNoSystemSummaryName, false, "")
+ Cmd.Flags().IntVar(&flagMaxDepth, flagMaxDepthName, 0, "")
common.AddTargetFlags(Cmd)
@@ -101,13 +105,17 @@ func getFlagGroups() []common.FlagGroup {
Help: "number of samples taken per second",
},
{
- Name: flagPidName,
- Help: "pid to collect data from. If not specified, all pids will be collected",
+ Name: flagPidsName,
+ Help: "comma separated list of PIDs. If not specified, all PIDs will be collected",
},
{
Name: common.FlagFormatName,
Help: fmt.Sprintf("choose output format(s) from: %s", strings.Join(append([]string{report.FormatAll}, report.FormatHtml, report.FormatTxt, report.FormatJson), ", ")),
},
+ {
+ Name: flagMaxDepthName,
+ Help: "maximum render depth of call stack in flamegraph (0 = no limit)",
+ },
{
Name: flagNoSystemSummaryName,
Help: "do not include system summary table in report",
@@ -159,8 +167,15 @@ func validateFlags(cmd *cobra.Command, args []string) error {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return err
}
- if flagPid < 0 {
- err := fmt.Errorf("pid must be greater than or equal to 0")
+ for _, pid := range flagPids {
+ if pid < 0 {
+ err := fmt.Errorf("PID must be greater than or equal to 0")
+ fmt.Fprintf(os.Stderr, "Error: %v\n", err)
+ return err
+ }
+ }
+ if flagMaxDepth < 0 {
+ err := fmt.Errorf("max depth must be greater than or equal to 0")
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
return err
}
@@ -177,14 +192,15 @@ func runCmd(cmd *cobra.Command, args []string) error {
if !flagNoSystemSummary {
tableNames = append(tableNames, report.BriefSysSummaryTableName)
}
- tableNames = append(tableNames, report.CodePathFrequencyTableName)
+ tableNames = append(tableNames, report.CallStackFrequencyTableName)
reportingCommand := common.ReportingCommand{
Cmd: cmd,
ReportNamePost: "flame",
ScriptParams: map[string]string{
"Frequency": strconv.Itoa(flagFrequency),
"Duration": strconv.Itoa(flagDuration),
- "PID": strconv.Itoa(flagPid),
+ "PIDs": strings.Join(util.IntSliceToStringSlice(flagPids), ","),
+ "MaxDepth": strconv.Itoa(flagMaxDepth),
},
TableNames: tableNames,
}
diff --git a/internal/report/render_html.go b/internal/report/render_html.go
index 715ac60d..c7778601 100644
--- a/internal/report/render_html.go
+++ b/internal/report/render_html.go
@@ -1284,7 +1284,7 @@ func gaudiTelemetryTableHTMLRenderer(tableValues TableValues, targetName string)
return out
}
-func codePathFrequencyTableHTMLRenderer(tableValues TableValues, targetName string) string {
+func callStackFrequencyTableHTMLRenderer(tableValues TableValues, targetName string) string {
out := `
`
- out += renderFlameGraph("System", tableValues, "System Paths")
- out += renderFlameGraph("Java", tableValues, "Java Paths")
+ out += renderFlameGraph("Native", tableValues, "Native Stacks")
+ out += renderFlameGraph("Java", tableValues, "Java Stacks")
return out
}
diff --git a/internal/report/render_html_flamegraph.go b/internal/report/render_html_flamegraph.go
index 02d3b8bf..5e679c89 100644
--- a/internal/report/render_html_flamegraph.go
+++ b/internal/report/render_html_flamegraph.go
@@ -8,8 +8,9 @@ import (
"bytes"
"encoding/json"
"fmt"
- "log"
+ "log/slog"
"math/rand/v2" // nosemgrep
+ "slices"
"strconv"
"strings"
texttemplate "text/template" // nosemgrep
@@ -81,11 +82,6 @@ type flameGraphTemplateStruct struct {
// Folded data conversion adapted from https://github.com/spiermar/burn
// Copyright © 2017 Martin Spier
// Apache License, Version 2.0
-func reverse(strings []string) {
- for i, j := 0, len(strings)-1; i < j; i, j = i+1, j-1 {
- strings[i], strings[j] = strings[j], strings[i]
- }
-}
type Node struct {
Name string
@@ -123,24 +119,24 @@ func (n *Node) MarshalJSON() ([]byte, error) {
})
}
-var maxStackDepth = 50
-
-func convertFoldedToJSON(folded string) (out string, err error) {
+func convertFoldedToJSON(folded string, maxStackDepth int) (out string, err error) {
rootNode := Node{Name: "root", Value: 0, Children: make(map[string]*Node)}
scanner := bufio.NewScanner(strings.NewReader(folded))
for scanner.Scan() {
line := scanner.Text()
- sep := strings.LastIndex(line, " ")
- s := line[:sep]
- v := line[sep+1:]
- stack := strings.Split(s, ";")
- reverse(stack)
- if len(stack) > maxStackDepth {
- log.Printf("Trimming call stack depth from %d to %d", len(stack), maxStackDepth)
- stack = stack[:maxStackDepth]
+ sep := strings.LastIndex(line, " ") // space separates the call stack from the count
+ callstack := line[:sep]
+ count := line[sep+1:]
+ stack := strings.Split(callstack, ";") // semicolon separates the functions in the call stack
+ slices.Reverse(stack)
+ if maxStackDepth > 0 {
+ if len(stack) > maxStackDepth {
+ slog.Info("Trimming call stack depth", slog.Int("stack length", len(stack)), slog.Int("max depth", maxStackDepth))
+ stack = stack[:maxStackDepth]
+ }
}
var i int
- i, err = strconv.Atoi(v)
+ i, err = strconv.Atoi(count)
if err != nil {
return
}
@@ -152,9 +148,21 @@ func convertFoldedToJSON(folded string) (out string, err error) {
}
func renderFlameGraph(header string, tableValues TableValues, field string) (out string) {
+ maxDepthFieldIndex, err := getFieldIndex("Maximum Render Depth", tableValues)
+ if err != nil {
+ slog.Error("didn't find expected field (Maximum Render Depth) in table", slog.String("error", err.Error()))
+ return
+ }
+ maxDepth := tableValues.Fields[maxDepthFieldIndex].Values[0]
+ maxStackDepth, err := strconv.Atoi(strings.TrimSpace(maxDepth))
+ if err != nil {
+ slog.Error("failed to convert maximum stack depth", slog.String("error", err.Error()))
+ return
+ }
fieldIdx, err := getFieldIndex(field, tableValues)
if err != nil {
- log.Panicf("didn't find expected field (%s) in table: %v", field, err)
+ slog.Error("didn't find expected field in table", slog.String("field", field), slog.String("error", err.Error()))
+ return
}
folded := tableValues.Fields[fieldIdx].Values[0]
if folded == "" {
@@ -166,10 +174,10 @@ func renderFlameGraph(header string, tableValues TableValues, field string) (out
out += msg
return
}
- jsonStacks, err := convertFoldedToJSON(folded)
+ jsonStacks, err := convertFoldedToJSON(folded, maxStackDepth)
if err != nil {
- log.Printf("failed to convert folded data: %v", err)
- out += "Error."
+ slog.Error("failed to convert folded data", slog.String("error", err.Error()))
+ out = ""
return
}
fg := texttemplate.Must(texttemplate.New("flameGraphTemplate").Parse(flameGraphTemplate))
@@ -180,8 +188,8 @@ func renderFlameGraph(header string, tableValues TableValues, field string) (out
Header: header,
})
if err != nil {
- log.Printf("failed to render flame graph template: %v", err)
- out += "Error."
+ slog.Error("failed to render flame graph template", slog.String("error", err.Error()))
+ out = ""
return
}
out += buf.String()
diff --git a/internal/report/table_defs.go b/internal/report/table_defs.go
index 69fb5dec..6d6e1a40 100644
--- a/internal/report/table_defs.go
+++ b/internal/report/table_defs.go
@@ -129,7 +129,7 @@ const (
// config table names
ConfigurationTableName = "Configuration"
// flamegraph table names
- CodePathFrequencyTableName = "Code Path Frequency"
+ CallStackFrequencyTableName = "Call Stack Frequency"
// lock table names
KernelLockAnalysisTableName = "Kernel Lock Analysis"
// common table names
@@ -742,15 +742,14 @@ var tableDefinitions = map[string]TableDefinition{
//
// flamegraph tables
//
- CodePathFrequencyTableName: {
- Name: CodePathFrequencyTableName,
- MenuLabel: CodePathFrequencyTableName,
+ CallStackFrequencyTableName: {
+ Name: CallStackFrequencyTableName,
+ MenuLabel: CallStackFrequencyTableName,
ScriptNames: []string{
- script.ProfileJavaScriptName,
- script.ProfileSystemScriptName,
+ script.CollapsedCallStacksScriptName,
},
- FieldsFunc: codePathFrequencyTableValues,
- HTMLTableRendererFunc: codePathFrequencyTableHTMLRenderer},
+ FieldsFunc: callStackFrequencyTableValues,
+ HTMLTableRendererFunc: callStackFrequencyTableHTMLRenderer},
//
// kernel lock analysis tables
//
@@ -2382,10 +2381,11 @@ func gaudiTelemetryTableValues(outputs map[string]script.ScriptOutput) []Field {
return fields
}
-func codePathFrequencyTableValues(outputs map[string]script.ScriptOutput) []Field {
+func callStackFrequencyTableValues(outputs map[string]script.ScriptOutput) []Field {
fields := []Field{
- {Name: "System Paths", Values: []string{systemFoldedFromOutput(outputs)}},
- {Name: "Java Paths", Values: []string{javaFoldedFromOutput(outputs)}},
+ {Name: "Native Stacks", Values: []string{nativeFoldedFromOutput(outputs)}},
+ {Name: "Java Stacks", Values: []string{javaFoldedFromOutput(outputs)}},
+ {Name: "Maximum Render Depth", Values: []string{maxRenderDepthFromOutput(outputs)}},
}
return fields
}
diff --git a/internal/report/table_helpers.go b/internal/report/table_helpers.go
index 3f75b111..85554ac8 100644
--- a/internal/report/table_helpers.go
+++ b/internal/report/table_helpers.go
@@ -2080,29 +2080,28 @@ func sectionValueFromOutput(output string, sectionName string) string {
}
func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string {
- sections := getSectionsFromOutput(outputs[script.ProfileJavaScriptName].Stdout)
+ sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout)
if len(sections) == 0 {
- slog.Warn("no sections in java profiling output")
+ slog.Warn("no sections in collapsed call stack output")
return ""
}
javaFolded := make(map[string]string)
re := regexp.MustCompile(`^async-profiler (\d+) (.*)$`)
for header, stacks := range sections {
- if stacks == "" {
- slog.Info("no stacks for java process", slog.String("header", header))
- continue
- }
- if strings.HasPrefix(stacks, "Failed to inject profiler") {
- slog.Warn("profiling data error", slog.String("header", header))
- continue
- }
match := re.FindStringSubmatch(header)
if match == nil {
- slog.Warn("profiling data error, regex didn't match header", slog.String("header", header))
continue
}
pid := match[1]
processName := match[2]
+ if stacks == "" {
+ slog.Warn("no stacks for java process", slog.String("header", header))
+ continue
+ }
+ if strings.HasPrefix(stacks, "Failed to inject profiler") {
+ slog.Error("profiling data error", slog.String("header", header))
+ continue
+ }
_, ok := javaFolded[processName]
if processName == "" {
processName = "java (" + pid + ")"
@@ -2113,15 +2112,15 @@ func javaFoldedFromOutput(outputs map[string]script.ScriptOutput) string {
}
folded, err := mergeJavaFolded(javaFolded)
if err != nil {
- slog.Warn("err merging java stacks", slog.String("error", err.Error()))
+ slog.Error("failed to merge java stacks", slog.String("error", err.Error()))
}
return folded
}
-func systemFoldedFromOutput(outputs map[string]script.ScriptOutput) string {
- sections := getSectionsFromOutput(outputs[script.ProfileSystemScriptName].Stdout)
+func nativeFoldedFromOutput(outputs map[string]script.ScriptOutput) string {
+ sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout)
if len(sections) == 0 {
- slog.Warn("no sections in system profiling output")
+ slog.Warn("no sections in collapsed call stack output")
return ""
}
var dwarfFolded, fpFolded string
@@ -2137,7 +2136,21 @@ func systemFoldedFromOutput(outputs map[string]script.ScriptOutput) string {
}
folded, err := mergeSystemFolded(fpFolded, dwarfFolded)
if err != nil {
- slog.Warn("error merging folded stacks", slog.String("error", err.Error()))
+ slog.Error("failed to merge native stacks", slog.String("error", err.Error()))
}
return folded
}
+
+func maxRenderDepthFromOutput(outputs map[string]script.ScriptOutput) string {
+ sections := getSectionsFromOutput(outputs[script.CollapsedCallStacksScriptName].Stdout)
+ if len(sections) == 0 {
+ slog.Warn("no sections in collapsed call stack output")
+ return ""
+ }
+ for header, content := range sections {
+ if header == "maximum depth" {
+ return content
+ }
+ }
+ return ""
+}
diff --git a/internal/report/table_helpers_dimm.go b/internal/report/table_helpers_dimm.go
index 3e0191c1..e0ad06c5 100644
--- a/internal/report/table_helpers_dimm.go
+++ b/internal/report/table_helpers_dimm.go
@@ -5,7 +5,6 @@ package report
import (
"fmt"
- "log"
"log/slog"
"perfspect/internal/script"
"regexp"
@@ -67,7 +66,7 @@ func installedMemoryFromOutput(outputs map[string]script.ScriptOutput) string {
if match != nil {
size, err := strconv.Atoi(match[1])
if err != nil {
- log.Printf("Don't recognize DIMM size format: %s", fields[1])
+ slog.Error("Don't recognize DIMM size format.", slog.String("field", fields[1]))
return ""
}
sum := count * size
diff --git a/internal/script/script.go b/internal/script/script.go
index 49d63260..c992611b 100644
--- a/internal/script/script.go
+++ b/internal/script/script.go
@@ -468,11 +468,13 @@ func prepareTargetToRunScripts(myTarget target.Target, scripts []ScriptDefinitio
for _, dependency := range script.Depends {
dependenciesToCopy[path.Join(targetArchitecture, dependency)] = 1
}
- // add user's path to script
- scriptWithPath := fmt.Sprintf("export PATH=\"%s\"\n%s", userPath, script.ScriptTemplate)
+ // add cd command to the script to change to the target's local temp directory
+ targetScript := fmt.Sprintf("cd %s\n%s", targetTempDirectory, script.ScriptTemplate)
+ // add PATH (including the target temporary directory) to the script
+ targetScript = fmt.Sprintf("export PATH=\"%s\"\n%s", userPath, targetScript)
// write script to the target's local temp directory
scriptPath := path.Join(localTempDir, myTarget.GetName(), scriptNameToFilename(script.Name))
- err = os.WriteFile(scriptPath, []byte(scriptWithPath), 0644)
+ err = os.WriteFile(scriptPath, []byte(targetScript), 0644)
if err != nil {
err = fmt.Errorf("error writing script to local file: %v", err)
return
diff --git a/internal/script/script_defs.go b/internal/script/script_defs.go
index 89c6e111..568dff0c 100644
--- a/internal/script/script_defs.go
+++ b/internal/script/script_defs.go
@@ -112,10 +112,8 @@ const (
TurbostatTelemetryScriptName = "turbostat telemetry"
InstructionTelemetryScriptName = "instruction telemetry"
GaudiTelemetryScriptName = "gaudi telemetry"
-
// flamegraph scripts
- ProfileJavaScriptName = "profile java"
- ProfileSystemScriptName = "profile system"
+ CollapsedCallStacksScriptName = "collapsed call stacks"
// lock scripts
ProfileKernelLockScriptName = "profile kernel lock"
)
@@ -1237,180 +1235,155 @@ fi
Superuser: true,
NeedsKill: true,
},
- // profile (flamegraph) scripts
- ProfileJavaScriptName: {
- Name: ProfileJavaScriptName,
- ScriptTemplate: `# JAVA app (async profiler) call stack collection
-pid={{.PID}}
+ // flamegraph scripts
+ CollapsedCallStacksScriptName: {
+ Name: CollapsedCallStacksScriptName,
+ ScriptTemplate: `# Combined (perf record and async profiler) call stack collection
+pids={{.PIDs}}
duration={{.Duration}}
frequency={{.Frequency}}
+maxdepth={{.MaxDepth}}
ap_interval=0
if [ "$frequency" -ne 0 ]; then
- ap_interval=$((1000000000 / frequency))
+ ap_interval=$((1000000000 / frequency))
fi
-# if pid is provided, use it
-if [ "$pid" -ne 0 ]; then
- # check if the provided pid is running
- if [ ! -d "/proc/$pid" ]; then
- echo "pid $pid not running"
- exit 1
- fi
- # check if pid is a java process, i.e., command line contains java
- if ! tr '\000' ' ' < /proc/"$pid"/cmdline | grep -q java; then
- echo "pid $pid is not a java process"
- exit 1
- fi
- pids="$pid"
-else
- # get all java pids
- pids=$( pgrep java )
-fi
-
-# check if any java pids are found
-if [ -z "$pids" ]; then
- echo "no java processes found"
- exit 1
-fi
-
-# start java profiling for each java pid
-declare -a java_pids=()
-declare -a java_cmds=()
-for pid in $pids ; do
- java_pids+=("$pid")
- java_cmds+=("$( tr '\000' ' ' < /proc/"$pid"/cmdline )")
- # profile pid in background
- async-profiler/profiler.sh start -i "$ap_interval" -o collapsed "$pid"
-done
-
-# wait for the specified duration
-sleep "$duration"
-
-# stop java profiling for each java pid
-for idx in "${!java_pids[@]}"; do
- pid="${java_pids[$idx]}"
- cmd="${java_cmds[$idx]}"
- echo "########## async-profiler $pid $cmd ##########"
- async-profiler/profiler.sh stop -o collapsed "$pid"
-done
-
-`,
- Superuser: true,
- Depends: []string{"async-profiler"},
- },
- ProfileSystemScriptName: {
- Name: ProfileSystemScriptName,
- ScriptTemplate: `# native (perf record) call stack collection
-pid={{.PID}}
-duration={{.Duration}}
-frequency={{.Frequency}}
-
# Function to restore original settings and clean up
-# This function will be called on exit
restore_settings() {
- echo "$PERF_EVENT_PARANOID" > /proc/sys/kernel/perf_event_paranoid
- echo "$KPTR_RESTRICT" > /proc/sys/kernel/kptr_restrict
- rm -f "$perf_fp_data"
- rm -f "$perf_dwarf_data"
- rm -f "$perf_dwarf_folded"
- rm -f "$perf_fp_folded"
+ echo "$PERF_EVENT_PARANOID" > /proc/sys/kernel/perf_event_paranoid
+ echo "$KPTR_RESTRICT" > /proc/sys/kernel/kptr_restrict
if [ -n "$perf_fp_pid" ]; then
kill -0 $perf_fp_pid 2>/dev/null && kill -INT $perf_fp_pid
fi
if [ -n "$perf_dwarf_pid" ]; then
kill -0 $perf_dwarf_pid 2>/dev/null && kill -INT $perf_dwarf_pid
fi
+ for pid in "${java_pids[@]}"; do
+ async-profiler/profiler.sh stop -o collapsed "$pid"
+ done
}
-# create temporary output files
-perf_fp_data=$(mktemp)
-perf_dwarf_data=$(mktemp)
-perf_dwarf_folded=$(mktemp)
-perf_fp_folded=$(mktemp)
-
-# adjust perf_event_paranoid and kptr_restrict
-PERF_EVENT_PARANOID=$( cat /proc/sys/kernel/perf_event_paranoid )
+# Adjust perf_event_paranoid and kptr_restrict
+PERF_EVENT_PARANOID=$(cat /proc/sys/kernel/perf_event_paranoid)
echo -1 >/proc/sys/kernel/perf_event_paranoid
-KPTR_RESTRICT=$( cat /proc/sys/kernel/kptr_restrict )
+KPTR_RESTRICT=$(cat /proc/sys/kernel/kptr_restrict)
echo 0 >/proc/sys/kernel/kptr_restrict
# Ensure settings are restored on exit
trap restore_settings EXIT
-# if pid is not zero, check if the process is running
-if [ "$pid" -ne 0 ]; then
- if ! ps -p "$pid" > /dev/null; then
- echo "Error: Process $pid is not running."
- exit 1
- fi
+# Check if at least one process is running
+if [ -n "$pids" ]; then
+ IFS=',' read -r -a pid_array <<< "$pids"
+ for p in "${pid_array[@]}"; do
+ if ps -p "$p" > /dev/null; then
+ if tr '\000' ' ' < /proc/"$p"/cmdline | grep -q java; then
+ java_pids+=("$p")
+ fi
+ else
+ echo "Error: Process $p is not running." >&2
+ exit 1
+ fi
+ done
+else
+ mapfile -t java_pids < <(pgrep java)
fi
-# frame pointer mode
-# if pid was provided, use it
-if [ "$pid" -ne 0 ]; then
- perf record -F "$frequency" -p "$pid" -g -o "$perf_fp_data" -m 129 &
+# Frame pointer mode
+if [ -n "$pids" ]; then
+ perf record -F "$frequency" -p "$pids" -g -o perf_fp_data -m 129 &
else
- # if no pid was provided, use system-wide profiling
- perf record -F "$frequency" -a -g -o "$perf_fp_data" -m 129 &
+ perf record -F "$frequency" -a -g -o perf_fp_data -m 129 &
fi
perf_fp_pid=$!
if ! kill -0 $perf_fp_pid 2>/dev/null; then
- echo "Failed to start perf record in frame pointer mode"
- exit 1
+ echo "Failed to start perf record in frame pointer mode" >&2
+ exit 1
fi
-# dwarf mode
-# if pid was provided, use it
-if [ "$pid" -ne 0 ]; then
- perf record -F "$frequency" -p "$pid" -g -o "$perf_dwarf_data" -m 257 --call-graph dwarf,8192 &
+# Dwarf mode
+if [ -n "$pids" ]; then
+ perf record -F "$frequency" -p "$pids" -g -o perf_dwarf_data -m 257 --call-graph dwarf,8192 &
else
- # if no pid was provided, use system-wide profiling
- perf record -F "$frequency" -a -g -o "$perf_dwarf_data" -m 257 --call-graph dwarf,8192 &
+ perf record -F "$frequency" -a -g -o perf_dwarf_data -m 257 --call-graph dwarf,8192 &
fi
perf_dwarf_pid=$!
if ! kill -0 $perf_dwarf_pid 2>/dev/null; then
- echo "Failed to start perf record in dwarf mode"
- exit 1
+ echo "Failed to start perf record in dwarf mode" >&2
+ exit 1
fi
-# wait for the specified duration
+# Start Java profiling for each Java PID
+for pid in "${java_pids[@]}"; do
+ java_cmds+=("$(tr '\000' ' ' < /proc/"$pid"/cmdline)")
+ async-profiler/profiler.sh start -i "$ap_interval" -o collapsed "$pid"
+done
+
+# Wait for the specified duration
sleep "$duration"
-# stop perf recording
+# Stop perf recording
if ! kill -0 $perf_fp_pid 2>/dev/null; then
- echo "Frame pointer mode already stopped"
+ echo "Frame pointer mode already stopped" >&2
else
kill -INT $perf_fp_pid
fi
if ! kill -0 $perf_dwarf_pid 2>/dev/null; then
- echo "Dwarf mode already stopped"
+ echo "Dwarf mode already stopped" >&2
else
kill -INT $perf_dwarf_pid
fi
-# wait for perf to finish
+# Stop Java profiling, write output to ap_folded_ files
+for pid in "${java_pids[@]}"; do
+ async-profiler/profiler.sh stop -o collapsed -f ap_folded_"$pid" "$pid"
+done
+
+# Wait for perf to finish
wait ${perf_fp_pid} ${perf_dwarf_pid}
-# collapse perf data
-perf script -i "$perf_dwarf_data" | stackcollapse-perf.pl > "$perf_dwarf_folded"
-perf script -i "$perf_fp_data" | stackcollapse-perf.pl > "$perf_fp_folded"
+# Collapse perf data
+if [ -f perf_dwarf_data ]; then
+ perf script -i perf_dwarf_data > perf_dwarf_stacks
+ stackcollapse-perf perf_dwarf_stacks > perf_dwarf_folded
+else
+ echo "Error: perf_dwarf_data file not found" >&2
+fi
+if [ -f perf_fp_data ]; then
+ perf script -i perf_fp_data > perf_fp_stacks
+ stackcollapse-perf perf_fp_stacks > perf_fp_folded
+else
+ echo "Error: perf_fp_data file not found" >&2
+fi
+
+# Dump results to stdout
+echo "########## maximum depth ##########"
+echo "$maxdepth"
-# Display results
-if [ -f "$perf_dwarf_folded" ]; then
- echo "########## perf_dwarf ##########"
- cat "$perf_dwarf_folded"
+if [ -f perf_dwarf_folded ]; then
+ echo "########## perf_dwarf ##########"
+ cat perf_dwarf_folded
fi
-if [ -f "$perf_fp_folded" ]; then
- echo "########## perf_fp ##########"
- cat "$perf_fp_folded"
+if [ -f perf_fp_folded ]; then
+ echo "########## perf_fp ##########"
+ cat perf_fp_folded
fi
-# Clean up temporary files
-rm -f "$perf_fp_data" "$perf_dwarf_data" "$perf_dwarf_folded" "$perf_fp_folded"
+for idx in "${!java_pids[@]}"; do
+ pid="${java_pids[$idx]}"
+ cmd="${java_cmds[$idx]}"
+ echo "########## async-profiler $pid $cmd ##########"
+ if [ -f ap_folded_"$pid" ]; then
+ cat ap_folded_"$pid"
+ else
+ echo "Error: async-profiler output file not found for PID $pid" >&2
+ fi
+done
`,
- Superuser: true,
- Depends: []string{"perf", "stackcollapse-perf.pl"},
+ Superuser: true,
+ Sequential: true,
+ Depends: []string{"async-profiler", "perf", "stackcollapse-perf"},
},
// lock analysis scripts
ProfileKernelLockScriptName: {
diff --git a/internal/util/util.go b/internal/util/util.go
index c2176719..d76f7df4 100644
--- a/internal/util/util.go
+++ b/internal/util/util.go
@@ -521,3 +521,12 @@ func SelectiveIntRangeToIntList(input string) ([]int, error) {
}
return result, nil
}
+
+// IntSliceToStringSlice converts a slice of integers to a slice of strings.
+func IntSliceToStringSlice(ints []int) []string {
+ strs := make([]string, len(ints))
+ for i, v := range ints {
+ strs[i] = strconv.Itoa(v)
+ }
+ return strs
+}
diff --git a/internal/util/util_test.go b/internal/util/util_test.go
index 17adc27e..7ce36a83 100644
--- a/internal/util/util_test.go
+++ b/internal/util/util_test.go
@@ -149,3 +149,22 @@ func TestSelectiveIntRangeToIntList(t *testing.T) {
}
}
}
+func TestIntSliceToStringSlice(t *testing.T) {
+ tests := []struct {
+ input []int
+ expected []string
+ }{
+ {[]int{1, 2, 3}, []string{"1", "2", "3"}}, // Simple case
+ {[]int{-1, 0, 1}, []string{"-1", "0", "1"}}, // Negative, zero, and positive integers
+ {[]int{}, []string{}}, // Empty slice
+ {[]int{123, 456, 789}, []string{"123", "456", "789"}}, // Larger numbers
+ {[]int{-123, -456, -789}, []string{"-123", "-456", "-789"}}, // Negative larger numbers
+ }
+
+ for _, test := range tests {
+ result := IntSliceToStringSlice(test.input)
+ if !slices.Equal(result, test.expected) {
+ t.Errorf("expected %v, got %v for input %v", test.expected, result, test.input)
+ }
+ }
+}
diff --git a/tools/Makefile b/tools/Makefile
index afb59d72..a39bc266 100644
--- a/tools/Makefile
+++ b/tools/Makefile
@@ -5,9 +5,9 @@
#
default: tools
-.PHONY: default tools async-profiler avx-turbo cpuid dmidecode ethtool fio flamegraph ipmitool lshw lspci msr-tools pcm perf processwatch spectre-meltdown-checker sshpass stress-ng sysstat tsc turbostat
+.PHONY: default tools async-profiler avx-turbo cpuid dmidecode ethtool fio ipmitool lshw lspci msr-tools pcm perf processwatch spectre-meltdown-checker sshpass stackcollapse-perf stress-ng sysstat tsc turbostat
-tools: async-profiler avx-turbo cpuid dmidecode ethtool fio flamegraph ipmitool lshw lspci msr-tools pcm spectre-meltdown-checker sshpass stress-ng sysstat tsc turbostat
+tools: async-profiler avx-turbo cpuid dmidecode ethtool fio ipmitool lshw lspci msr-tools pcm spectre-meltdown-checker sshpass stackcollapse-perf stress-ng sysstat tsc turbostat
mkdir -p bin
cp -R async-profiler bin/
cp avx-turbo/avx-turbo bin/
@@ -15,7 +15,7 @@ tools: async-profiler avx-turbo cpuid dmidecode ethtool fio flamegraph ipmitool
cp dmidecode/dmidecode bin/
cp ethtool/ethtool bin/
cp fio/fio bin/
- cp flamegraph/stackcollapse-perf.pl bin/
+ cp stackcollapse-perf/stackcollapse-perf bin/
cp ipmitool/src/ipmitool.static bin/ipmitool
cp lshw/src/lshw-static bin/lshw
cp lspci/lspci bin/
@@ -103,14 +103,6 @@ ifeq ("$(wildcard fio/config.log)","")
endif
cd fio && make
-FLAMEGRAPH_VERSION := "master"
-flamegraph:
-ifeq ("$(wildcard flamegraph)","")
- git clone https://github.com/brendangregg/FlameGraph.git flamegraph
- # small modification to script to include module name in output
- cd flamegraph && sed -i '382 a \\t\t\t\t$$func = \$$func."'" "'".\$$mod;\t# add module name' stackcollapse-perf.pl
-endif
-
IPMITOOL_VERSION := "IPMITOOL_1_8_19"
ipmitool:
ifeq ("$(wildcard ipmitool)","")
@@ -213,6 +205,9 @@ ifeq ("$(wildcard sshpass)","")
endif
cd sshpass && make
+stackcollapse-perf:
+ cd stackcollapse-perf && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
+
STRESS_NG_VERSION := "V0.13.08"
stress-ng:
ifeq ("$(wildcard stress-ng)","")
@@ -252,7 +247,6 @@ reset:
cd dmidecode && git clean -fdx && git reset --hard
cd ethtool && git clean -fdx && git reset --hard
cd fio && git clean -fdx && git reset --hard
- cd flamegraph && git clean -fdx && git reset --hard
cd ipmitool && git clean -fdx && git reset --hard
cd lshw && git clean -fdx && git reset --hard
cd lspci && git clean -fdx && git reset --hard
@@ -275,5 +269,5 @@ libcrypt.tar.gz:
libs: glibc-2.19.tar.bz2 zlib.tar.gz libcrypt.tar.gz
oss-source: reset libs
- tar --exclude-vcs -czf oss_source.tgz async-profiler/ cpuid/ dmidecode/ ethtool/ fio/ flamegraph/ ipmitool/ lshw/ lspci/ msr-tools/ pcm/ spectre-meltdown-checker/ sshpass/ stress-ng/ sysstat/ linux_turbostat/tools/power/x86/turbostat glibc-2.19.tar.bz2 zlib.tar.gz libcrypt.tar.gz
+ tar --exclude-vcs -czf oss_source.tgz async-profiler/ cpuid/ dmidecode/ ethtool/ fio/ ipmitool/ lshw/ lspci/ msr-tools/ pcm/ spectre-meltdown-checker/ sshpass/ stress-ng/ sysstat/ linux_turbostat/tools/power/x86/turbostat glibc-2.19.tar.bz2 zlib.tar.gz libcrypt.tar.gz
md5sum oss_source.tgz > oss_source.tgz.md5
diff --git a/tools/stackcollapse-perf/go.mod b/tools/stackcollapse-perf/go.mod
new file mode 100644
index 00000000..b3cd282a
--- /dev/null
+++ b/tools/stackcollapse-perf/go.mod
@@ -0,0 +1,3 @@
+module intel.com/stackcollapse-perf
+
+go 1.24
diff --git a/tools/stackcollapse-perf/stackcollapse-perf.go b/tools/stackcollapse-perf/stackcollapse-perf.go
new file mode 100644
index 00000000..e6bf29b8
--- /dev/null
+++ b/tools/stackcollapse-perf/stackcollapse-perf.go
@@ -0,0 +1,328 @@
+package main
+
+// Copyright (C) 2021-2025 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+
+// This code is a port of the Perl script stackcollapse-perf.pl from Brendan
+// Gregg's Flamegraph project -- github.com/brendangregg/FlameGraph.
+// All credit to Brendan Gregg for the original implementation and the flamegraph concept.
+
+import (
+ "bufio"
+ "flag"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+)
+
+// Config holds configuration options for processing stacks.
+// It includes options for annotating kernel and JIT symbols, including process names, PIDs, TIDs, and addresses,
+// as well as options for tidying Java and generic function names, filtering events, and showing inline or context information.
+type Config struct {
+ AnnotateKernel bool
+ AnnotateJit bool
+ IncludePname bool
+ IncludePid bool
+ IncludeTid bool
+ IncludeAddrs bool
+ TidyJava bool
+ TidyGeneric bool
+ EventFilter string
+ ShowInline bool
+ ShowContext bool
+ SrcLineInInput bool
+}
+
+// StackAggregator aggregates stack traces and their counts.
+// It provides a method to remember stacks and their associated counts.
+type StackAggregator struct {
+ collapsed map[string]int
+}
+
+// NewStackAggregator creates and returns a new StackAggregator instance.
+func NewStackAggregator() *StackAggregator {
+ return &StackAggregator{collapsed: make(map[string]int)}
+}
+
+// RememberStack adds a stack trace and its count to the aggregator.
+func (sa *StackAggregator) RememberStack(stack string, count int) {
+ sa.collapsed[stack] += count
+}
+
+func main() {
+ var config Config
+
+ flag.BoolVar(&config.AnnotateKernel, "kernel", false, "annotate kernel functions with a _[k]")
+ flag.BoolVar(&config.AnnotateJit, "jit", false, "annotate jit functions with a _[j]")
+ var annotateAll bool
+ flag.BoolVar(&annotateAll, "all", false, "all annotations (--kernel --jit)")
+ flag.BoolVar(&config.IncludePname, "pname", true, "include process names in stacks")
+ flag.BoolVar(&config.IncludePid, "pid", false, "include PID with process names")
+ flag.BoolVar(&config.IncludeTid, "tid", false, "include TID and PID with process names")
+ flag.BoolVar(&config.IncludeAddrs, "addrs", false, "include raw addresses where symbols can't be found")
+ flag.BoolVar(&config.TidyJava, "java", true, "condense Java signatures")
+ flag.BoolVar(&config.TidyGeneric, "generic", true, "clean up function names a little")
+ flag.StringVar(&config.EventFilter, "event-filter", "", "event name filter")
+ flag.BoolVar(&config.ShowInline, "inline", false, "un-inline using addr2line")
+ flag.BoolVar(&config.ShowContext, "context", false, "adds source context to --inline")
+ flag.BoolVar(&config.SrcLineInInput, "srcline", false, "parses output of 'perf script -F+srcline' and adds source context")
+
+ flag.Usage = func() {
+ fmt.Fprintf(os.Stderr, "USAGE: %s [options] [infile] > outfile\n", os.Args[0])
+ fmt.Fprintf(os.Stderr, "\nOptions:\n")
+ flag.PrintDefaults()
+ fmt.Fprintf(os.Stderr, "\n[1] perf script must emit both PID and TIDs for these to work; eg, Linux < 4.1:\n")
+ fmt.Fprintf(os.Stderr, " perf script -f comm,pid,tid,cpu,time,event,ip,sym,dso,trace\n")
+ fmt.Fprintf(os.Stderr, " for Linux >= 4.1:\n")
+ fmt.Fprintf(os.Stderr, " perf script -F comm,pid,tid,cpu,time,event,ip,sym,dso,trace\n")
+ fmt.Fprintf(os.Stderr, " If you save this output add --header on Linux >= 3.14 to include perf info.\n")
+ }
+
+ flag.Parse()
+
+ if annotateAll {
+ config.AnnotateKernel = true
+ config.AnnotateJit = true
+ }
+
+ if config.ShowInline {
+ fmt.Fprintf(os.Stderr, "--inline is not implemented\n")
+ os.Exit(1)
+ }
+ if config.SrcLineInInput {
+ fmt.Fprintf(os.Stderr, "--srcline is not implemented\n")
+ os.Exit(1)
+ }
+
+ var input *os.File
+ var err error
+
+ args := flag.Args()
+ if len(args) > 0 {
+ input, err = os.Open(args[0])
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error opening file: %s\n", err)
+ os.Exit(1)
+ }
+ defer input.Close()
+ } else {
+ input = os.Stdin
+ }
+
+ err = ProcessStacks(input, os.Stdout, config)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error processing stacks: %s\n", err)
+ os.Exit(1)
+ }
+}
+
+// Regular expressions for parsing the perf output
+var (
+ eventLineRegex = regexp.MustCompile(`^(\S.+?)\s+(\d+)\/*(\d+)*\s+`)
+ eventTypeRegex = regexp.MustCompile(`:\s*(\d+)*\s+(\S+):\s*$`)
+ stackLineRegex = regexp.MustCompile(`^\s*(\w+)\s*(.+) \((.*)\)`)
+ stripSymbolOffsetRegex = regexp.MustCompile(`\+0x[\da-f]+$`)
+ goMethodRegex = regexp.MustCompile(`\.\(.*\)\.`)
+ jitRegex = regexp.MustCompile(`/tmp/perf-\d+\.map`)
+)
+
+// ProcessStacks processes stack traces from the input reader and writes the collapsed stacks to the output writer.
+// It uses the provided configuration to control the processing behavior.
+func ProcessStacks(input io.Reader, output io.Writer, config Config) error {
+ var stack []string
+ var processName string
+ var period int
+ aggregator := NewStackAggregator()
+ scanner := bufio.NewScanner(input)
+
+ // main loop, read lines from stdin
+ for scanner.Scan() {
+ line := scanner.Text()
+ // skip comments
+ if strings.HasPrefix(line, "#") {
+ continue
+ }
+ // skip empty lines that are not after a stack
+ if line == "" && processName == "" {
+ continue
+ }
+ // check for end of stack
+ if line == "" {
+ if config.IncludePname {
+ stack = append([]string{processName}, stack...)
+ }
+ if stack != nil {
+ aggregator.RememberStack(strings.Join(stack, ";"), period)
+ }
+ stack = nil
+ processName = ""
+ continue
+ }
+ // check for event record
+ if eventLineRegex.MatchString(line) {
+ var err error
+ processName, period, err = handleEventRecord(line, config)
+ if err != nil {
+ fmt.Fprintf(output, "Error: %s\n", err)
+ }
+ continue
+ }
+ // check for stack line
+ if stackLineRegex.MatchString(line) {
+ err := handleStackLine(line, &stack, processName, config)
+ if err != nil {
+ fmt.Fprintf(output, "Error: %s\n", err)
+ }
+ continue
+ }
+ }
+ // Check for errors during scanning
+ if err := scanner.Err(); err != nil {
+ fmt.Fprintf(os.Stderr, "Error reading input: %s\n", err)
+ return err
+ }
+ // Output results
+ keys := make([]string, 0, len(aggregator.collapsed))
+ for k := range aggregator.collapsed {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ fmt.Fprintf(output, "%s %d\n", k, aggregator.collapsed[k])
+ }
+ return nil
+}
+
+// handleEventRecord parses an event record line and updates the process name and period based on the configuration.
+func handleEventRecord(line string, config Config) (processName string, period int, err error) {
+ matches := eventLineRegex.FindStringSubmatch(line)
+ if matches == nil {
+ return
+ }
+
+ comm, pid, tid := matches[1], matches[2], matches[3]
+ if tid == "" {
+ tid = pid
+ pid = "?"
+ }
+
+ if eventMatches := eventTypeRegex.FindStringSubmatch(line); eventMatches != nil {
+ eventPeriod := eventMatches[1]
+ if eventPeriod == "" {
+ period = 1
+ } else {
+ var eventPeriodInt int
+ eventPeriodInt, err = strconv.Atoi(eventPeriod)
+ if err != nil {
+ err = fmt.Errorf("failed to parse event period: %s, error: %v", eventPeriod, err)
+ return
+ }
+ period = eventPeriodInt
+ }
+ event := eventMatches[2]
+
+ if config.EventFilter == "" {
+ config.EventFilter = event
+ } else if event != config.EventFilter {
+ err = fmt.Errorf("event type mismatch: %s != %s", event, config.EventFilter)
+ return
+ }
+ }
+
+ if config.IncludeTid {
+ processName = fmt.Sprintf("%s-%s/%s", comm, pid, tid)
+ } else if config.IncludePid {
+ processName = fmt.Sprintf("%s-%s", comm, pid)
+ } else {
+ processName = comm
+ }
+ processName = strings.ReplaceAll(processName, " ", "_")
+ return
+}
+
+// handleStackLine parses a stack line and appends the function name to the stack based on the configuration.
+func handleStackLine(line string, stack *[]string, pname string, config Config) error {
+ matches := stackLineRegex.FindStringSubmatch(line)
+ if matches == nil || pname == "" {
+ return nil
+ }
+
+ pc, rawFunc, mod := matches[1], matches[2], matches[3]
+
+ rawFunc = stripSymbolOffsetRegex.ReplaceAllString(rawFunc, "")
+
+ // skip process names
+ if strings.HasPrefix(rawFunc, "(") {
+ return nil
+ }
+
+ *stack = append(processFunctionName(rawFunc, mod, pc, config), *stack...)
+ return nil
+}
+
+// processFunctionName processes a raw function name, module, and program counter (PC) based on the configuration.
+// It returns a slice of processed function names.
+func processFunctionName(rawFunc, mod, pc string, config Config) []string {
+ var inline []string
+ for funcname := range strings.SplitSeq(rawFunc, "->") {
+ if funcname == "[unknown]" { // use module name instead, if known
+ if mod != "[unknown]" {
+ funcname = filepath.Base(mod)
+ } else {
+ funcname = "unknown"
+ }
+
+ if config.IncludeAddrs {
+ funcname = fmt.Sprintf("[%s <%s>]", funcname, pc)
+ } else {
+ funcname = fmt.Sprintf("[%s]", funcname)
+ }
+ }
+ if config.TidyGeneric {
+ funcname = strings.ReplaceAll(funcname, ";", ":")
+ if !goMethodRegex.MatchString(funcname) {
+ funcname = stripParenArgsUnlessAnonymous(funcname)
+ }
+ funcname = strings.ReplaceAll(funcname, "\"", "")
+ funcname = strings.ReplaceAll(funcname, "'", "")
+ }
+ if config.TidyJava {
+ if strings.Contains(funcname, "/") {
+ funcname = strings.TrimPrefix(funcname, "L")
+ }
+ }
+ // annotations
+ if len(inline) > 0 {
+ if !strings.Contains(funcname, "_[i]") {
+ funcname = fmt.Sprintf("%s_[i]", funcname)
+ }
+ } else if config.AnnotateKernel && (strings.HasPrefix(mod, "[") || strings.HasSuffix(mod, "vmlinux")) && !strings.Contains(mod, "unknown") {
+ funcname = fmt.Sprintf("%s_[k]", funcname)
+ } else if config.AnnotateJit && jitRegex.MatchString(mod) {
+ if !strings.Contains(funcname, "_[j]") {
+ funcname = fmt.Sprintf("%s_[j]", funcname)
+ }
+ }
+
+ inline = append(inline, funcname)
+ }
+ return inline
+}
+
+// stripParenArgsUnlessAnonymous removes everything after the first '(' unless it is immediately followed by 'anonymous namespace'.
+func stripParenArgsUnlessAnonymous(s string) string {
+ idx := strings.Index(s, "(")
+ if idx == -1 {
+ return s
+ }
+ // Check if what follows is 'anonymous namespace'
+ if strings.HasPrefix(s[idx:], "(anonymous namespace") {
+ return s
+ }
+ return s[:idx]
+}
diff --git a/tools/stackcollapse-perf/stackcollapse-perf_test.go b/tools/stackcollapse-perf/stackcollapse-perf_test.go
new file mode 100644
index 00000000..acc38ec5
--- /dev/null
+++ b/tools/stackcollapse-perf/stackcollapse-perf_test.go
@@ -0,0 +1,192 @@
+package main
+
+// Copyright (C) 2021-2025 Intel Corporation
+// SPDX-License-Identifier: BSD-3-Clause
+
+import (
+ "bytes"
+ "strings"
+ "testing"
+)
+
+// keep for debugging
+// func TestProcessStacksFromFile(t *testing.T) {
+// filePath := filepath.Join("testdata", "perf_fp_stacks")
+// file, err := os.Open(filePath)
+// if err != nil {
+// t.Fatalf("failed to open test file: %v", err)
+// }
+// defer file.Close()
+
+// output := &bytes.Buffer{}
+// config := Config{
+// IncludePname: true,
+// IncludePid: false,
+// IncludeTid: false,
+// IncludeAddrs: false,
+// TidyJava: true,
+// TidyGeneric: true,
+// }
+
+// err = ProcessStacks(file, output, config)
+// if err != nil {
+// t.Fatalf("unexpected error: %v", err)
+// }
+
+// if output.Len() == 0 {
+// t.Errorf("expected output, got empty result")
+// }
+// }
+
+func TestProcessStacks(t *testing.T) {
+ input := strings.NewReader(`
+stress-ng-cpu 1230556 [121] 6223127.073349: 293637623 cycles:P:
+ 61e248df6091 [unknown] (/usr/bin/stress-ng)
+
+stress-ng-cpu 1230793 [098] 6223127.074783: 307465331 cycles:P:
+ ffffffffa7c00f0b asm_sysvec_apic_timer_interrupt+0x1b ([kernel.kallsyms])
+ 760c9702dc5d [unknown] (/usr/lib/x86_64-linux-gnu/libm.so.6)
+ 760c96fda3a2 sincosf64x+0x122 (/usr/lib/x86_64-linux-gnu/libm.so.6)
+
+ `)
+ output := &bytes.Buffer{}
+
+ config := Config{
+ IncludePname: true,
+ IncludePid: false,
+ IncludeTid: false,
+ IncludeAddrs: false,
+ TidyJava: true,
+ TidyGeneric: true,
+ }
+
+ err := ProcessStacks(input, output, config)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ expected := "stress-ng-cpu;[stress-ng] 293637623\nstress-ng-cpu;sincosf64x;[libm.so.6];asm_sysvec_apic_timer_interrupt 307465331\n"
+ if output.String() != expected {
+ t.Errorf("expected %q, got %q", expected, output.String())
+ }
+}
+
+func TestHandleEventRecord(t *testing.T) {
+ line := "stress-ng-cpu 1230556 [121] 6223127.073349: 293637623 cycles:P: "
+ var processName string
+ var period int
+ config := Config{
+ IncludePname: true,
+ IncludePid: false,
+ IncludeTid: false,
+ IncludeAddrs: false,
+ TidyJava: true,
+ TidyGeneric: true,
+ }
+
+ processName, period, err := handleEventRecord(line, config)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ expectedProcessName := "stress-ng-cpu"
+ if processName != "stress-ng-cpu" {
+ t.Errorf("expected processName to be '%s', got %q", expectedProcessName, processName)
+ }
+ expectedPeriod := 293637623
+ if period != expectedPeriod {
+ t.Errorf("expected period to be %d, got %d", expectedPeriod, period)
+ }
+}
+
+func TestHandleStackLine(t *testing.T) {
+ line := "0x1234 someFunction (module)"
+ var stack []string
+ processName := "main"
+ config := Config{
+ IncludePname: true,
+ IncludePid: false,
+ IncludeTid: false,
+ IncludeAddrs: false,
+ TidyJava: true,
+ TidyGeneric: true,
+ }
+
+ err := handleStackLine(line, &stack, processName, config)
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if len(stack) != 1 || stack[0] != "someFunction" {
+ t.Errorf("expected stack to contain 'someFunction', got %v", stack)
+ }
+}
+
+func TestProcessFunctionName(t *testing.T) {
+ tests := []struct {
+ rawFunc string
+ mod string
+ pc string
+ config Config
+ expected []string
+ }{
+ {
+ rawFunc: "[unknown]",
+ mod: "[kernel]",
+ pc: "0x1234",
+ config: Config{IncludeAddrs: true},
+ expected: []string{"[[kernel] <0x1234>]"},
+ },
+ {
+ rawFunc: "Lcom/example/MyClass",
+ mod: "[unknown]",
+ pc: "0x5678",
+ config: Config{TidyJava: true},
+ expected: []string{"com/example/MyClass"},
+ },
+ {
+ rawFunc: "someFunction;bar\"hello'world(remove me).foo",
+ mod: "module.so",
+ pc: "0x9abc",
+ config: Config{TidyGeneric: true},
+ expected: []string{"someFunction:barhelloworld"},
+ },
+ }
+
+ for _, test := range tests {
+ result := processFunctionName(test.rawFunc, test.mod, test.pc, test.config)
+ if len(result) != len(test.expected) {
+ t.Errorf("expected %d results, got %d", len(test.expected), len(result))
+ continue
+ }
+ for i := range result {
+ if result[i] != test.expected[i] {
+ t.Errorf("expected %q, got %q", test.expected[i], result[i])
+ }
+ }
+ }
+}
+func TestStripParenArgsUnlessAnonymous(t *testing.T) {
+ tests := []struct {
+ input string
+ expected string
+ }{
+ {"foo(int, float)", "foo"},
+ {"bar()", "bar"},
+ {"baz", "baz"},
+ {"qux(anonymous namespace)", "qux(anonymous namespace)"},
+ {"func(anonymous namespace::Type)", "func(anonymous namespace::Type)"},
+ {"func(anonymous namespace", "func(anonymous namespace"},
+ {"func(int) (anonymous namespace)", "func"},
+ {"func()", "func"},
+ {"func", "func"},
+ {"func(abc", "func"},
+ }
+
+ for _, test := range tests {
+ result := stripParenArgsUnlessAnonymous(test.input)
+ if result != test.expected {
+ t.Errorf("stripParenArgsUnlessAnonymous(%q) = %q; want %q", test.input, result, test.expected)
+ }
+ }
+}