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 }