Skip to content

Add --pids and --max-depth to flame command and use Go implementation of stackcollapse-perf to eliminate Perl dependency #332

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

Merged
merged 31 commits into from
May 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bddbd30
Use Go implementation of stackcollapse-perf to eliminate Perl depende…
harp-intel May 6, 2025
e6cb128
Add go.mod file for stackcollapse-perf module
harp-intel May 6, 2025
64bd60f
spelling
harp-intel May 6, 2025
9c0bff0
Remove stackcollapse-perf reset command from Makefile
harp-intel May 6, 2025
dbe2716
use go stackcollapse
harp-intel May 6, 2025
22da023
Use Go implementation of stackcollapse-perf to eliminate Perl depende…
harp-intel May 6, 2025
b29f02d
Add go.mod file for stackcollapse-perf module
harp-intel May 6, 2025
14317ba
spelling
harp-intel May 6, 2025
94b6662
Remove stackcollapse-perf reset command from Makefile
harp-intel May 6, 2025
4145397
use go stackcollapse
harp-intel May 6, 2025
61aa706
resolve conflict
harp-intel May 7, 2025
7c72161
fix out of range panic
harp-intel May 7, 2025
d41eb46
add maximum depth of flame graph rendering
harp-intel May 7, 2025
775255c
refactor and add unit tests
harp-intel May 8, 2025
ff19d06
Use Go implementation of stackcollapse-perf to eliminate Perl depende…
harp-intel May 6, 2025
811d8a6
Add go.mod file for stackcollapse-perf module
harp-intel May 6, 2025
556d58a
spelling
harp-intel May 6, 2025
d788587
Remove stackcollapse-perf reset command from Makefile
harp-intel May 6, 2025
c6c2797
use go stackcollapse
harp-intel May 6, 2025
d21319c
Use Go implementation of stackcollapse-perf to eliminate Perl depende…
harp-intel May 6, 2025
cfdb78c
Add go.mod file for stackcollapse-perf module
harp-intel May 6, 2025
cf72f78
Remove stackcollapse-perf reset command from Makefile
harp-intel May 6, 2025
bbf2669
fix out of range panic
harp-intel May 7, 2025
e4d4674
add maximum depth of flame graph rendering
harp-intel May 7, 2025
e3454b2
refactor and add unit tests
harp-intel May 8, 2025
08d4a5c
Merge branch 'stackcollapse' of github.com:intel/PerfSpect into stack…
harp-intel May 8, 2025
139f652
support multiple pids
harp-intel May 9, 2025
96adc0e
set cwd to temp dir in script
harp-intel May 9, 2025
f1db0a9
Merge branch 'main' into stackcollapse
harp-intel May 9, 2025
52c4a8f
more refactoring
harp-intel May 11, 2025
9203d3f
replace log with slog
harp-intel May 12, 2025
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
22 changes: 21 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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..."
Expand All @@ -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:
Expand Down
23 changes: 0 additions & 23 deletions THIRD_PARTY_PROGRAMS
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
34 changes: 25 additions & 9 deletions cmd/flame/flame.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"
"perfspect/internal/common"
"perfspect/internal/report"
"perfspect/internal/util"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -40,24 +41,27 @@ 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() {
Cmd.Flags().StringVar(&common.FlagInput, common.FlagInputName, "", "")
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)

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
}
Expand All @@ -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,
}
Expand Down
6 changes: 3 additions & 3 deletions internal/report/render_html.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 := `<style>

/* Custom page header */
Expand All @@ -1308,8 +1308,8 @@ func codePathFrequencyTableHTMLRenderer(tableValues TableValues, targetName stri
}
</style>
`
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
}

Expand Down
56 changes: 32 additions & 24 deletions internal/report/render_html_flamegraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ import (
"bytes"
"encoding/json"
"fmt"
"log"
"log/slog"
"math/rand/v2" // nosemgrep
"slices"
"strconv"
"strings"
texttemplate "text/template" // nosemgrep
Expand Down Expand Up @@ -81,11 +82,6 @@ type flameGraphTemplateStruct struct {
// Folded data conversion adapted from https://github.com/spiermar/burn
// Copyright © 2017 Martin Spier <spiermar@gmail.com>
// 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
Expand Down Expand Up @@ -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
}
Expand All @@ -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 == "" {
Expand All @@ -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))
Expand All @@ -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()
Expand Down
22 changes: 11 additions & 11 deletions internal/report/table_defs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
//
Expand Down Expand Up @@ -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
}
Expand Down
Loading