xref: /aosp_15_r20/external/bcc/examples/lua/memleak.lua (revision 387f9dfdfa2baef462e92476d413c7bc2470293e)
1#!/usr/bin/env bcc-lua
2--[[
3Copyright 2016 GitHub, Inc
4
5Licensed under the Apache License, Version 2.0 (the "License");
6you may not use this file except in compliance with the License.
7You may obtain a copy of the License at
8
9http://www.apache.org/licenses/LICENSE-2.0
10
11Unless required by applicable law or agreed to in writing, software
12distributed under the License is distributed on an "AS IS" BASIS,
13WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14See the License for the specific language governing permissions and
15limitations under the License.
16]]
17
18local bpf_source = [[
19#include <uapi/linux/ptrace.h>
20
21struct alloc_info_t {
22        u64 size;
23        u64 timestamp_ns;
24        int stack_id;
25};
26
27BPF_HASH(sizes, u64);
28BPF_HASH(allocs, u64, struct alloc_info_t);
29BPF_STACK_TRACE(stack_traces, 10240);
30
31int alloc_enter(struct pt_regs *ctx, size_t size)
32{
33        SIZE_FILTER
34        if (SAMPLE_EVERY_N > 1) {
35                u64 ts = bpf_ktime_get_ns();
36                if (ts % SAMPLE_EVERY_N != 0)
37                        return 0;
38        }
39
40        u64 pid = bpf_get_current_pid_tgid();
41        u64 size64 = size;
42        sizes.update(&pid, &size64);
43
44        if (SHOULD_PRINT)
45                bpf_trace_printk("alloc entered, size = %u\n", size);
46        return 0;
47}
48
49int alloc_exit(struct pt_regs *ctx)
50{
51        u64 address = PT_REGS_RC(ctx);
52        u64 pid = bpf_get_current_pid_tgid();
53        u64* size64 = sizes.lookup(&pid);
54        struct alloc_info_t info = {0};
55
56        if (size64 == 0)
57                return 0; // missed alloc entry
58
59        info.size = *size64;
60        sizes.delete(&pid);
61
62        info.timestamp_ns = bpf_ktime_get_ns();
63        info.stack_id = stack_traces.get_stackid(ctx, STACK_FLAGS);
64
65        allocs.update(&address, &info);
66
67        if (SHOULD_PRINT) {
68                bpf_trace_printk("alloc exited, size = %lu, result = %lx\n",
69                                 info.size, address);
70        }
71        return 0;
72}
73
74int free_enter(struct pt_regs *ctx, void *address)
75{
76        u64 addr = (u64)address;
77        struct alloc_info_t *info = allocs.lookup(&addr);
78        if (info == 0)
79                return 0;
80
81        allocs.delete(&addr);
82
83        if (SHOULD_PRINT) {
84                bpf_trace_printk("free entered, address = %lx, size = %lu\n",
85                                 address, info->size);
86        }
87        return 0;
88}
89]]
90
91return function(BPF, utils)
92  local parser = utils.argparse("memleak", "Catch memory leaks")
93  parser:flag("-t --trace")
94  parser:flag("-a --show-allocs")
95  parser:option("-p --pid"):convert(tonumber)
96
97  parser:option("-i --interval", "", 5):convert(tonumber)
98  parser:option("-o --older", "", 500):convert(tonumber)
99  parser:option("-s --sample-rate", "", 1):convert(tonumber)
100
101  parser:option("-z --min-size", ""):convert(tonumber)
102  parser:option("-Z --max-size", ""):convert(tonumber)
103  parser:option("-T --top", "", 10):convert(tonumber)
104
105  local args = parser:parse()
106
107  local size_filter = ""
108  if args.min_size and args.max_size then
109    size_filter = "if (size < %d || size > %d) return 0;" %  {args.min_size, args.max_size}
110  elseif args.min_size then
111    size_filter = "if (size < %d) return 0;" % args.min_size
112  elseif args.max_size then
113    size_filter = "if (size > %d) return 0;" % args.max_size
114  end
115
116  local stack_flags = "0"
117  if args.pid then
118    stack_flags = stack_flags .. "|BPF_F_USER_STACK"
119  end
120
121  local text = bpf_source
122  text = text:gsub("SIZE_FILTER", size_filter)
123  text = text:gsub("STACK_FLAGS",  stack_flags)
124  text = text:gsub("SHOULD_PRINT", args.trace and "1" or "0")
125  text = text:gsub("SAMPLE_EVERY_N", tostring(args.sample_rate))
126
127  local bpf = BPF:new{text=text, debug=0}
128  local syms = nil
129  local min_age_ns = args.older * 1e6
130
131  if args.pid then
132    print("Attaching to malloc and free in pid %d, Ctrl+C to quit." % args.pid)
133    bpf:attach_uprobe{name="c", sym="malloc", fn_name="alloc_enter", pid=args.pid}
134    bpf:attach_uprobe{name="c", sym="malloc", fn_name="alloc_exit", pid=args.pid, retprobe=true}
135    bpf:attach_uprobe{name="c", sym="free", fn_name="free_enter", pid=args.pid}
136  else
137    print("Attaching to kmalloc and kfree, Ctrl+C to quit.")
138    bpf:attach_kprobe{event="__kmalloc", fn_name="alloc_enter"}
139    bpf:attach_kprobe{event="__kmalloc", fn_name="alloc_exit", retprobe=true} -- TODO
140    bpf:attach_kprobe{event="kfree", fn_name="free_enter"}
141  end
142
143  local syms = BPF.SymbolCache(args.pid)
144  local allocs = bpf:get_table("allocs")
145  local stack_traces = bpf:get_table("stack_traces")
146
147  local function resolve(addr)
148    local sym = syms:resolve(addr)
149    if args.pid == nil then
150      sym = sym .. " [kernel]"
151    end
152    return string.format("%s (%p)", sym, addr)
153  end
154
155  local function print_outstanding()
156    local alloc_info = {}
157    local now = utils.posix.time_ns()
158
159    print("[%s] Top %d stacks with outstanding allocations:" %
160      {os.date("%H:%M:%S"), args.top})
161
162    for address, info in allocs:items() do
163      if now - min_age_ns >= tonumber(info.timestamp_ns) then
164        local stack_id = tonumber(info.stack_id)
165
166        if stack_id >= 0 then
167          if alloc_info[stack_id] then
168            local s = alloc_info[stack_id]
169            s.count = s.count + 1
170            s.size = s.size + tonumber(info.size)
171          else
172            local stack = stack_traces:get(stack_id, resolve)
173            alloc_info[stack_id] = { stack=stack, count=1, size=tonumber(info.size) }
174          end
175        end
176
177        if args.show_allocs then
178          print("\taddr = %p size = %s" % {address, tonumber(info.size)})
179        end
180      end
181    end
182
183    local top = table.values(alloc_info)
184    table.sort(top, function(a, b) return a.size > b.size end)
185
186    for n, alloc in ipairs(top) do
187      print("\t%d bytes in %d allocations from stack\n\t\t%s" %
188        {alloc.size, alloc.count, table.concat(alloc.stack, "\n\t\t")})
189      if n == args.top then break end
190    end
191  end
192
193  if args.trace then
194    local pipe = bpf:pipe()
195    while true do
196      print(pipe:trace_fields())
197    end
198  else
199    while true do
200      utils.posix.sleep(args.interval)
201      syms:refresh()
202      print_outstanding()
203    end
204  end
205end
206