# report time spent in compaction
# Licensed under the terms of the GNU GPL License version 2

# testing:
# 'echo 1 > /proc/sys/vm/compact_memory' to force compaction of all zones

import os
import sys
import re

import signal
signal.signal(signal.SIGPIPE, signal.SIG_DFL)

usage = "usage: perf script report compaction-times.py -- [-h] [-u] [-p|-pv] [-t | [-m] [-fs] [-ms]] [pid|pid-range|comm-regex]\n"

class popt:
	DISP_DFL = 0
	DISP_PROC = 1
	DISP_PROC_VERBOSE=2

class topt:
	DISP_TIME = 0
	DISP_MIG = 1
	DISP_ISOLFREE = 2
	DISP_ISOLMIG = 4
	DISP_ALL = 7

class comm_filter:
	def __init__(self, re):
		self.re = re

	def filter(self, pid, comm):
		m = self.re.search(comm)
		return m == None or m.group() == ""

class pid_filter:
	def __init__(self, low, high):
		self.low = (0 if low == "" else int(low))
		self.high = (0 if high == "" else int(high))

	def filter(self, pid, comm):
		return not (pid >= self.low and (self.high == 0 or pid <= self.high))

def set_type(t):
	global opt_disp
	opt_disp = (t if opt_disp == topt.DISP_ALL else opt_disp|t)

def ns(sec, nsec):
	return (sec * 1000000000) + nsec

def time(ns):
	return "%dns" % ns if opt_ns else "%dus" % (round(ns, -3) / 1000)

class pair:
	def __init__(self, aval, bval, alabel = None, blabel = None):
		self.alabel = alabel
		self.blabel = blabel
		self.aval = aval
		self.bval = bval

	def __add__(self, rhs):
		self.aval += rhs.aval
		self.bval += rhs.bval
		return self

	def __str__(self):
		return "%s=%d %s=%d" % (self.alabel, self.aval, self.blabel, self.bval)

class cnode:
	def __init__(self, ns):
		self.ns = ns
		self.migrated = pair(0, 0, "moved", "failed")
		self.fscan = pair(0,0, "scanned", "isolated")
		self.mscan = pair(0,0, "scanned", "isolated")

	def __add__(self, rhs):
		self.ns += rhs.ns
		self.migrated += rhs.migrated
		self.fscan += rhs.fscan
		self.mscan += rhs.mscan
		return self

	def __str__(self):
		prev = 0
		s = "%s " % time(self.ns)
		if (opt_disp & topt.DISP_MIG):
			s += "migration: %s" % self.migrated
			prev = 1
		if (opt_disp & topt.DISP_ISOLFREE):
			s += "%sfree_scanner: %s" % (" " if prev else "", self.fscan)
			prev = 1
		if (opt_disp & topt.DISP_ISOLMIG):
			s += "%smigration_scanner: %s" % (" " if prev else "", self.mscan)
		return s

	def complete(self, secs, nsecs):
		self.ns = ns(secs, nsecs) - self.ns

	def increment(self, migrated, fscan, mscan):
		if (migrated != None):
			self.migrated += migrated
		if (fscan != None):
			self.fscan += fscan
		if (mscan != None):
			self.mscan += mscan


class chead:
	heads = {}
	val = cnode(0);
	fobj = None

	@classmethod
	def add_filter(cls, filter):
		cls.fobj = filter

	@classmethod
	def create_pending(cls, pid, comm, start_secs, start_nsecs):
		filtered = 0
		try:
			head = cls.heads[pid]
			filtered = head.is_filtered()
		except KeyError:
			if cls.fobj != None:
				filtered = cls.fobj.filter(pid, comm)
			head = cls.heads[pid] = chead(comm, pid, filtered)

		if not filtered:
			head.mark_pending(start_secs, start_nsecs)

	@classmethod
	def increment_pending(cls, pid, migrated, fscan, mscan):
		head = cls.heads[pid]
		if not head.is_filtered():
			if head.is_pending():
				head.do_increment(migrated, fscan, mscan)
			else:
				sys.stderr.write("missing start compaction event for pid %d\n" % pid)

	@classmethod
	def complete_pending(cls, pid, secs, nsecs):
		head = cls.heads[pid]
		if not head.is_filtered():
			if head.is_pending():
				head.make_complete(secs, nsecs)
			else:
				sys.stderr.write("missing start compaction event for pid %d\n" % pid)

	@classmethod
	def gen(cls):
		if opt_proc != popt.DISP_DFL:
			for i in cls.heads:
				yield cls.heads[i]

	@classmethod
	def str(cls):
		return cls.val

	def __init__(self, comm, pid, filtered):
		self.comm = comm
		self.pid = pid
		self.val = cnode(0)
		self.pending = None
		self.filtered = filtered
		self.list = []

	def __add__(self, rhs):
		self.ns += rhs.ns
		self.val += rhs.val
		return self

	def mark_pending(self, secs, nsecs):
		self.pending = cnode(ns(secs, nsecs))

	def do_increment(self, migrated, fscan, mscan):
		self.pending.increment(migrated, fscan, mscan)

	def make_complete(self, secs, nsecs):
		self.pending.complete(secs, nsecs)
		chead.val += self.pending

		if opt_proc != popt.DISP_DFL:
			self.val += self.pending

			if opt_proc == popt.DISP_PROC_VERBOSE:
				self.list.append(self.pending)
		self.pending = None

	def enumerate(self):
		if opt_proc == popt.DISP_PROC_VERBOSE and not self.is_filtered():
			for i, pelem in enumerate(self.list):
				sys.stdout.write("%d[%s].%d: %s\n" % (self.pid, self.comm, i+1, pelem))

	def is_pending(self):
		return self.pending != None

	def is_filtered(self):
		return self.filtered

	def display(self):
		if not self.is_filtered():
			sys.stdout.write("%d[%s]: %s\n" % (self.pid, self.comm, self.val))


def trace_end():
	sys.stdout.write("total: %s\n" % chead.str())
	for i in chead.gen():
		i.display(),
		i.enumerate()

def compaction__mm_compaction_migratepages(event_name, context, common_cpu,
	common_secs, common_nsecs, common_pid, common_comm,
	common_callchain, nr_migrated, nr_failed):

	chead.increment_pending(common_pid,
		pair(nr_migrated, nr_failed), None, None)

def compaction__mm_compaction_isolate_freepages(event_name, context, common_cpu,
	common_secs, common_nsecs, common_pid, common_comm,
	common_callchain, start_pfn, end_pfn, nr_scanned, nr_taken):

	chead.increment_pending(common_pid,
		None, pair(nr_scanned, nr_taken), None)

def compaction__mm_compaction_isolate_migratepages(event_name, context, common_cpu,
	common_secs, common_nsecs, common_pid, common_comm,
	common_callchain, start_pfn, end_pfn, nr_scanned, nr_taken):

	chead.increment_pending(common_pid,
		None, None, pair(nr_scanned, nr_taken))

def compaction__mm_compaction_end(event_name, context, common_cpu,
	common_secs, common_nsecs, common_pid, common_comm,
	common_callchain, zone_start, migrate_start, free_start, zone_end,
	sync, status):

	chead.complete_pending(common_pid, common_secs, common_nsecs)

def compaction__mm_compaction_begin(event_name, context, common_cpu,
	common_secs, common_nsecs, common_pid, common_comm,
	common_callchain, zone_start, migrate_start, free_start, zone_end,
	sync):

	chead.create_pending(common_pid, common_comm, common_secs, common_nsecs)

def pr_help():
	global usage

	sys.stdout.write(usage)
	sys.stdout.write("\n")
	sys.stdout.write("-h	display this help\n")
	sys.stdout.write("-p	display by process\n")
	sys.stdout.write("-pv	display by process (verbose)\n")
	sys.stdout.write("-t	display stall times only\n")
	sys.stdout.write("-m	display stats for migration\n")
	sys.stdout.write("-fs	display stats for free scanner\n")
	sys.stdout.write("-ms	display stats for migration scanner\n")
	sys.stdout.write("-u	display results in microseconds (default nanoseconds)\n")


comm_re = None
pid_re = None
pid_regex = r"^(\d*)-(\d*)$|^(\d*)$"

opt_proc = popt.DISP_DFL
opt_disp = topt.DISP_ALL

opt_ns = True

argc = len(sys.argv) - 1
if argc >= 1:
	pid_re = re.compile(pid_regex)

	for i, opt in enumerate(sys.argv[1:]):
		if opt[0] == "-":
			if opt == "-h":
				pr_help()
				exit(0);
			elif opt == "-p":
				opt_proc = popt.DISP_PROC
			elif opt == "-pv":
				opt_proc = popt.DISP_PROC_VERBOSE
			elif opt == '-u':
				opt_ns = False
			elif opt == "-t":
				set_type(topt.DISP_TIME)
			elif opt == "-m":
				set_type(topt.DISP_MIG)
			elif opt == "-fs":
				set_type(topt.DISP_ISOLFREE)
			elif opt == "-ms":
				set_type(topt.DISP_ISOLMIG)
			else:
				sys.exit(usage)

		elif i == argc - 1:
			m = pid_re.search(opt)
			if m != None and m.group() != "":
				if m.group(3) != None:
					f = pid_filter(m.group(3), m.group(3))
				else:
					f = pid_filter(m.group(1), m.group(2))
			else:
				try:
					comm_re=re.compile(opt)
				except:
					sys.stderr.write("invalid regex '%s'" % opt)
					sys.exit(usage)
				f = comm_filter(comm_re)

			chead.add_filter(f)
