Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
19 changes: 13 additions & 6 deletions cmd/flamegraph/flamegraph.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ var (
flagNoSystemSummary bool
flagMaxDepth int
flagPerfEvent string
flagAsprofArguments string
)

const (
Expand All @@ -61,6 +62,7 @@ const (
flagNoSystemSummaryName = "no-summary"
flagMaxDepthName = "max-depth"
flagPerfEventName = "perf-event"
flagAsprofArgumentsName = "asprof-args"
)

func init() {
Expand All @@ -72,7 +74,7 @@ func init() {
Cmd.Flags().BoolVar(&flagNoSystemSummary, flagNoSystemSummaryName, false, "")
Cmd.Flags().IntVar(&flagMaxDepth, flagMaxDepthName, 0, "")
Cmd.Flags().StringVar(&flagPerfEvent, flagPerfEventName, "cycles:P", "")

Cmd.Flags().StringVar(&flagAsprofArguments, flagAsprofArgumentsName, "-t -F probesp+vtable", "")
workflow.AddTargetFlags(Cmd)

Cmd.SetUsageFunc(usageFunc)
Expand Down Expand Up @@ -122,6 +124,10 @@ func getFlagGroups() []app.FlagGroup {
Name: flagPerfEventName,
Help: "perf event to use for native sampling (e.g., cpu-cycles, instructions, cache-misses, branches, context-switches, mem-loads, mem-stores, etc.)",
},
{
Name: flagAsprofArgumentsName,
Help: "arguments to pass to async-profiler, e.g., $ asprof start <these arguments> -i <interval> <pid>.",
},
{
Name: flagMaxDepthName,
Help: "maximum render depth of call stack in flamegraph (0 = no limit)",
Expand Down Expand Up @@ -198,11 +204,12 @@ func runCmd(cmd *cobra.Command, args []string) error {
Cmd: cmd,
ReportNamePost: "flame",
ScriptParams: map[string]string{
"Frequency": strconv.Itoa(flagFrequency),
"Duration": strconv.Itoa(flagDuration),
"PIDs": strings.Join(util.IntSliceToStringSlice(flagPids), ","),
"MaxDepth": strconv.Itoa(flagMaxDepth),
"PerfEvent": flagPerfEvent,
"Frequency": strconv.Itoa(flagFrequency),
"Duration": strconv.Itoa(flagDuration),
"PIDs": strings.Join(util.IntSliceToStringSlice(flagPids), ","),
"MaxDepth": strconv.Itoa(flagMaxDepth),
"PerfEvent": flagPerfEvent,
"AsprofArguments": flagAsprofArguments,
},
Tables: tables,
Input: flagInput,
Expand Down
27 changes: 25 additions & 2 deletions cmd/flamegraph/flamegraph_renderers.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ func renderFlameGraph(header string, tableValues table.TableValues, field string
return
}
maxDepth := tableValues.Fields[maxDepthFieldIndex].Values[0]
if maxDepth == "" {
slog.Error("maximum render depth field is empty")
return
}
maxStackDepth, err := strconv.Atoi(maxDepth)
if err != nil {
slog.Error("failed to convert maximum stack depth", slog.String("error", err.Error()))
Expand Down Expand Up @@ -229,8 +233,27 @@ func callStackFrequencyTableHTMLRenderer(tableValues table.TableValues, targetNa
slog.Error("didn't find expected field (Perf Event) in table", slog.String("error", err.Error()))
return out
}
if len(tableValues.Fields[perfEventFieldIndex].Values) == 0 {
slog.Error("no values for perf event field in table")
return out
}
perfEvent := tableValues.Fields[perfEventFieldIndex].Values[0]
out += renderFlameGraph(fmt.Sprintf("Native (%s)", perfEvent), tableValues, "Native Stacks")
out += renderFlameGraph("Java (async-profiler)", tableValues, "Java Stacks")
out += renderFlameGraph(fmt.Sprintf("Native (perf record -e %s)", perfEvent), tableValues, "Native Stacks")

// get the asprof arguments from the table values
asprofArgumentsFieldIndex, err := table.GetFieldIndex("Asprof Arguments", tableValues)
if err != nil {
slog.Error("didn't find expected field (Asprof Arguments) in table", slog.String("error", err.Error()))
return out
}
if len(tableValues.Fields[asprofArgumentsFieldIndex].Values) == 0 {
slog.Error("no values for asprof arguments field in table")
return out
}
asprofArguments := tableValues.Fields[asprofArgumentsFieldIndex].Values[0]
if asprofArguments != "" {
asprofArguments = " " + asprofArguments
}
out += renderFlameGraph(fmt.Sprintf("Java (asprof start%s)", asprofArguments), tableValues, "Java Stacks")
return out
}
20 changes: 19 additions & 1 deletion cmd/flamegraph/flamegraph_tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func flameGraphTableValues(outputs map[string]script.ScriptOutput) []table.Field
{Name: "Java Stacks", Values: []string{javaFoldedFromOutput(outputs)}},
{Name: "Maximum Render Depth", Values: []string{maxRenderDepthFromOutput(outputs)}},
{Name: "Perf Event", Values: []string{perfEventFromOutput(outputs)}},
{Name: "Asprof Arguments", Values: []string{asprofArgumentsFromOutput(outputs)}},
}
return fields
}
Expand Down Expand Up @@ -104,7 +105,6 @@ func nativeFoldedFromOutput(outputs map[string]script.ScriptOutput) string {
}
}
if dwarfFolded == "" && fpFolded == "" {
slog.Warn("no native folded stacks found")
// "event syntax error: 'foo'" indicates that the perf event specified is invalid/unsupported
if strings.Contains(outputs[script.FlameGraphScriptName].Stderr, "event syntax error") {
slog.Error("unsupported perf event specified", slog.String("error", outputs[script.FlameGraphScriptName].Stderr))
Expand Down Expand Up @@ -154,6 +154,24 @@ func perfEventFromOutput(outputs map[string]script.ScriptOutput) string {
return ""
}

func asprofArgumentsFromOutput(outputs map[string]script.ScriptOutput) string {
if outputs[script.FlameGraphScriptName].Stdout == "" {
slog.Warn("collapsed call stack output is empty")
return ""
}
sections := extract.GetSectionsFromOutput(outputs[script.FlameGraphScriptName].Stdout)
if len(sections) == 0 {
slog.Warn("no sections in collapsed call stack output")
return ""
}
for header, content := range sections {
if header == "asprof_arguments" {
return strings.TrimSpace(content)
}
}
return ""
}

// ProcessStacks ...
// [processName][callStack]=count
type ProcessStacks map[string]Stacks
Expand Down
20 changes: 14 additions & 6 deletions internal/script/script.go
Original file line number Diff line number Diff line change
Expand Up @@ -319,21 +319,29 @@ kill_script() {
local pid="${pids[$s]:-}"
[[ -z "$pid" ]] && return 0
if ! ps -p "$pid" > /dev/null 2>&1; then return 0; fi
# Send signal to the process group (negative PID)
# Signal the process group (negative PID)
echo "Sending SIGINT to script '${orig_names[$s]}' with PID $pid" >&2
kill -SIGINT -"$pid" 2>/dev/null || true
# Give it time to clean up so child script can finalize
# Wait up to 5 seconds in 0.5s intervals
# Wait up to 1 minute in 1s intervals
local waited=0
while ps -p "$pid" > /dev/null 2>&1 && [ "$waited" -lt 10 ]; do
sleep 0.5
echo "Waiting for script '${orig_names[$s]}' with PID $pid to exit gracefully" >&2
while ps -p "$pid" > /dev/null 2>&1 && [ "$waited" -lt 60 ]; do
echo -n "." >&2
sleep 1
waited=$((waited + 1))
done
echo "Done waiting for script '${orig_names[$s]}' with PID $pid to exit gracefully" >&2
# Force kill the process group if still alive
if ps -p "$pid" > /dev/null 2>&1; then
echo "Force killing script '${orig_names[$s]}' with PID $pid" >&2
kill -SIGKILL -"$pid" 2>/dev/null || true
fi
wait "$pid" 2>/dev/null || true
if [[ -z "${exitcodes[$s]:-}" ]]; then exitcodes[$s]=130; fi
echo "Script '${orig_names[$s]}' with PID $pid has been killed" >&2
if [[ -z "${exitcodes[$s]:-}" ]]; then
echo "Setting exit code for script '${orig_names[$s]}' to 130 (terminated by SIGINT)" >&2
exitcodes[$s]=130
fi
}

wait_for_concurrent_scripts() {
Expand Down
Loading