#!/usr/bin/python
# @lint-avoid-python-3-compatibility-imports
#
# uobjnew  Summarize object allocations in high-level languages.
#          For Linux, uses BCC, eBPF.
#
# USAGE: uobjnew [-h] [-T TOP] [-v] {c,java,ruby,tcl} pid [interval]
#
# Copyright 2016 Sasha Goldshtein
# Licensed under the Apache License, Version 2.0 (the "License")
#
# 25-Oct-2016   Sasha Goldshtein   Created this.

from __future__ import print_function
import argparse
from bcc import BPF, USDT, utils
from time import sleep
import os

# C needs to be the last language.
languages = ["c", "java", "ruby", "tcl"]

examples = """examples:
    ./uobjnew -l java 145         # summarize Java allocations in process 145
    ./uobjnew -l c 2020 1         # grab malloc() sizes and print every second
    ./uobjnew -l ruby 6712 -C 10  # top 10 Ruby types by number of allocations
    ./uobjnew -l ruby 6712 -S 10  # top 10 Ruby types by total size
"""
parser = argparse.ArgumentParser(
    description="Summarize object allocations in high-level languages.",
    formatter_class=argparse.RawDescriptionHelpFormatter,
    epilog=examples)
parser.add_argument("-l", "--language", choices=languages,
    help="language to trace")
parser.add_argument("pid", type=int, help="process id to attach to")
parser.add_argument("interval", type=int, nargs='?',
    help="print every specified number of seconds")
parser.add_argument("-C", "--top-count", type=int,
    help="number of most frequently allocated types to print")
parser.add_argument("-S", "--top-size", type=int,
    help="number of largest types by allocated bytes to print")
parser.add_argument("-v", "--verbose", action="store_true",
    help="verbose mode: print the BPF program (for debugging purposes)")
parser.add_argument("--ebpf", action="store_true",
    help=argparse.SUPPRESS)
args = parser.parse_args()

language = args.language
if not language:
    language = utils.detect_language(languages, args.pid)

program = """
#include <linux/ptrace.h>

struct key_t {
#if MALLOC_TRACING
    u64 size;
#else
    char name[50];
#endif
};

struct val_t {
    u64 total_size;
    u64 num_allocs;
};

BPF_HASH(allocs, struct key_t, struct val_t);
""".replace("MALLOC_TRACING", "1" if language == "c" else "0")

usdt = USDT(pid=args.pid)

#
# C
#
if language == "c":
    program += """
int alloc_entry(struct pt_regs *ctx, size_t size) {
    struct key_t key = {};
    struct val_t *valp, zero = {};
    key.size = size;
    valp = allocs.lookup_or_try_init(&key, &zero);
    if (valp) {
        valp->total_size += size;
        valp->num_allocs += 1;
    }
    return 0;
}
    """
#
# Java
#
elif language == "java":
    program += """
int alloc_entry(struct pt_regs *ctx) {
    struct key_t key = {};
    struct val_t *valp, zero = {};
    u64 classptr = 0, size = 0;
    bpf_usdt_readarg(2, ctx, &classptr);
    bpf_usdt_readarg(4, ctx, &size);
    bpf_probe_read_user(&key.name, sizeof(key.name), (void *)classptr);
    valp = allocs.lookup_or_try_init(&key, &zero);
    if (valp) {
        valp->total_size += size;
        valp->num_allocs += 1;
    }
    return 0;
}
    """
    usdt.enable_probe_or_bail("object__alloc", "alloc_entry")
#
# Ruby
#
elif language == "ruby":
    create_template = """
int THETHING_alloc_entry(struct pt_regs *ctx) {
    struct key_t key = { .name = "THETHING" };
    struct val_t *valp, zero = {};
    u64 size = 0;
    bpf_usdt_readarg(1, ctx, &size);
    valp = allocs.lookup_or_try_init(&key, &zero);
    if (valp) {
        valp->total_size += size;
        valp->num_allocs += 1;
    }
    return 0;
}
    """
    program += """
int object_alloc_entry(struct pt_regs *ctx) {
    struct key_t key = {};
    struct val_t *valp, zero = {};
    u64 classptr = 0;
    bpf_usdt_readarg(1, ctx, &classptr);
    bpf_probe_read_user(&key.name, sizeof(key.name), (void *)classptr);
    valp = allocs.lookup_or_try_init(&key, &zero);
    if (valp) {
        valp->num_allocs += 1;  // We don't know the size, unfortunately
    }
    return 0;
}
    """
    usdt.enable_probe_or_bail("object__create", "object_alloc_entry")
    for thing in ["string", "hash", "array"]:
        program += create_template.replace("THETHING", thing)
        usdt.enable_probe_or_bail("%s__create" % thing,
                                  "%s_alloc_entry" % thing)
#
# Tcl
#
elif language == "tcl":
    program += """
int alloc_entry(struct pt_regs *ctx) {
    struct key_t key = { .name = "<ALL>" };
    struct val_t *valp, zero = {};
    valp = allocs.lookup_or_try_init(&key, &zero);
    if (valp) {
        valp->num_allocs += 1;
    }
    return 0;
}
    """
    usdt.enable_probe_or_bail("obj__create", "alloc_entry")
else:
    print("No language detected; use -l to trace a language.")
    exit(1)


if args.ebpf or args.verbose:
    if args.verbose:
        print(usdt.get_text())
    print(program)
    if args.ebpf:
        exit()

bpf = BPF(text=program, usdt_contexts=[usdt])
if language == "c":
    bpf.attach_uprobe(name="c", sym="malloc", fn_name="alloc_entry",
                      pid=args.pid)

exit_signaled = False
print("Tracing allocations in process %d (language: %s)... Ctrl-C to quit." %
      (args.pid, language or "none"))
while True:
    try:
        sleep(args.interval or 99999999)
    except KeyboardInterrupt:
        exit_signaled = True
    print()
    data = bpf["allocs"]
    if args.top_count:
        data = sorted(data.items(), key=lambda kv: kv[1].num_allocs)
        data = data[-args.top_count:]
    elif args.top_size:
        data = sorted(data.items(), key=lambda kv: kv[1].total_size)
        data = data[-args.top_size:]
    else:
        data = sorted(data.items(), key=lambda kv: kv[1].total_size)
    print("%-30s %8s %12s" % ("NAME/TYPE", "# ALLOCS", "# BYTES"))
    for key, value in data:
        if language == "c":
            obj_type = "block size %d" % key.size
        else:
            obj_type = key.name
        print("%-30s %8d %12d" %
              (obj_type, value.num_allocs, value.total_size))
    if args.interval and not exit_signaled:
        bpf["allocs"].clear()
    else:
        exit()
