github.com/inspektor-gadget/inspektor-gadget@v0.28.1/pkg/gadgets/top/ebpf/tracer/tracer.go (about)

     1  // Copyright 2019-2023 The Inspektor Gadget authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  //go:build !withoutebpf
    16  
    17  package tracer
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"errors"
    23  	"fmt"
    24  	"math"
    25  	"os"
    26  	"path/filepath"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/cilium/ebpf"
    32  	"github.com/tklauser/numcpus"
    33  
    34  	"github.com/inspektor-gadget/inspektor-gadget/pkg/bpfstats"
    35  	"github.com/inspektor-gadget/inspektor-gadget/pkg/columns"
    36  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets"
    37  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/top"
    38  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/top/ebpf/piditer"
    39  	"github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/top/ebpf/types"
    40  	"github.com/inspektor-gadget/inspektor-gadget/pkg/utils/host"
    41  )
    42  
    43  type Config struct {
    44  	MaxRows    int
    45  	Interval   time.Duration
    46  	Iterations int
    47  	SortBy     []string
    48  }
    49  
    50  type programStats struct {
    51  	runtime  int64
    52  	runCount uint64
    53  }
    54  
    55  type Tracer struct {
    56  	config        *Config
    57  	enricher      gadgets.DataNodeEnricher
    58  	eventCallback func(*top.Event[types.Stats])
    59  	done          chan bool
    60  
    61  	iter                *piditer.PidIter
    62  	useFallbackIterator bool
    63  
    64  	startStats map[string]programStats
    65  	prevStats  map[string]programStats
    66  	colMap     columns.ColumnMap[types.Stats]
    67  }
    68  
    69  func NewTracer(config *Config, enricher gadgets.DataNodeEnricher,
    70  	eventCallback func(*top.Event[types.Stats]),
    71  ) (*Tracer, error) {
    72  	t := &Tracer{
    73  		config:        config,
    74  		enricher:      enricher,
    75  		eventCallback: eventCallback,
    76  		done:          make(chan bool),
    77  		prevStats:     make(map[string]programStats),
    78  	}
    79  
    80  	if err := t.install(); err != nil {
    81  		t.close()
    82  		return nil, err
    83  	}
    84  
    85  	statCols, err := columns.NewColumns[types.Stats]()
    86  	if err != nil {
    87  		t.close()
    88  		return nil, err
    89  	}
    90  	t.colMap = statCols.GetColumnMap()
    91  
    92  	go t.run(context.TODO())
    93  
    94  	return t, nil
    95  }
    96  
    97  func (t *Tracer) install() error {
    98  	// Enable stats collection
    99  	err := bpfstats.EnableBPFStats()
   100  	if err != nil {
   101  		return err
   102  	}
   103  
   104  	t.useFallbackIterator = false
   105  
   106  	// To resolve pids, we will first try to iterate using a bpf
   107  	// program. If that doesn't work, we will fall back to scanning
   108  	// all used fds in all processes /proc/$pid/fdinfo/$fd.
   109  	iter, err := piditer.NewTracer()
   110  	if err != nil {
   111  		t.useFallbackIterator = true
   112  	} else {
   113  		t.iter = iter
   114  	}
   115  
   116  	return nil
   117  }
   118  
   119  // Stop stops the tracer
   120  // TODO: Remove after refactoring
   121  func (t *Tracer) Stop() {
   122  	t.close()
   123  }
   124  
   125  func (t *Tracer) close() {
   126  	close(t.done)
   127  
   128  	if t.iter != nil {
   129  		t.iter.Close()
   130  	}
   131  
   132  	bpfstats.DisableBPFStats()
   133  }
   134  
   135  func getPidMapFromPids(pids []*piditer.PidIterEntry) map[uint32][]*types.Process {
   136  	pidmap := make(map[uint32][]*types.Process)
   137  	for _, e := range pids {
   138  		if _, ok := pidmap[e.ProgID]; !ok {
   139  			pidmap[e.ProgID] = make([]*types.Process, 0, 1)
   140  		}
   141  		pidmap[e.ProgID] = append(pidmap[e.ProgID], &types.Process{
   142  			Pid:  e.Pid,
   143  			Comm: e.Comm,
   144  		})
   145  	}
   146  	return pidmap
   147  }
   148  
   149  func getProgIDFromFile(fn string) (uint32, error) {
   150  	f, err := os.Open(fn)
   151  	if err != nil {
   152  		return 0, err
   153  	}
   154  	defer f.Close()
   155  
   156  	sc := bufio.NewScanner(f)
   157  	for sc.Scan() {
   158  		if strings.HasPrefix(sc.Text(), "prog_id:") {
   159  			progID, _ := strconv.ParseUint(strings.TrimSpace(strings.Split(sc.Text(), ":")[1]), 10, 32)
   160  			return uint32(progID), nil
   161  		}
   162  	}
   163  	return 0, os.ErrNotExist
   164  }
   165  
   166  func getPidMapFromProcFs() (map[uint32][]*types.Process, error) {
   167  	processes, err := os.ReadDir(host.HostProcFs)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	pidmap := make(map[uint32][]*types.Process)
   172  	for _, p := range processes {
   173  		if !p.IsDir() {
   174  			continue
   175  		}
   176  		_, err := strconv.Atoi(p.Name())
   177  		if err != nil {
   178  			continue
   179  		}
   180  		fdescs, err := os.ReadDir(filepath.Join(host.HostProcFs, p.Name(), "fdinfo"))
   181  		if err != nil {
   182  			continue
   183  		}
   184  		for _, fd := range fdescs {
   185  			if progID, err := getProgIDFromFile(filepath.Join(host.HostProcFs, p.Name(), "fdinfo", fd.Name())); err == nil {
   186  				pid, err := strconv.ParseUint(p.Name(), 10, 32)
   187  				if err != nil {
   188  					return nil, err
   189  				}
   190  				if pid > math.MaxInt32 {
   191  					return nil, fmt.Errorf("PID (%d) exceeds math.MaxInt32 (%d)", pid, math.MaxInt32)
   192  				}
   193  				if _, ok := pidmap[progID]; !ok {
   194  					pidmap[progID] = make([]*types.Process, 0, 1)
   195  				}
   196  				comm := host.GetProcComm(int(pid))
   197  				pidmap[progID] = append(pidmap[progID], &types.Process{
   198  					Pid:  uint32(pid),
   199  					Comm: strings.TrimSpace(string(comm)),
   200  				})
   201  			}
   202  		}
   203  	}
   204  	return pidmap, nil
   205  }
   206  
   207  func getMemoryUsage(m *ebpf.Map) (uint64, error) {
   208  	fdInfoPath := filepath.Join(host.HostProcFs, "self", "fdinfo", fmt.Sprint(m.FD()))
   209  	f, err := os.Open(fdInfoPath)
   210  	if err != nil {
   211  		return 0, fmt.Errorf("reading fdinfo: %w", err)
   212  	}
   213  	defer f.Close()
   214  
   215  	sc := bufio.NewScanner(f)
   216  	for sc.Scan() {
   217  		if strings.HasPrefix(sc.Text(), "memlock:\t") {
   218  			lineSplit := strings.Split(sc.Text(), "\t")
   219  			if len(lineSplit) == 2 {
   220  				size, err := strconv.ParseUint(lineSplit[1], 10, 64)
   221  				if err != nil {
   222  					return 0, fmt.Errorf("reading memlock: %w", err)
   223  				}
   224  				return size, nil
   225  			}
   226  		}
   227  	}
   228  	return 0, fmt.Errorf("finding memlock in fdinfo")
   229  }
   230  
   231  func (t *Tracer) nextStats() ([]*types.Stats, error) {
   232  	stats := make([]*types.Stats, 0)
   233  
   234  	var err error
   235  	var prog *ebpf.Program
   236  	var mapData *ebpf.Map
   237  	var pids []*piditer.PidIterEntry
   238  	curID := ebpf.ProgramID(0)
   239  	nextID := ebpf.ProgramID(0)
   240  
   241  	curMapID := ebpf.MapID(0)
   242  	nextMapID := ebpf.MapID(0)
   243  
   244  	curStats := make(map[string]programStats)
   245  
   246  	mapSizes := make(map[ebpf.MapID]uint64)
   247  
   248  	numOnlineCPUs, err := numcpus.GetOnline()
   249  	if err != nil {
   250  		return nil, fmt.Errorf("getting number of online cpu: %w", err)
   251  	}
   252  
   253  	// Get memory usage by maps
   254  	for {
   255  		nextMapID, err = ebpf.MapGetNextID(curMapID)
   256  		if err != nil {
   257  			if errors.Is(err, os.ErrNotExist) {
   258  				break
   259  			}
   260  			return nil, fmt.Errorf("getting next map ID: %w", err)
   261  		}
   262  		if nextMapID <= curMapID {
   263  			break
   264  		}
   265  		curMapID = nextMapID
   266  		mapData, err = ebpf.NewMapFromID(curMapID)
   267  		if err != nil {
   268  			continue
   269  		}
   270  
   271  		mapSizes[curMapID], err = getMemoryUsage(mapData)
   272  		mapData.Close()
   273  		if err != nil {
   274  			return nil, fmt.Errorf("getting memory usage of map ID (%d): %w", curMapID, err)
   275  		}
   276  	}
   277  
   278  	for {
   279  		nextID, err = ebpf.ProgramGetNextID(curID)
   280  		if err != nil {
   281  			if errors.Is(err, os.ErrNotExist) {
   282  				break
   283  			}
   284  			return nil, fmt.Errorf("getting next program ID: %w", err)
   285  		}
   286  		if nextID <= curID {
   287  			break
   288  		}
   289  		curID = nextID
   290  		prog, err = ebpf.NewProgramFromID(curID)
   291  		if err != nil {
   292  			continue
   293  		}
   294  		pi, err := prog.Info()
   295  		if err != nil {
   296  			prog.Close()
   297  			continue
   298  		}
   299  
   300  		totalMapMemory := uint64(0)
   301  		mapIDs, _ := pi.MapIDs()
   302  		for _, mapID := range mapIDs {
   303  			if size, ok := mapSizes[mapID]; ok {
   304  				totalMapMemory += size
   305  			}
   306  		}
   307  
   308  		totalRuntime, _ := pi.Runtime()
   309  		totalRunCount, _ := pi.RunCount()
   310  
   311  		curRuntime := int64(0)
   312  		curRunCount := uint64(0)
   313  		cumulativeRuntime := int64(0)
   314  		cumulativeRunCount := uint64(0)
   315  
   316  		pkey := fmt.Sprintf("%d-%s", curID, pi.Tag)
   317  
   318  		// calculate delta, if possible
   319  		if old, ok := t.prevStats[pkey]; ok {
   320  			curRuntime = int64(totalRuntime) - old.runtime
   321  			curRunCount = totalRunCount - old.runCount
   322  		}
   323  		if t.startStats != nil {
   324  			if start, ok := t.startStats[pkey]; ok {
   325  				cumulativeRuntime = int64(totalRuntime) - start.runtime
   326  				cumulativeRunCount = totalRunCount - start.runCount
   327  			} else {
   328  				cumulativeRuntime = int64(totalRuntime)
   329  				cumulativeRunCount = totalRunCount
   330  			}
   331  		}
   332  
   333  		curStats[pkey] = programStats{
   334  			runtime:  int64(totalRuntime),
   335  			runCount: totalRunCount,
   336  		}
   337  
   338  		totalCpuUsage := 100 * float64(curRuntime) / float64(t.config.Interval.Nanoseconds())
   339  
   340  		stat := &types.Stats{
   341  			ProgramID:          uint32(curID),
   342  			Name:               pi.Name,
   343  			Type:               pi.Type.String(),
   344  			CurrentRuntime:     curRuntime,
   345  			CurrentRunCount:    curRunCount,
   346  			TotalRuntime:       int64(totalRuntime),
   347  			TotalRunCount:      totalRunCount,
   348  			CumulativeRuntime:  cumulativeRuntime,
   349  			CumulativeRunCount: cumulativeRunCount,
   350  			MapMemory:          totalMapMemory,
   351  			MapCount:           uint32(len(mapIDs)),
   352  			TotalCpuUsage:      totalCpuUsage,
   353  			PerCpuUsage:        totalCpuUsage / float64(numOnlineCPUs),
   354  		}
   355  
   356  		if t.enricher != nil {
   357  			t.enricher.EnrichNode(&stat.CommonData)
   358  		}
   359  
   360  		stats = append(stats, stat)
   361  
   362  		prog.Close()
   363  	}
   364  
   365  	if t.startStats == nil {
   366  		t.startStats = curStats
   367  	}
   368  
   369  	t.prevStats = curStats
   370  
   371  	var processMap map[uint32][]*types.Process
   372  
   373  	if !t.useFallbackIterator {
   374  		pids, err = t.iter.DumpPids()
   375  		if err != nil {
   376  			return nil, fmt.Errorf("getting pids for programs using iterator: %w", err)
   377  		}
   378  		processMap = getPidMapFromPids(pids)
   379  	} else {
   380  		// Fallback...
   381  		processMap, err = getPidMapFromProcFs()
   382  		if err != nil {
   383  			return nil, fmt.Errorf("getting pids for programs using fallback method: %w", err)
   384  		}
   385  	}
   386  
   387  	for i := range stats {
   388  		if tmpProcesses, ok := processMap[stats[i].ProgramID]; ok {
   389  			stats[i].Processes = tmpProcesses
   390  		}
   391  	}
   392  
   393  	top.SortStats(stats, t.config.SortBy, &t.colMap)
   394  
   395  	return stats, nil
   396  }
   397  
   398  func (t *Tracer) run(ctx context.Context) error {
   399  	// Don't use a context with a timeout but a counter to avoid having to deal
   400  	// with two timers: one for the timeout and another for the ticker.
   401  	count := t.config.Iterations
   402  	ticker := time.NewTicker(t.config.Interval)
   403  	defer ticker.Stop()
   404  
   405  	for {
   406  		select {
   407  		case <-t.done:
   408  			// TODO: Once we completely move to use Run instead of NewTracer,
   409  			// we can remove this as nobody will directly call Stop (cleanup).
   410  			return nil
   411  		case <-ctx.Done():
   412  			return nil
   413  		case <-ticker.C:
   414  			stats, err := t.nextStats()
   415  			if err != nil {
   416  				return fmt.Errorf("getting next stats: %w", err)
   417  			}
   418  
   419  			n := len(stats)
   420  			if n > t.config.MaxRows {
   421  				n = t.config.MaxRows
   422  			}
   423  			t.eventCallback(&top.Event[types.Stats]{Stats: stats[:n]})
   424  
   425  			// Count down only if user requested a finite number of iterations
   426  			// through a timeout.
   427  			if t.config.Iterations > 0 {
   428  				count--
   429  				if count == 0 {
   430  					return nil
   431  				}
   432  			}
   433  		}
   434  	}
   435  }
   436  
   437  // --- Registry changes
   438  
   439  func (t *Tracer) Run(gadgetCtx gadgets.GadgetContext) error {
   440  	if err := t.init(gadgetCtx); err != nil {
   441  		return fmt.Errorf("initializing tracer: %w", err)
   442  	}
   443  
   444  	defer t.close()
   445  	if err := t.install(); err != nil {
   446  		return fmt.Errorf("installing tracer: %w", err)
   447  	}
   448  
   449  	return t.run(gadgetCtx.Context())
   450  }
   451  
   452  func (t *Tracer) SetEventHandlerArray(handler any) {
   453  	nh, ok := handler.(func(ev []*types.Stats))
   454  	if !ok {
   455  		panic("event handler invalid")
   456  	}
   457  
   458  	// TODO: add errorHandler
   459  	t.eventCallback = func(ev *top.Event[types.Stats]) {
   460  		if ev.Error != "" {
   461  			return
   462  		}
   463  		nh(ev.Stats)
   464  	}
   465  }
   466  
   467  func (g *GadgetDesc) NewInstance() (gadgets.Gadget, error) {
   468  	tracer := &Tracer{
   469  		config:    &Config{},
   470  		done:      make(chan bool),
   471  		prevStats: make(map[string]programStats),
   472  	}
   473  	return tracer, nil
   474  }
   475  
   476  func (t *Tracer) init(gadgetCtx gadgets.GadgetContext) error {
   477  	params := gadgetCtx.GadgetParams()
   478  	t.config.MaxRows = params.Get(gadgets.ParamMaxRows).AsInt()
   479  	t.config.SortBy = params.Get(gadgets.ParamSortBy).AsStringSlice()
   480  	t.config.Interval = time.Second * time.Duration(params.Get(gadgets.ParamInterval).AsInt())
   481  
   482  	var err error
   483  	if t.config.Iterations, err = top.ComputeIterations(t.config.Interval, gadgetCtx.Timeout()); err != nil {
   484  		return err
   485  	}
   486  
   487  	statCols, err := columns.NewColumns[types.Stats]()
   488  	if err != nil {
   489  		return err
   490  	}
   491  	t.colMap = statCols.GetColumnMap()
   492  
   493  	return nil
   494  }