diff --git a/runsc/boot/BUILD b/runsc/boot/BUILD index 389bcb301..998ad5cf7 100644 --- a/runsc/boot/BUILD +++ b/runsc/boot/BUILD @@ -15,7 +15,6 @@ go_library( "loader.go", "mount_hints.go", "network.go", - "profile.go", "seccheck.go", "strace.go", "vfs.go", @@ -116,6 +115,7 @@ go_library( "//runsc/boot/pprof", "//runsc/boot/procfs", "//runsc/config", + "//runsc/profile", "//runsc/specutils", "//runsc/specutils/seccomp", "@com_github_opencontainers_runtime_spec//specs-go:go_default_library", diff --git a/runsc/boot/loader.go b/runsc/boot/loader.go index 166b19a9d..f78df13da 100644 --- a/runsc/boot/loader.go +++ b/runsc/boot/loader.go @@ -74,6 +74,7 @@ import ( _ "gvisor.dev/gvisor/runsc/boot/platforms" // register all platforms. "gvisor.dev/gvisor/runsc/boot/pprof" "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/profile" "gvisor.dev/gvisor/runsc/specutils" "gvisor.dev/gvisor/runsc/specutils/seccomp" @@ -205,21 +206,6 @@ type Args struct { TotalMem uint64 // UserLogFD is the file descriptor to write user logs to. UserLogFD int - // ProfileBlockFD is the file descriptor to write a block profile to. - // Valid if >=0. - ProfileBlockFD int - // ProfileCPUFD is the file descriptor to write a CPU profile to. - // Valid if >=0. - ProfileCPUFD int - // ProfileHeapFD is the file descriptor to write a heap profile to. - // Valid if >=0. - ProfileHeapFD int - // ProfileMutexFD is the file descriptor to write a mutex profile to. - // Valid if >=0. - ProfileMutexFD int - // TraceFD is the file descriptor to write a Go execution trace to. - // Valid if >=0. - TraceFD int // ProductName is the value to show in // /sys/devices/virtual/dmi/id/product_name. ProductName string @@ -229,6 +215,9 @@ type Args struct { // SinkFDs is an ordered array of file descriptors to be used by seccheck // sinks configured from the --pod-init-config file. SinkFDs []int + // ProfileOpts contains the set of profiles to enable and the + // corresponding FDs where profile data will be written. + ProfileOpts profile.Opts } // make sure stdioFDs are always the same on initial start and on restore @@ -237,7 +226,7 @@ const startingStdioFD = 256 // New initializes a new kernel loader configured by spec. // New also handles setting up a kernel for restoring a container. func New(args Args) (*Loader, error) { - stopProfiling := startProfiling(args) + stopProfiling := profile.Start(args.ProfileOpts) // We initialize the rand package now to make sure /dev/urandom is pre-opened // on kernels that do not support getrandom(2). diff --git a/runsc/boot/profile.go b/runsc/boot/profile.go deleted file mode 100644 index 3ecd3e532..000000000 --- a/runsc/boot/profile.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2021 The gVisor 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 boot - -import ( - "os" - "runtime" - "runtime/pprof" - "runtime/trace" - - "gvisor.dev/gvisor/pkg/log" - "gvisor.dev/gvisor/pkg/sentry/control" -) - -// startProfiling initiates profiling as defined by the ProfileConfig, and -// returns a function that should be called to stop profiling. -func startProfiling(args Args) func() { - var onStopProfiling []func() - stopProfiling := func() { - for _, f := range onStopProfiling { - f() - } - } - - if args.ProfileBlockFD >= 0 { - file := os.NewFile(uintptr(args.ProfileBlockFD), "profile-block") - - runtime.SetBlockProfileRate(control.DefaultBlockProfileRate) - onStopProfiling = append(onStopProfiling, func() { - if err := pprof.Lookup("block").WriteTo(file, 0); err != nil { - log.Warningf("Error writing block profile: %v", err) - } - file.Close() - runtime.SetBlockProfileRate(0) - }) - } - - if args.ProfileCPUFD >= 0 { - file := os.NewFile(uintptr(args.ProfileCPUFD), "profile-cpu") - - pprof.StartCPUProfile(file) - onStopProfiling = append(onStopProfiling, func() { - pprof.StopCPUProfile() - file.Close() - }) - } - - if args.ProfileHeapFD >= 0 { - file := os.NewFile(uintptr(args.ProfileHeapFD), "profile-heap") - - onStopProfiling = append(onStopProfiling, func() { - if err := pprof.Lookup("heap").WriteTo(file, 0); err != nil { - log.Warningf("Error writing heap profile: %v", err) - } - file.Close() - }) - } - - if args.ProfileMutexFD >= 0 { - file := os.NewFile(uintptr(args.ProfileMutexFD), "profile-mutex") - - prev := runtime.SetMutexProfileFraction(control.DefaultMutexProfileRate) - onStopProfiling = append(onStopProfiling, func() { - if err := pprof.Lookup("mutex").WriteTo(file, 0); err != nil { - log.Warningf("Error writing mutex profile: %v", err) - } - file.Close() - runtime.SetMutexProfileFraction(prev) - }) - } - - if args.TraceFD >= 0 { - file := os.NewFile(uintptr(args.TraceFD), "trace") - - trace.Start(file) - onStopProfiling = append(onStopProfiling, func() { - trace.Stop() - file.Close() - }) - } - - return stopProfiling -} diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index fc6e68e1d..8a1ac21af 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -65,6 +65,7 @@ go_library( "//runsc/fsgofer", "//runsc/fsgofer/filter", "//runsc/mitigate", + "//runsc/profile", "//runsc/specutils", "@com_github_google_subcommands//:go_default_library", "@com_github_opencontainers_runtime_spec//specs-go:go_default_library", diff --git a/runsc/cmd/boot.go b/runsc/cmd/boot.go index b35051a15..95143e90b 100644 --- a/runsc/cmd/boot.go +++ b/runsc/cmd/boot.go @@ -31,6 +31,7 @@ import ( "gvisor.dev/gvisor/runsc/cmd/util" "gvisor.dev/gvisor/runsc/config" "gvisor.dev/gvisor/runsc/flag" + "gvisor.dev/gvisor/runsc/profile" "gvisor.dev/gvisor/runsc/specutils" ) @@ -82,26 +83,6 @@ type Boot struct { // sandbox (e.g. gofer) and sent through this FD. mountsFD int - // profileBlockFD is the file descriptor to write a block profile to. - // Valid if >= 0. - profileBlockFD int - - // profileCPUFD is the file descriptor to write a CPU profile to. - // Valid if >= 0. - profileCPUFD int - - // profileHeapFD is the file descriptor to write a heap profile to. - // Valid if >= 0. - profileHeapFD int - - // profileMutexFD is the file descriptor to write a mutex profile to. - // Valid if >= 0. - profileMutexFD int - - // traceFD is the file descriptor to write a Go execution trace to. - // Valid if >= 0. - traceFD int - podInitConfigFD int sinkFDs intFlags @@ -117,6 +98,9 @@ type Boot struct { // productName is the value to show in // /sys/devices/virtual/dmi/id/product_name. productName string + + // FDs for profile data. + profileFDs profile.FDArgs } // Name implements subcommands.Command.Name. @@ -154,13 +138,11 @@ func (b *Boot) SetFlags(f *flag.FlagSet) { f.IntVar(&b.userLogFD, "user-log-fd", 0, "file descriptor to write user logs to. 0 means no logging.") f.IntVar(&b.startSyncFD, "start-sync-fd", -1, "required FD to used to synchronize sandbox startup") f.IntVar(&b.mountsFD, "mounts-fd", -1, "mountsFD is the file descriptor to read list of mounts after they have been resolved (direct paths, no symlinks).") - f.IntVar(&b.profileBlockFD, "profile-block-fd", -1, "file descriptor to write block profile to. -1 disables profiling.") - f.IntVar(&b.profileCPUFD, "profile-cpu-fd", -1, "file descriptor to write CPU profile to. -1 disables profiling.") - f.IntVar(&b.profileHeapFD, "profile-heap-fd", -1, "file descriptor to write heap profile to. -1 disables profiling.") - f.IntVar(&b.profileMutexFD, "profile-mutex-fd", -1, "file descriptor to write mutex profile to. -1 disables profiling.") - f.IntVar(&b.traceFD, "trace-fd", -1, "file descriptor to write Go execution trace to. -1 disables tracing.") f.IntVar(&b.podInitConfigFD, "pod-init-config-fd", -1, "file descriptor to the pod init configuration file.") f.Var(&b.sinkFDs, "sink-fds", "ordered list of file descriptors to be used by the sinks defined in --pod-init-config.") + + // Profiling flags. + b.profileFDs.SetFromFlags(f) } // Execute implements subcommands.Command.Execute. It starts a sandbox in a @@ -308,14 +290,10 @@ func (b *Boot) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) NumCPU: b.cpuNum, TotalMem: b.totalMem, UserLogFD: b.userLogFD, - ProfileBlockFD: b.profileBlockFD, - ProfileCPUFD: b.profileCPUFD, - ProfileHeapFD: b.profileHeapFD, - ProfileMutexFD: b.profileMutexFD, - TraceFD: b.traceFD, ProductName: b.productName, PodInitConfigFD: b.podInitConfigFD, SinkFDs: b.sinkFDs.GetArray(), + ProfileOpts: b.profileFDs.ToOpts(), } l, err := boot.New(bootArgs) if err != nil { diff --git a/runsc/cmd/gofer.go b/runsc/cmd/gofer.go index 88c1c6ade..a04d7a2ba 100644 --- a/runsc/cmd/gofer.go +++ b/runsc/cmd/gofer.go @@ -37,6 +37,7 @@ import ( "gvisor.dev/gvisor/runsc/flag" "gvisor.dev/gvisor/runsc/fsgofer" "gvisor.dev/gvisor/runsc/fsgofer/filter" + "gvisor.dev/gvisor/runsc/profile" "gvisor.dev/gvisor/runsc/specutils" ) @@ -68,6 +69,9 @@ type Gofer struct { specFD int mountsFD int syncUsernsFD int + + profileFDs profile.FDArgs + stopProfiling func() } // Name implements subcommands.Command. @@ -96,6 +100,9 @@ func (g *Gofer) SetFlags(f *flag.FlagSet) { f.IntVar(&g.specFD, "spec-fd", -1, "required fd with the container spec") f.IntVar(&g.mountsFD, "mounts-fd", -1, "mountsFD is the file descriptor to write list of mounts after they have been resolved (direct paths, no symlinks).") f.IntVar(&g.syncUsernsFD, "sync-userns-fd", -1, "file descriptor used to synchronize rootless user namespace initialization.") + + // Profiling flags. + g.profileFDs.SetFromFlags(f) } // Execute implements subcommands.Command. @@ -156,6 +163,11 @@ func (g *Gofer) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) // We expcec that setCapsAndCallSelfsetCapsAndCallSelf has to be called in this case. panic("unreachable") } + + // Start profiling. This will be a noop if no profiling arguments were passed. + profileOpts := g.profileFDs.ToOpts() + g.stopProfiling = profile.Start(profileOpts) + // At this point we won't re-execute, so it's safe to limit via rlimits. Any // limit >= 0 works. If the limit is lower than the current number of open // files, then Setrlimit will succeed, and the next open will fail. @@ -214,11 +226,11 @@ func (g *Gofer) Execute(_ context.Context, f *flag.FlagSet, args ...interface{}) log.Infof("Process chroot'd to %q", root) // Initialize filters. - if conf.FSGoferHostUDS { - filter.InstallUDSFilters() + opts := filter.Options{ + UDSEnabled: conf.FSGoferHostUDS, + ProfileEnabled: len(profileOpts) > 0, } - - if err := filter.Install(); err != nil { + if err := filter.Install(opts); err != nil { util.Fatalf("installing seccomp filters: %v", err) } @@ -295,6 +307,9 @@ func (g *Gofer) serveLisafs(spec *specs.Spec, conf *config.Config, root string) server.Wait() server.Destroy() log.Infof("All lisafs servers exited.") + if g.stopProfiling != nil { + g.stopProfiling() + } return subcommands.ExitSuccess } @@ -353,6 +368,9 @@ func (g *Gofer) serve9P(spec *specs.Spec, conf *config.Config, root string) subc } wg.Wait() log.Infof("All 9P servers exited.") + if g.stopProfiling != nil { + g.stopProfiling() + } return subcommands.ExitSuccess } diff --git a/runsc/container/container.go b/runsc/container/container.go index dcd28acc6..8e0ff3b85 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -927,6 +927,11 @@ func (c *Container) createGoferProcess(spec *specs.Spec, conf *config.Config, bu } donations.DonateAndClose("spec-fd", specFile) + // Donate any profile FDs to the gofer. + if err := c.donateGoferProfileFDs(conf, &donations); err != nil { + return nil, nil, fmt.Errorf("donating gofer profile fds: %w", err) + } + // Create pipe that allows gofer to send mount list to sandbox after all paths // have been resolved. mountsSand, mountsGofer, err := os.Pipe() @@ -1341,6 +1346,45 @@ func (c *Container) setupCgroupForSubcontainer(conf *config.Config, spec *specs. return cgroupInstall(conf, cg, &specs.LinuxResources{}) } +// donateGoferProfileFDs will open profile files and donate their FDs to the +// gofer. +func (c *Container) donateGoferProfileFDs(conf *config.Config, donations *donation.Agency) error { + // The gofer profile files are named based on the provided flag, but + // suffixed with "gofer" and the container ID to avoid collisions with + // sentry profile files or profile files from other gofers. + // + // TODO(b/243183772): Merge gofer profile data with sentry profile data + // into a single file. + profSuffix := ".gofer." + c.ID + const profFlags = os.O_CREATE | os.O_WRONLY | os.O_TRUNC + if conf.ProfileBlock != "" { + if err := donations.OpenAndDonate("profile-block-fd", conf.ProfileBlock+profSuffix, profFlags); err != nil { + return err + } + } + if conf.ProfileCPU != "" { + if err := donations.OpenAndDonate("profile-cpu-fd", conf.ProfileCPU+profSuffix, profFlags); err != nil { + return err + } + } + if conf.ProfileHeap != "" { + if err := donations.OpenAndDonate("profile-heap-fd", conf.ProfileHeap+profSuffix, profFlags); err != nil { + return err + } + } + if conf.ProfileMutex != "" { + if err := donations.OpenAndDonate("profile-mutex-fd", conf.ProfileMutex+profSuffix, profFlags); err != nil { + return err + } + } + if conf.TraceFile != "" { + if err := donations.OpenAndDonate("trace-fd", conf.TraceFile+profSuffix, profFlags); err != nil { + return err + } + } + return nil +} + // cgroupInstall creates cgroups dir structure and sets their respective // resources. In case of success, returns the cgroups instance and nil error. // For rootless, it's possible that cgroups operations fail, in this case the diff --git a/runsc/fsgofer/filter/BUILD b/runsc/fsgofer/filter/BUILD index 9707edd68..886275991 100644 --- a/runsc/fsgofer/filter/BUILD +++ b/runsc/fsgofer/filter/BUILD @@ -8,6 +8,7 @@ go_library( "config.go", "config_amd64.go", "config_arm64.go", + "config_profile.go", "extra_filters.go", "extra_filters_msan.go", "extra_filters_race.go", diff --git a/runsc/fsgofer/filter/config_profile.go b/runsc/fsgofer/filter/config_profile.go new file mode 100644 index 000000000..358448243 --- /dev/null +++ b/runsc/fsgofer/filter/config_profile.go @@ -0,0 +1,47 @@ +// Copyright 2020 The gVisor 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 filter + +import ( + "golang.org/x/sys/unix" + "gvisor.dev/gvisor/pkg/seccomp" +) + +var profileFilters = seccomp.SyscallRules{ + unix.SYS_OPENAT: []seccomp.Rule{ + { + seccomp.MatchAny{}, + seccomp.MatchAny{}, + seccomp.EqualTo(unix.O_RDONLY | unix.O_LARGEFILE | unix.O_CLOEXEC), + }, + }, + unix.SYS_SETITIMER: {}, + unix.SYS_TIMER_CREATE: []seccomp.Rule{ + { + seccomp.EqualTo(unix.CLOCK_THREAD_CPUTIME_ID), /* which */ + seccomp.MatchAny{}, /* sevp */ + seccomp.MatchAny{}, /* timerid */ + }, + }, + unix.SYS_TIMER_DELETE: []seccomp.Rule{}, + unix.SYS_TIMER_SETTIME: []seccomp.Rule{ + { + seccomp.MatchAny{}, /* timerid */ + seccomp.EqualTo(0), /* flags */ + seccomp.MatchAny{}, /* new_value */ + seccomp.EqualTo(0), /* old_value */ + }, + }, +} diff --git a/runsc/fsgofer/filter/filter.go b/runsc/fsgofer/filter/filter.go index e87fc81d9..8665c548b 100644 --- a/runsc/fsgofer/filter/filter.go +++ b/runsc/fsgofer/filter/filter.go @@ -18,21 +18,38 @@ package filter import ( + "gvisor.dev/gvisor/pkg/log" "gvisor.dev/gvisor/pkg/seccomp" ) +// Options are seccomp filter related options. +type Options struct { + UDSEnabled bool + ProfileEnabled bool +} + // Install installs seccomp filters. -func Install() error { +func Install(opt Options) error { + s := allowedSyscalls + + if opt.ProfileEnabled { + report("profile enabled: syscall filters less restrictive!") + s.Merge(profileFilters) + } + + if opt.UDSEnabled { + report("host UDS enabled: syscall filters less restrictive!") + s.Merge(udsSyscalls) + } + // Set of additional filters used by -race and -msan. Returns empty // when not enabled. - allowedSyscalls.Merge(instrumentationFilters()) + s.Merge(instrumentationFilters()) - return seccomp.Install(allowedSyscalls, seccomp.DenyNewExecMappings) + return seccomp.Install(s, seccomp.DenyNewExecMappings) } -// InstallUDSFilters extends the allowed syscalls to include those necessary for -// creating and connecting to host UDS. -func InstallUDSFilters() { - // Add additional filters required for connecting to the host's sockets. - allowedSyscalls.Merge(udsSyscalls) +// report writes a warning message to the log. +func report(msg string) { + log.Warningf("*** SECCOMP WARNING: %s", msg) } diff --git a/runsc/profile/BUILD b/runsc/profile/BUILD new file mode 100644 index 000000000..aa3a4f4f2 --- /dev/null +++ b/runsc/profile/BUILD @@ -0,0 +1,16 @@ +load("//tools:defs.bzl", "go_library") + +package(licenses = ["notice"]) + +go_library( + name = "profile", + srcs = ["profile.go"], + visibility = [ + "//runsc:__subpackages__", + ], + deps = [ + "//pkg/log", + "//pkg/sentry/control", + "//runsc/flag", + ], +) diff --git a/runsc/profile/profile.go b/runsc/profile/profile.go new file mode 100644 index 000000000..64b5369f1 --- /dev/null +++ b/runsc/profile/profile.go @@ -0,0 +1,179 @@ +// Copyright 2021 The gVisor 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 profile contains profiling utils. +package profile + +import ( + "os" + "runtime" + "runtime/pprof" + "runtime/trace" + + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/sentry/control" + "gvisor.dev/gvisor/runsc/flag" +) + +// Kind is the kind of profiling to perform. +type Kind int + +const ( + // Block profile. + Block Kind = iota + // CPU profile. + CPU + // Heap profile. + Heap + // Mutex profile. + Mutex + // Trace profile. + Trace +) + +// FDArgs are the arguments that describe which profiles to enable and which +// FDs to write them to. Profiling of a given type will only be enabled if the +// corresponding FD is >=0. +type FDArgs struct { + // BlockFD is the file descriptor to write a block profile to. + // Valid if >=0. + BlockFD int + // CPUFD is the file descriptor to write a CPU profile to. + // Valid if >=0. + CPUFD int + // HeapFD is the file descriptor to write a heap profile to. + // Valid if >=0. + HeapFD int + // MutexFD is the file descriptor to write a mutex profile to. + // Valid if >=0. + MutexFD int + // TraceFD is the file descriptor to write a Go execution trace to. + // Valid if >=0. + TraceFD int +} + +// SetFromFlags sets the FDArgs from the given flags. The default value for +// each FD is -1. +func (fds *FDArgs) SetFromFlags(f *flag.FlagSet) { + f.IntVar(&fds.BlockFD, "profile-block-fd", -1, "file descriptor to write block profile to. -1 disables profiling.") + f.IntVar(&fds.CPUFD, "profile-cpu-fd", -1, "file descriptor to write CPU profile to. -1 disables profiling.") + f.IntVar(&fds.HeapFD, "profile-heap-fd", -1, "file descriptor to write heap profile to. -1 disables profiling.") + f.IntVar(&fds.MutexFD, "profile-mutex-fd", -1, "file descriptor to write mutex profile to. -1 disables profiling.") + f.IntVar(&fds.TraceFD, "trace-fd", -1, "file descriptor to write Go execution trace to. -1 disables tracing.") +} + +// Opts is a map of profile Kind to FD. +type Opts map[Kind]uintptr + +// ToOpts turns FDArgs into an Opts struct which can be passed to Start. +func (fds *FDArgs) ToOpts() Opts { + o := Opts{} + if fds.BlockFD >= 0 { + o[Block] = uintptr(fds.BlockFD) + } + if fds.CPUFD >= 0 { + o[CPU] = uintptr(fds.CPUFD) + } + if fds.HeapFD >= 0 { + o[Heap] = uintptr(fds.HeapFD) + } + if fds.MutexFD >= 0 { + o[Mutex] = uintptr(fds.MutexFD) + } + if fds.TraceFD >= 0 { + o[Trace] = uintptr(fds.TraceFD) + } + return o +} + +// Start starts profiling for the given Kinds in opts, and writes the profile +// data to the corresponding FDs in opts. It returns a function which will stop +// profiling. +func Start(opts Opts) func() { + var onStopProfiling []func() + stopProfiling := func() { + for _, f := range onStopProfiling { + f() + } + } + + if fd, ok := opts[Block]; ok { + log.Infof("Block profiling enabled") + file := os.NewFile(fd, "profile-block") + + runtime.SetBlockProfileRate(control.DefaultBlockProfileRate) + onStopProfiling = append(onStopProfiling, func() { + if err := pprof.Lookup("block").WriteTo(file, 0); err != nil { + log.Warningf("Error writing block profile: %v", err) + } + file.Close() + runtime.SetBlockProfileRate(0) + log.Infof("Block profiling stopped") + }) + } + + if fd, ok := opts[CPU]; ok { + log.Infof("CPU profiling enabled") + file := os.NewFile(fd, "profile-cpu") + + pprof.StartCPUProfile(file) + onStopProfiling = append(onStopProfiling, func() { + pprof.StopCPUProfile() + file.Close() + log.Infof("CPU profiling stopped") + }) + } + + if fd, ok := opts[Heap]; ok { + log.Infof("Heap profiling enabled") + file := os.NewFile(fd, "profile-heap") + + onStopProfiling = append(onStopProfiling, func() { + if err := pprof.Lookup("heap").WriteTo(file, 0); err != nil { + log.Warningf("Error writing heap profile: %v", err) + } + file.Close() + log.Infof("Heap profiling stopped") + }) + } + + if fd, ok := opts[Mutex]; ok { + log.Infof("Mutex profiling enabled") + file := os.NewFile(fd, "profile-mutex") + + prev := runtime.SetMutexProfileFraction(control.DefaultMutexProfileRate) + onStopProfiling = append(onStopProfiling, func() { + if err := pprof.Lookup("mutex").WriteTo(file, 0); err != nil { + log.Warningf("Error writing mutex profile: %v", err) + } + file.Close() + runtime.SetMutexProfileFraction(prev) + log.Infof("Mutex profiling stopped") + }) + } + + if fd, ok := opts[Trace]; ok { + log.Infof("Tracing enabled") + file := os.NewFile(fd, "trace") + + trace.Start(file) + onStopProfiling = append(onStopProfiling, func() { + trace.Stop() + file.Close() + log.Infof("Tracing stopped") + }) + } + + return stopProfiling +}