Skip to content
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

metadata: Cache and control compiler metadata fetches #2287

Merged
merged 3 commits into from
Nov 24, 2023
Merged
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: 11 additions & 8 deletions cmd/parca-agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import (
"net/url"
"os"
"os/exec"
"runtime"
goruntime "runtime"
runtimepprof "runtime/pprof"
"strconv"
"strings"
Expand Down Expand Up @@ -85,6 +85,7 @@ import (
"github.com/parca-dev/parca-agent/pkg/profiler"
"github.com/parca-dev/parca-agent/pkg/profiler/cpu"
"github.com/parca-dev/parca-agent/pkg/rlimit"
"github.com/parca-dev/parca-agent/pkg/runtime"
"github.com/parca-dev/parca-agent/pkg/template"
"github.com/parca-dev/parca-agent/pkg/tracer"
"github.com/parca-dev/parca-agent/pkg/vdso"
Expand Down Expand Up @@ -310,7 +311,7 @@ func getTelemetryMetadata() map[string]string {

r["git_commit"] = commit
r["agent_version"] = version
r["go_arch"] = runtime.GOARCH
r["go_arch"] = goruntime.GOARCH
r["kernel_release"] = si.Kernel.Release
r["cpu_cores"] = strconv.Itoa(cpuinfo.NumCPU())

Expand Down Expand Up @@ -360,7 +361,7 @@ func main() {

// Run garbage collector to minimize the amount of memory that the parent
// telemetry process uses.
runtime.GC()
goruntime.GC()
err := cmd.Run()
if err != nil {
level.Error(logger).Log("msg", "======================= unexpected error =======================")
Expand Down Expand Up @@ -470,7 +471,7 @@ func main() {
intro.Print()

// TODO(sylfrena): Entirely remove once full support for DWARF Unwinding Arm64 is added and production tested for a few days
if runtime.GOARCH == "arm64" {
if goruntime.GOARCH == "arm64" {
flags.DWARFUnwinding.Disable = false
level.Info(logger).Log("msg", "ARM64 support is currently in beta. DWARF-based unwinding is not fully supported yet, see https://github.com/parca-dev/parca-agent/discussions/1376 for more details")
}
Expand Down Expand Up @@ -514,8 +515,8 @@ func main() {
}

// Set profiling rates.
runtime.SetBlockProfileRate(flags.BlockProfileRate)
runtime.SetMutexProfileFraction(flags.MutexProfileFraction)
goruntime.SetBlockProfileRate(flags.BlockProfileRate)
goruntime.SetMutexProfileFraction(flags.MutexProfileFraction)

if err := run(logger, reg, flags); err != nil {
level.Error(logger).Log("err", err)
Expand Down Expand Up @@ -695,7 +696,7 @@ func run(logger log.Logger, reg *prometheus.Registry, flags flags) error {
a := analytics.NewSender(
logger,
c,
runtime.GOARCH,
goruntime.GOARCH,
cpuinfo.NumCPU(),
version,
si,
Expand Down Expand Up @@ -840,12 +841,13 @@ func run(logger log.Logger, reg *prometheus.Registry, flags flags) error {
defer ofp.Close() // Will make sure all the files are closed.

nsCache := namespace.NewCache(logger, reg, flags.Profiling.Duration)
compilerInfoManager := runtime.NewCompilerInfoManager(reg, ofp)

// All the metadata providers work best-effort.
providers := []metadata.Provider{
discoveryMetadata,
metadata.Target(flags.Node, flags.Metadata.ExternalLabels),
metadata.Compiler(logger, reg, ofp),
metadata.Compiler(logger, reg, pfs, compilerInfoManager),
metadata.Runtime(reg, pfs),
metadata.Java(logger, nsCache),
metadata.Process(pfs),
Expand Down Expand Up @@ -930,6 +932,7 @@ func run(logger log.Logger, reg *prometheus.Registry, flags flags) error {
log.With(logger, "component", "cpu_profiler"),
reg,
processInfoManager,
compilerInfoManager,
converter.NewManager(
log.With(logger, "component", "converter_manager"),
reg,
Expand Down
37 changes: 15 additions & 22 deletions pkg/metadata/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,15 @@ package metadata
import (
"context"
"fmt"
"os"
"path/filepath"
"strconv"

"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/common/model"
"github.com/xyproto/ainur"
"github.com/prometheus/procfs"

"github.com/parca-dev/parca-agent/pkg/cache"
"github.com/parca-dev/parca-agent/pkg/objectfile"
"github.com/parca-dev/parca-agent/pkg/runtime"
)

type compilerProvider struct {
Expand All @@ -40,41 +38,36 @@ func (p *compilerProvider) ShouldCache() bool {
}

// Compiler provides metadata for determined compiler.
func Compiler(logger log.Logger, reg prometheus.Registerer, objFilePool *objectfile.Pool) Provider {
func Compiler(logger log.Logger, reg prometheus.Registerer, procfs procfs.FS, cic *runtime.CompilerInfoManager) Provider {
cache := cache.NewLRUCache[string, model.LabelSet](
prometheus.WrapRegistererWith(prometheus.Labels{"cache": "metadata_compiler"}, reg),
512,
)
return &compilerProvider{
StatelessProvider{"compiler", func(ctx context.Context, pid int) (model.LabelSet, error) {
// do not use filepath.EvalSymlinks
// it will return error if exe not existed in / directory
// but in /proc/pid/root directory
path, err := os.Readlink(fmt.Sprintf("/proc/%d/exe", pid))
p, err := procfs.Proc(pid)
if err != nil {
return nil, fmt.Errorf("failed to get path for process %d: %w", pid, err)
return nil, fmt.Errorf("failed to instantiate procfs for PID %d: %w", pid, err)
}

path = filepath.Join(fmt.Sprintf("/proc/%d/root", pid), path)
path, err := p.Executable()
if err != nil {
return nil, fmt.Errorf("failed to get executable path for PID %d: %w", pid, err)
}
if cachedLabels, ok := cache.Get(path); ok {
return cachedLabels, nil
}

obj, err := objFilePool.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open ELF file for process %d: %w", pid, err)
}

ef, err := obj.ELF()
compiler, err := cic.Fetch(path) // nolint:contextcheck
if err != nil {
return nil, fmt.Errorf("failed to get ELF file for process %d: %w", pid, err)
return nil, fmt.Errorf("failed to get compiler info for %s: %w", path, err)
}

labels := model.LabelSet{
"compiler": model.LabelValue(ainur.Compiler(ef)),
"stripped": model.LabelValue(strconv.FormatBool(ainur.Stripped(ef))),
"static": model.LabelValue(strconv.FormatBool(ainur.Static(ef))),
"buildid": model.LabelValue(obj.BuildID),
"compiler": model.LabelValue(compiler.Type),
"stripped": model.LabelValue(strconv.FormatBool(compiler.Stripped)),
"static": model.LabelValue(strconv.FormatBool(compiler.Static)),
"buildid": model.LabelValue(compiler.BuildID),
}
cache.Add(path, labels)
return labels, nil
Expand Down
10 changes: 6 additions & 4 deletions pkg/profiler/cpu/cpu.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import (
"fmt"
"os"
"regexp"
"runtime"
goruntime "runtime"
"strings"
"sync"
"syscall"
Expand All @@ -52,6 +52,7 @@ import (
bpfmetrics "github.com/parca-dev/parca-agent/pkg/profiler/cpu/bpf/metrics"
bpfprograms "github.com/parca-dev/parca-agent/pkg/profiler/cpu/bpf/programs"
"github.com/parca-dev/parca-agent/pkg/rlimit"
"github.com/parca-dev/parca-agent/pkg/runtime"
"github.com/parca-dev/parca-agent/pkg/stack/unwind"
)

Expand Down Expand Up @@ -126,6 +127,7 @@ func NewCPUProfiler(
logger log.Logger,
reg prometheus.Registerer,
processInfoManager profiler.ProcessInfoManager,
compilerInfoManager *runtime.CompilerInfoManager,
profileConverter *pprof.Manager,
profileWriter profiler.ProfileStore,
config *Config,
Expand All @@ -143,7 +145,7 @@ func NewCPUProfiler(
profileStore: profileWriter,

// CPU profiler specific caches.
framePointerCache: unwind.NewHasFramePointersCache(logger, reg),
framePointerCache: unwind.NewHasFramePointersCache(logger, reg, compilerInfoManager),

byteOrder: byteorder.GetHostByteOrder(),

Expand Down Expand Up @@ -509,7 +511,7 @@ func (p *CPU) onDemandUnwindInfoBatcher(ctx context.Context, requestUnwindInfoCh

func (p *CPU) addUnwindTableForProcess(ctx context.Context, pid int) {
executable := fmt.Sprintf("/proc/%d/exe", pid)
hasFramePointers, err := p.framePointerCache.HasFramePointers(executable)
hasFramePointers, err := p.framePointerCache.HasFramePointers(executable) // nolint:contextcheck
if err != nil {
// It might not exist as reading procfs is racy. If the executable has no symbols
// that we use as a heuristic to detect whether it has frame pointers or not,
Expand Down Expand Up @@ -1128,7 +1130,7 @@ func preprocessRawData(rawData map[profileKey]map[bpfprograms.CombinedStack]uint
}

func getArch() elf.Machine {
switch runtime.GOARCH {
switch goruntime.GOARCH {
case "arm64":
return elf.EM_AARCH64
case "amd64":
Expand Down
123 changes: 123 additions & 0 deletions pkg/runtime/compiler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// 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 runtime

import (
"context"
"fmt"
"runtime"
"strings"
"time"

"github.com/Masterminds/semver/v3"
"github.com/prometheus/client_golang/prometheus"
"github.com/xyproto/ainur"
"golang.org/x/sync/semaphore"

"github.com/parca-dev/parca-agent/pkg/cache"
"github.com/parca-dev/parca-agent/pkg/objectfile"
)

type Compiler struct {
Runtime

Type string
BuildID string
Stripped bool
Static bool
}

// TODO(kakkoyun): Consider moving this to the ProcessInfoManager.
// Requires FramePointerCache to be moved as well.

// CompilerInfoManager is a cache for compiler information.
// Fetching this information is expensive, so we cache it.
// The cache is safe for concurrent use.
// It also controls throughput of fetches.
type CompilerInfoManager struct {
p *objectfile.Pool
c *cache.Cache[string, *Compiler]

tokens *semaphore.Weighted
}

func NewCompilerInfoManager(reg prometheus.Registerer, objFilePool *objectfile.Pool) *CompilerInfoManager {
cores := runtime.NumCPU()
return &CompilerInfoManager{
p: objFilePool,
c: cache.NewLFUCache[string, *Compiler](
prometheus.WrapRegistererWith(prometheus.Labels{"cache": "runtime_compiler_info"}, reg),
2048,
),
tokens: semaphore.NewWeighted(int64(cores/2) + 1),
}
}

func (c *CompilerInfoManager) Fetch(path string) (*Compiler, error) {
if compiler, ok := c.c.Get(path); ok {
return compiler, nil
}

obj, err := c.p.Open(path)
if err != nil {
return nil, fmt.Errorf("failed to open ELF file %s: %w", path, err)
}

ef, err := obj.ELF()
if err != nil {
return nil, fmt.Errorf("failed to get ELF file %s: %w", path, err)
}

// Prevent too many concurrent fetches.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := c.tokens.Acquire(ctx, 1); err != nil {
return nil, fmt.Errorf("failed to acquire semaphore token: %w", err)
}
defer c.tokens.Release(1)

cType := ainur.Compiler(ef)
compiler := &Compiler{
Runtime: Runtime{
Name: RuntimeName(name(cType)),
Version: version(cType),
},
Type: cType,
Static: ainur.Static(ef),
Stripped: ainur.Stripped(ef),
BuildID: obj.BuildID,
}
c.c.Add(path, compiler)
return compiler, nil
}

func name(cType string) string {
parts := strings.Split(cType, " ")
if len(parts) > 0 && parts[0] != "" {
return parts[0]
}
return "unknown"
}

func version(cType string) *semver.Version {
parts := strings.Split(cType, " ")
if len(parts) < 2 {
return nil
}
ver, err := semver.NewVersion(parts[1])
if err != nil {
return nil
}
return ver
}
59 changes: 59 additions & 0 deletions pkg/runtime/compiler_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 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 runtime

import (
"testing"

"github.com/Masterminds/semver/v3"
)

func Test_version(t *testing.T) {
tests := []struct {
input string
want *semver.Version
}{
{
input: "GCC 7.2.0",
want: semver.MustParse("7.2.0"),
},
{
input: "Clang 8.2.1",
want: semver.MustParse("8.2.1"),
},
{
input: "Go 1.16.5",
want: semver.MustParse("1.16.5"),
},
{
input: "Rust (GCC 9.3.0)",
want: nil,
},
{
input: "Rust 1.27.0-nightly",
want: semver.MustParse("1.27.0-nightly"),
},
{
input: "DMD",
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
if got := version(tt.input); got != nil && got.Compare(tt.want) != 0 {
t.Errorf("version() = %v, want %v", got, tt.want)
}
})
}
}
Loading
Loading