Skip to content

feat(profiler): inject PID/TGID/cgroup/pgroup dimensions as pprof labels (#329 PR 1/4)#396

Open
123123213weqw wants to merge 1 commit into
ccfos:mainfrom
123123213weqw:feat/profiler-dimensions-labels-329
Open

feat(profiler): inject PID/TGID/cgroup/pgroup dimensions as pprof labels (#329 PR 1/4)#396
123123213weqw wants to merge 1 commit into
ccfos:mainfrom
123123213weqw:feat/profiler-dimensions-labels-329

Conversation

@123123213weqw

Copy link
Copy Markdown
Contributor

What

First PR toward issue #329 (multi-dimension profiling capture). Adds opt-in dimension capture + label injection in the Go profiler pipeline. The three lock-contention profilers (mutex/spinlock/rwlock) will follow as separate PRs once the label format is signed off here.

Why

Closes one third of issue #329's acceptance criteria ("PID/TGID/cgroup/进程组维度采集可用" + "多维度标签正确注入"). Today profiler output has no per-sample dimension context, so aggregating or filtering profiles by cgroup/process-group downstream isn't possible. This PR adds the Go-side capture + injection; the three lock profilers will feed off the same machinery.

How

Opt-in by default, byte-identical when off. A new ParseOption.Dimensions struct selects which labels to emit; zero value = no injection, so existing output is unchanged.

type Dimensions struct {
    PID          bool
    TGID         bool
    Cgroup       bool
    ProcessGroup bool
}

Carriers (all omitempty, so legacy JSON/data round-trips clean):

  • SampleOutput.TGID / .CgroupPath / .ProcessGroup — multi-PID path (ParseCollapsedData / ParseRawData with a JSON sample array)
  • ParseInput.SampleDimensions — single-PID path

Injection happens at the existing first-frame construction point (profiler.go:354 in parseCommonData, and the headerFrame block in parseMultiProcessData). The new helper renders the requested dimensions in canonical pprof form and appends them to the existing threadName#pid header via ;:

javaApp#4242;pid=4242;tgid=456;cgroup=/kubepods/burstable/pod-abc;pgroup=web-tier

Putting labels in the first frame means every output backend (collapsed/flamegraph/raw/chrometrace/speedscope — all under internal/profiler/output/) picks them up without per-backend branching, and Pyroscope/Parca can still parse the key=value pairs as labels within that frame.

Why pprof canonical form (key=value) over Brendan Gregg collapsed form (key_value)? It's what Pyroscope and Parca natively ingest. If maintainers prefer form B from the design comment, it's a one-line change in appendDimension.

API contract

Surface Change Back-compat
ParseOption adds Dimensions Dimensions (zero value off) ✅ existing callers unchanged
SampleOutput JSON adds tgid,cgroup_path,process_group (all omitempty) ✅ legacy JSON unmarshals clean
ParseInput adds SampleDimensions SampleDimensions (zero value off) ✅ existing callers unchanged
parseCommonData signature adds sampleDims SampleDimensions param internal only, 2 callers updated

Testing

New file internal/profiler/profiler_test.go (first test file for this package):

  • TestDimensions_Enabled — gate function: zero value disabled, every flag combo enabled.
  • TestBuildDimensionLabels (6 cases) — pprof form for every dimension combo, including PID=0 skip, separator-in-cgroup-path, all-requested-but-unset skip.
  • TestParseCommonData_DimensionInjection_Off — back-compat: zero Dimensions → no label, output unchanged.
  • TestParseCommonData_DimensionInjection_On — opt-in path: labels appended to header.
  • TestParseCommonData_DimensionInjection_NilOptnil ParseOption must not panic (matches existing nil-opt tolerance in opt.SampleRate).
  • TestParseRawData_MultiPID_DimensionInjection — multi-PID path: each sample's own dimensions land in its own header.
  • TestSampleOutput_JSONRoundTrip (3 cases) — legacy JSON unmarshals clean; new fields round-trip; zero values omitempty.
  • TestParseRawData_LegacyInput_Unchanged — contract guard: even when the sample carries dimension values, opt-out produces no labels.

All passing under go test ./internal/profiler/ -count=1 on Linux 6.8; go vet ./internal/profiler/ clean; go build ./cmd/... clean.

Verification note

BPF isn't touched in this PR — it's pure Go and fully testable on the server. The three lock-contention profilers (mutex/spinlock/rwlock) come in follow-up PRs and will reuse this label-injection machinery.

Checklist

  • Zero-value Dimensions = byte-identical output (back-compat contract)
  • nil ParseOption tolerated
  • JSON shape backward compatible (omitempty on all new fields)
  • Labels appear in first frame → works for all output backends without per-backend changes
  • Pure-Go PR, no BPF touched
  • Tests added and passing; go vet clean

Refs #329.

First PR toward issue ccfos#329 (multi-dimension profiling capture). Pure-Go
dimension capture and label injection; the three lock-contention profilers
(mutex/spinlock/rwlock) ship as separate PRs once the label format is
signed off by maintainers.

Adds:
- ParseOption.Dimensions: opt-in flags selecting which dimensions to emit
  (PID/TGID/Cgroup/ProcessGroup). Zero value = no injection, so existing
  profiler output is byte-for-byte unchanged.
- SampleOutput.TGID/CgroupPath/ProcessGroup (multi-PID path) and
  ParseInput.SampleDimensions (single-PID path): carriers for the values
  used when injection is requested. JSON omitempty so legacy data round-trips.
- buildDimensionLabels: renders the requested dimensions in canonical pprof
  form (e.g. "pid=123;tgid=456;cgroup=/kubepods/...;pgroup=web"), appended
  to the existing first-frame header ("threadName#pid;...") via ';'.

Labels live in the first frame so every output backend (collapsed/flamegraph/
raw/chrometrace/speedscope) picks them up without per-backend branching, and
Pyroscope/Parca can parse the key=value pairs as labels within that frame.

Refs ccfos#329.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant