Skip to content

Commit

Permalink
more changes
Browse files Browse the repository at this point in the history
Signed-off-by: Francisco Javier Honduvilla Coto <javierhonduco@gmail.com>
  • Loading branch information
javierhonduco committed May 10, 2023
1 parent 0454f89 commit 66c017c
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 29 deletions.
12 changes: 12 additions & 0 deletions pkg/process/noop.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
// Copyright 2022-2023 The Parca Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package process

import (
Expand Down
210 changes: 183 additions & 27 deletions pkg/profiler/cpu/cpu_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,13 @@ import (
"context"
"fmt"
"os/exec"
"strconv"
"strings"
"syscall"
"testing"
"time"
"unsafe"

//bpf "github.com/aquasecurity/libbpfgo"
//bpf "github.com/aquasecurity/libbpfgo"
bpf "github.com/aquasecurity/libbpfgo"
"github.com/go-kit/log"
"github.com/google/pprof/profile"
Expand Down Expand Up @@ -69,10 +68,111 @@ func (tpw *TestProfileWriter) Write(_ context.Context, labels model.LabelSet, pr
return nil
}

func (tpw *TestProfileWriter) SampleForProcess(pid int) *Sample {
type LocalSymbolizer struct {
addr2line string
timeout time.Duration
}

func NewLocalSymbolizer() *LocalSymbolizer {
return &LocalSymbolizer{addr2line: "/usr/bin/addr2line", timeout: time.Second * 5}
}

func (ls *LocalSymbolizer) Symbolize(executable string, address uint64) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), ls.timeout)
defer cancel()

//nolint:gosec
addr2lineCmd := exec.CommandContext(ctx, ls.addr2line, "--functions", "-e", executable, fmt.Sprintf("%x", address))
addr2lineCmd.Wait()
out, err := addr2lineCmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("CombinedOutput: %s, %w", out, err)
}
return strings.TrimSpace(strings.Split(string(out), "\n")[0]), nil
}

type LocalDemangler struct {
filtProg string
timeout time.Duration
}

func NewLocalDemangler() *LocalDemangler {
return &LocalDemangler{filtProg: "/usr/bin/c++filt", timeout: time.Second * 5}
}

func (ld *LocalDemangler) Demangle(name string) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), ld.timeout)
defer cancel()

//nolint:gosec
filtCmd := exec.CommandContext(ctx, ld.filtProg, name)
filtCmd.Wait()
out, err := filtCmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("CombinedOutput: %s %w", out, err)
}
return strings.TrimSpace(string(out)), nil
}

func symbolizeProfile(t *testing.T, profile *profile.Profile, demangle bool) [][]string {
t.Helper()

symbolizer := NewLocalSymbolizer()
demangler := NewLocalDemangler()

aggregatedStacks := make([][]string, 0)
for _, stack := range profile.Sample {
aggregatedStack := make([]string, 0, len(stack.Location))
for _, frame := range stack.Location {
address := frame.Address
file := frame.Mapping.File

if file == "<unknown path>" {
continue
}

funcName, err := symbolizer.Symbolize(file, address)
require.Nil(t, err)

if demangle {
funcName, err = demangler.Demangle(funcName)
require.Nil(t, err)
}

aggregatedStack = append(aggregatedStack, funcName)
}
aggregatedStacks = append(aggregatedStacks, aggregatedStack)
}

return aggregatedStacks
}

func anyStackEqual(aggregatedStacks [][]string, stack []string) bool {
for _, aggregatedStack := range aggregatedStacks {
if len(aggregatedStack) == len(stack) {
equal := true
for i := range aggregatedStack {
if aggregatedStack[i] != stack[i] {
equal = false
break
}
}
if equal {
return true
}
}
}
return false
}

func (tpw *TestProfileWriter) SampleForProcess(pid int) *Sample {
for _, sample := range tpw.samples {
if string(sample.labels["pid"]) == fmt.Sprint(pid) { // awful
foundPid, err := strconv.Atoi(string(sample.labels["pid"]))
if err != nil {
panic("label pid is not a valid integer")
}

if foundPid == pid {
return &sample
}
}
Expand All @@ -81,14 +181,16 @@ func (tpw *TestProfileWriter) SampleForProcess(pid int) *Sample {
}

func prepareProfiler(t *testing.T, profileWriter profiler.ProfileWriter, logger log.Logger, tempDir string) (*CPU, *objectfile.Pool) {
t.Helper()

loopDuration := 1 * time.Second
disableJit := true
frequency := uint64(27)
reg := prometheus.NewRegistry()
pfs, err := procfs.NewDefaultFS()
require.Nil(t, err)
bpfProgramLoaded := make(chan bool, 1)
debugNormalizeAddresses := false
normalizeAddresses := true
memlockRlimit := uint64(4000000)

curr, _, err := profiler.Files()
Expand All @@ -113,7 +215,7 @@ func prepareProfiler(t *testing.T, profileWriter profiler.ProfileWriter, logger
loopDuration,
)

return NewCPUProfiler(
profiler := NewCPUProfiler(
logger,
reg,
process.NewInfoManager(
Expand All @@ -124,7 +226,7 @@ func prepareProfiler(t *testing.T, profileWriter profiler.ProfileWriter, logger
labelsManager,
loopDuration,
),
address.NewNormalizer(logger, reg, debugNormalizeAddresses),
address.NewNormalizer(logger, reg, normalizeAddresses),
symbol.NewSymbolizer(
log.With(logger, "component", "symbolizer"),
perf.NewCache(logger),
Expand All @@ -140,8 +242,19 @@ func prepareProfiler(t *testing.T, profileWriter profiler.ProfileWriter, logger
false,
true,
bpfProgramLoaded,
), ofp
)

// Wait for the BPF program to be loaded.
for len(bpfProgramLoaded) > 0 {
<-bpfProgramLoaded
}

return profiler, ofp
}

// TestCPUProfilerWorks is the integration test for the CPU profiler. It
// uses an in-memory profile writer to be verify that the data we produce
// is correct.
func TestCPUProfilerWorks(t *testing.T) {
profileWriter := NewTestProfileWriter()
profileDuration := 3 * time.Second
Expand All @@ -154,32 +267,75 @@ func TestCPUProfilerWorks(t *testing.T) {
profiler, ofp := prepareProfiler(t, profileWriter, logger, tempDir)
defer ofp.Close()

// Test that we get samples from the DWARF-based unwinder.
dateCmd := exec.CommandContext(ctx, "../../../testdata/out/basic-cpp-plt")
err := dateCmd.Start()
// Test unwinding without frame pointers.
noFramePointersCmd := exec.Command("../../../testdata/out/basic-cpp-no-fp-with-debuginfo")
err := noFramePointersCmd.Start()
require.Nil(t, err)
defer noFramePointersCmd.Process.Kill()
dwarfUnwoundPid := noFramePointersCmd.Process.Pid

// Test unwinding with frame pointers.
framePointersCmd := exec.Command("../../../testdata/out/basic-go", "10000")
err = framePointersCmd.Start()
require.Nil(t, err)
dwarfUnwoundPid := dateCmd.Process.Pid
defer framePointersCmd.Process.Kill()
fpUnwoundPid := framePointersCmd.Process.Pid

err = profiler.Run(ctx)
require.Equal(t, err, context.DeadlineExceeded)

require.True(t, len(profileWriter.samples) > 0)

sample := profileWriter.SampleForProcess(dwarfUnwoundPid)
require.NotNil(t, sample)

// Test expected metadata.
require.Equal(t, string(sample.labels["comm"]), "basic-cpp-plt")
require.True(t, strings.Contains(string(sample.labels["executable"]), "basic-cpp-plt"))
require.True(t, strings.HasPrefix(string(sample.labels["compiler"]), "GCC"))
require.NotEqual(t, string(sample.labels["kernel_release"]), "")
require.NotEqual(t, string(sample.labels["cgroup_name"]), "")

// Test basic profiler structure.
require.True(t, sample.profile.DurationNanos < profileDuration.Nanoseconds())
require.True(t, len(sample.profile.Sample) > 0)
require.True(t, len(sample.profile.Location) > 0)
require.True(t, len(sample.profile.Mapping) > 0)
{
sample := profileWriter.SampleForProcess(dwarfUnwoundPid)
require.NotNil(t, sample)

// Test basic profile structure.
require.True(t, sample.profile.DurationNanos < profileDuration.Nanoseconds())
require.Equal(t, sample.profile.SampleType[0].Type, "samples")
require.Equal(t, sample.profile.SampleType[0].Unit, "count")

require.True(t, len(sample.profile.Sample) > 0)
require.True(t, len(sample.profile.Location) > 0)
require.True(t, len(sample.profile.Mapping) > 0)

// Test expected metadata.
require.Equal(t, string(sample.labels["comm"]), "basic-cpp-no-fp-with-debuginfo"[:15]) // comm is limited to 16 characters including NUL.
require.True(t, strings.Contains(string(sample.labels["executable"]), "basic-cpp-no-fp-with-debuginfo"))
require.True(t, strings.HasPrefix(string(sample.labels["compiler"]), "GCC"))
require.NotEmpty(t, string(sample.labels["kernel_release"]))
require.NotEmpty(t, string(sample.labels["cgroup_name"]))

// Test symbolized stacks.
aggregatedStacks := symbolizeProfile(t, sample.profile, true)
require.True(t, anyStackEqual(aggregatedStacks, []string{"top2()", "c2()", "b2()", "a2()", "main", "__libc_start_call_main", "__libc_start_main_alias_2"}))
require.True(t, anyStackEqual(aggregatedStacks, []string{"top1()", "c1()", "b1()", "a1()", "main", "__libc_start_call_main", "__libc_start_main_alias_2"}))
}

{
sample := profileWriter.SampleForProcess(fpUnwoundPid)
require.NotNil(t, sample)

// Test basic profile structure.
require.True(t, sample.profile.DurationNanos < profileDuration.Nanoseconds())
require.Equal(t, sample.profile.SampleType[0].Type, "samples")
require.Equal(t, sample.profile.SampleType[0].Unit, "count")

require.True(t, len(sample.profile.Sample) > 0)
require.True(t, len(sample.profile.Location) > 0)
require.True(t, len(sample.profile.Mapping) > 0)

// Test expected metadata.
require.Equal(t, string(sample.labels["comm"]), "basic-go")
require.True(t, strings.Contains(string(sample.labels["executable"]), "basic-go"))
require.True(t, strings.HasPrefix(string(sample.labels["compiler"]), "Go"))
require.NotEmpty(t, string(sample.labels["kernel_release"]))
require.NotEmpty(t, string(sample.labels["cgroup_name"]))

// Test symbolized stacks.
aggregatedStacks := symbolizeProfile(t, sample.profile, false)
require.True(t, anyStackEqual(aggregatedStacks, []string{"time.Now", "main.main", "runtime.main", "runtime.goexit.abi0"}))
}
}

// The intent of these tests is to ensure that libbpfgo behaves the
Expand Down
2 changes: 1 addition & 1 deletion scripts/check-license.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ set -o pipefail
set -u

licRes=$(
find . -type f -iname '*.go' ! -path '*/vendor/*' ! -path '*/internal/go/*' ! -path '*/internal/pprof/*' ! -path '*/internal/dwarf/*' -exec \
find . -type f -iname '*.go' ! -path '*/vendor/*' ! -path '*/internal/go/*' ! -path '*/internal/pprof/*' ! -path '*/internal/dwarf/*' ! -path '*/testdata/*' -exec \
sh -c 'head -n3 $1 | grep -Eq "(Copyright|generated|GENERATED)" || echo -e $1' {} {} \;
)

Expand Down
2 changes: 1 addition & 1 deletion testdata

0 comments on commit 66c017c

Please sign in to comment.