#!/usr/bin/env python
#
# llcstat.py Summarize cache references and cache misses by PID.
#            Cache reference and cache miss are corresponding events defined in
#            uapi/linux/perf_event.h, it varies to different architecture.
#            On x86-64, they mean LLC references and LLC misses.
#
#            For Linux, uses BCC, eBPF. Embedded C.
#
# SEE ALSO: perf top -e cache-misses -e cache-references -a -ns pid,cpu,comm
#
# REQUIRES: Linux 4.9+ (BPF_PROG_TYPE_PERF_EVENT support).
#
# Copyright (c) 2016 Facebook, Inc.
# Licensed under the Apache License, Version 2.0 (the "License")
#
# 19-Oct-2016   Teng Qin   Created this.
# 20-Jun-2022   YeZhengMao Added tid info.

from __future__ import print_function
import argparse
from bcc import BPF, PerfType, PerfHWConfig
import signal
from time import sleep

parser = argparse.ArgumentParser(
    description="Summarize cache references and misses by PID",
    formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument(
    "-c", "--sample_period", type=int, default=100,
    help="Sample one in this many number of cache reference / miss events")
parser.add_argument(
    "duration", nargs="?", default=10, help="Duration, in seconds, to run")
parser.add_argument(
    "-t", "--tid", action="store_true",
    help="Summarize cache references and misses by PID/TID"
)
parser.add_argument("--ebpf", action="store_true",
    help=argparse.SUPPRESS)
args = parser.parse_args()

# load BPF program
bpf_text="""
#include <linux/ptrace.h>
#include <uapi/linux/bpf_perf_event.h>

struct key_t {
    int cpu;
    u32 pid;
    u32 tid;
    char name[TASK_COMM_LEN];
};

BPF_HASH(ref_count, struct key_t);
BPF_HASH(miss_count, struct key_t);

static inline __attribute__((always_inline)) void get_key(struct key_t* key) {
    u64 pid_tgid = bpf_get_current_pid_tgid();
    key->cpu = bpf_get_smp_processor_id();
    key->pid = pid_tgid >> 32;
    key->tid = GET_TID ? (u32)pid_tgid : key->pid;
    bpf_get_current_comm(&(key->name), sizeof(key->name));
}

int on_cache_miss(struct bpf_perf_event_data *ctx) {
    struct key_t key = {};
    get_key(&key);

    miss_count.increment(key, ctx->sample_period);

    return 0;
}

int on_cache_ref(struct bpf_perf_event_data *ctx) {
    struct key_t key = {};
    get_key(&key);

    ref_count.increment(key, ctx->sample_period);

    return 0;
}
"""

bpf_text = bpf_text.replace("GET_TID", "1" if args.tid else "0")

if args.ebpf:
    print(bpf_text)
    exit()

b = BPF(text=bpf_text)
try:
    b.attach_perf_event(
        ev_type=PerfType.HARDWARE, ev_config=PerfHWConfig.CACHE_MISSES,
        fn_name="on_cache_miss", sample_period=args.sample_period)
    b.attach_perf_event(
        ev_type=PerfType.HARDWARE, ev_config=PerfHWConfig.CACHE_REFERENCES,
        fn_name="on_cache_ref", sample_period=args.sample_period)
except Exception:
    print("Failed to attach to a hardware event. Is this a virtual machine?")
    exit()

print("Running for {} seconds or hit Ctrl-C to end.".format(args.duration))

try:
    sleep(float(args.duration))
except KeyboardInterrupt:
    signal.signal(signal.SIGINT, lambda signal, frame: print())

miss_count = {}
for (k, v) in b.get_table('miss_count').items():
    if args.tid:
        miss_count[(k.pid, k.tid, k.cpu, k.name)] = v.value
    else:
        miss_count[(k.pid, k.cpu, k.name)] = v.value

header_text = 'PID      '
format_text = '{:<8d} '
if args.tid:
    header_text += 'TID      '
    format_text += '{:<8d} '

header_text += 'NAME             CPU     REFERENCE         MISS    HIT%'
format_text += '{:<16s} {:<4d} {:>12d} {:>12d} {:>6.2f}%'

print(header_text)
tot_ref = 0
tot_miss = 0
for (k, v) in b.get_table('ref_count').items():
    try:
        if args.tid:
            miss = miss_count[(k.pid, k.tid, k.cpu, k.name)]
        else:
            miss = miss_count[(k.pid, k.cpu, k.name)]
    except KeyError:
        miss = 0
    tot_ref += v.value
    tot_miss += miss
    # This happens on some PIDs due to missed counts caused by sampling
    hit = (v.value - miss) if (v.value >= miss) else 0
    if args.tid:
        print(format_text.format(
            k.pid, k.tid, k.name.decode('utf-8', 'replace'), k.cpu, v.value, miss,
            (float(hit) / float(v.value)) * 100.0))
    else:
        print(format_text.format(
            k.pid, k.name.decode('utf-8', 'replace'), k.cpu, v.value, miss,
            (float(hit) / float(v.value)) * 100.0))
print('Total References: {} Total Misses: {} Hit Rate: {:.2f}%'.format(
    tot_ref, tot_miss, (float(tot_ref - tot_miss) / float(tot_ref)) * 100.0))
