/* 2009-01-09: File added and changed by Sony Corporation */
#include <linux/kernel.h>
#include <linux/list.h>
#include <linux/init.h>
#include <linux/spinlock.h>
#include <linux/sched.h>
#include <linux/module.h>
#include <linux/seq_file.h>
#include <linux/fs.h>
#include <linux/proc_fs.h>
#include <linux/preempt.h>

#include <asm/percpu.h>
#include <asm/uaccess.h>

#define MAX_NUM CONFIG_SNSC_LATENCY_NUM

#define LT_IRQ   (&__get_cpu_var(latencies_irq))
#define LT_PRMPT (&__get_cpu_var(latencies_prmpt))
#define LT_INITED(lt) (lt->bl)

#define FNAME_SIZE 32

typedef struct record {
	char             fname[FNAME_SIZE]; /* last byte should be 0 */
#if defined(CONFIG_SNSC_LATENCY_UNIQUIFIED)
	const char       *fname_addr;
#endif
	u64              interval;
	int              irq;
	struct list_head list;
}record_t;

typedef struct _latency_type {
	record_t         cur;
	record_t         records[MAX_NUM];

	spinlock_t       lock;
	void             *filter;
	struct list_head head;
	int              idx;
	const char       **bl;
} latency_t;

static int check_blacklist(latency_t *lt, const char *func)
{
	const char **p = lt->bl;
	while (*p) {
		if (strcmp(func, *p) == 0)
			return 1;

		p ++;
	}

	return 0;
}

#define CUR(lt) (&lt->cur)
static void latency_off(latency_t *lt, const char *func, int irq, void *caller)
{
	record_t *cur = CUR(lt);

	if (unlikely(oops_in_progress))
		return;
#if defined(CONFIG_SNSC_LATENCY_UNIQUIFIED)
	if (unlikely(cur->fname_addr != NULL))
#else
	if (unlikely(cur->fname[0]))
#endif
		return;

	if (lt->filter && lt->filter < caller)
		return;
	if (check_blacklist(lt, func))
		return;

	cur->interval = sched_clock();
#if defined(CONFIG_SNSC_LATENCY_UNIQUIFIED)
	cur->fname_addr = func;
#else
	strncpy(cur->fname, func, FNAME_SIZE - 1);
#endif
	cur->irq = irq;
}

#if defined(CONFIG_SNSC_LATENCY_UNIQUIFIED)
static record_t *addr_hash(latency_t *lt, const char *fname_addr)
{
	int i = 0, idx = 0;
	idx = ((unsigned long)fname_addr >> 4) % MAX_NUM;
	while (i++ < MAX_NUM){
		if ( lt->records[idx].fname_addr == fname_addr ||
				lt->records[idx].fname_addr == NULL) {
			return &lt->records[idx];
		}
		idx ++;
		idx %= MAX_NUM;
	}
	return NULL;
}
static void latency_on(latency_t *lt, const char *func, int irq, void *caller)
{
	record_t *cur = CUR(lt);
	record_t *tgt, *tmp;
	unsigned long flag;
	unsigned long long sched_clock_val;

	if (unlikely(cur->fname_addr == NULL))
		return;

	raw_local_irq_save(flag);
	spin_lock(&lt->lock);
	sched_clock_val = sched_clock();
	if (sched_clock_val >= cur->interval)
		cur->interval = sched_clock_val - cur->interval;
	/* This need to be reconsidered.
	 * It's just for those machines whose sched_clock() depends on
	 * jiffies.
	 * jiffies will overflow after about 5 min since boot.
	 */
	else
		cur->interval = ((u64)1<<32) * (u64)tick_nsec - (cur->interval - sched_clock_val);

	/* if less than the smallest and record is full, no need to go on */
	if ( lt->idx >= MAX_NUM &&
			cur->interval <= list_entry(lt->head.prev, record_t, list)->interval) {
		goto out;
	}

	tgt = addr_hash(lt, cur->fname_addr);

	if (tgt == NULL) { /* no empty entry left */
		tgt = list_entry(lt->head.prev, record_t, list);

		list_del(&tgt->list);
		*tgt = *cur;
		INIT_LIST_HEAD(&tgt->list);

	}
	else if (tgt->fname_addr == NULL) { /* empty entry in records found */
		*tgt = *cur;
		INIT_LIST_HEAD(&tgt->list);
		lt->idx++;
		if (list_empty(&lt->head)) {
			strncpy(tgt->fname, cur->fname_addr, FNAME_SIZE - 1);
			list_add(&tgt->list, &lt->head);
			goto out;
		}
	}
	else if ( tgt->fname_addr == cur->fname_addr) { /* entry of the same name found */
		if (tgt->interval >= cur->interval ) /* no need to change */
			goto out;
		else {
			tgt->interval = cur->interval;
			tgt->irq = cur->irq;
			tmp = list_entry(tgt->list.prev, record_t, list);
			if (tgt->interval <= tmp->interval) /* no need to change again */
				goto out;
			else {		/* resort the list */
				list_del(&tgt->list);
				INIT_LIST_HEAD(&tgt->list);
			}
		}
	}

	strncpy(tgt->fname, cur->fname_addr, FNAME_SIZE - 1);
	list_for_each_entry_reverse(tmp, &lt->head, list) {
		if (tmp->interval > tgt->interval) {
			list_add(&tgt->list, &tmp->list);
			goto out;
		}
	}

	list_add(&tgt->list, &lt->head);
out:
	cur->interval = 0;
	cur->fname[0] = '\0';
	cur->irq = 0;
	cur->fname_addr = NULL;
	spin_unlock(&lt->lock);
	raw_local_irq_restore(flag);
}

#else

static void latency_on(latency_t *lt, const char *func, int irq, void *caller)
{
	record_t *cur = CUR(lt);
	record_t *tgt, *tmp;
	unsigned long flag;

	if (unlikely(!cur->fname[0]))
		return;

	raw_local_irq_save(flag);
	spin_lock(&lt->lock);

	cur->interval = sched_clock() - cur->interval;

	if (lt->idx < MAX_NUM) {
		tgt = &lt->records[lt->idx ++];
	}
	else {
		tgt = list_entry(lt->head.prev, record_t, list);
		if (tgt->interval >= cur->interval)
			goto out;

		list_del(&tgt->list);
	}

	*tgt = *cur;
	INIT_LIST_HEAD(&tgt->list);

	if (list_empty(&lt->head)) {
		list_add(&tgt->list, &lt->head);
		goto out;
	}

	list_for_each_entry_reverse(tmp, &lt->head, list) {
		if (tmp->interval > tgt->interval) {
			list_add(&tgt->list, &tmp->list);
			goto out;
		}
	}

	list_add(&tgt->list, &lt->head);
out:
	cur->interval = 0;
	cur->fname[0] = 0;
	cur->irq = 0;

	spin_unlock(&lt->lock);
	raw_local_irq_restore(flag);
}
#endif

static void *latency_start(latency_t *lt, struct seq_file *m, loff_t *pos)
{
	loff_t n = *pos;

	if (!n) {
		record_t *tmp;
		unsigned long flag;

		raw_local_irq_save(flag);
		spin_lock(&lt->lock);
		list_for_each_entry(tmp, &lt->head, list) {
			unsigned long long t;
			unsigned long nanosec_rem;

			t = tmp->interval;
			nanosec_rem = do_div(t, 1000000000)/1000;
			if (tmp->irq >= 0)
				seq_printf(m, "%5llu.%06lu  %s  %d\n", t, nanosec_rem, tmp->fname, tmp->irq);
			else
				seq_printf(m, "%5llu.%06lu  %s\n", t, nanosec_rem, tmp->fname);
		}
		spin_unlock(&lt->lock);
		raw_local_irq_restore(flag);
	}
	return (void *)1;
}

static void *latency_next(struct seq_file *m, void *p, loff_t *pos)
{
	++ *pos;
	return 0;
}

static void latency_stop(struct seq_file *m, void *p)
{
}

static int latency_show(struct seq_file *m, void *p)
{
	return 0;
}

static ssize_t latency_write(latency_t *lt, struct file *f, const char __user *buf, size_t size, loff_t *pos)
{
	unsigned long flag;
	char b[20];
#if defined(CONFIG_SNSC_LATENCY_UNIQUIFIED)
	int i;
	for (i=0; i<MAX_NUM; i++)
		lt->records[i].fname_addr = NULL;
#endif
	if (!copy_from_user(b, buf, size>20?20:size)) {
		lt->filter = (void *)simple_strtoul(b, NULL, 0);
	}

	raw_local_irq_save(flag);
	spin_lock(&lt->lock);
	lt->idx = 0;
	INIT_LIST_HEAD(&lt->head);
	spin_unlock(&lt->lock);
	raw_local_irq_restore(flag);

	return size;
}

static void create_seq_entry(char *name, mode_t mode, struct file_operations *f)
{
	struct proc_dir_entry *entry;
	entry = create_proc_entry(name, mode, NULL);
	if (entry)
		entry->proc_fops = f;
}

static void init_latency(latency_t *lt, const char **bl)
{
	if (unlikely(!LT_INITED(lt))) {
		lt->bl = bl;
		INIT_LIST_HEAD(&lt->head);
		spin_lock_init(&lt->lock);
	}
}

#ifdef CONFIG_SNSC_IRQ_LATENCY
static const char *irq_blacklist[] = {
	"default_idle",
	"preempt_schedule_irq",
	NULL
};

static DEFINE_PER_CPU(latency_t, latencies_irq);

static void *irqlatency_start(struct seq_file *m, loff_t *pos)
{
	loff_t n = *pos;

	if (!n) {
		int i;
		for_each_online_cpu(i) {
			latency_t *lt;
			seq_printf(m, "irq latency worst %d @ CPU %d\n", MAX_NUM, i);
			lt = &per_cpu(latencies_irq, i);
			if (LT_INITED(lt))
				latency_start(lt, m, pos);
		}
	}

	return (void *)1;
}

static struct seq_operations irqlatency_op = {
	.start	= irqlatency_start,
	.next	= latency_next,
	.stop	= latency_stop,
	.show	= latency_show,
};

static int irqlatency_open(struct inode *inode, struct file *file)
{
	return seq_open(file, &irqlatency_op);
}

static ssize_t irqlatency_write(struct file *f, const char __user *buf, size_t size, loff_t *pos)
{
	int i;
	for_each_online_cpu(i) {
		latency_t *lt;
		lt = &per_cpu(latencies_irq, i);
		if (LT_INITED(lt))
			latency_write(lt, f, buf, size, pos);
	}

	return size;
}

static struct file_operations irqlatency_operations = {
	.open		= irqlatency_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= seq_release,
	.write          = irqlatency_write,
};

void irqlatency_irq_off(const char *func, int irq)
{
	init_latency(LT_IRQ, irq_blacklist);
	latency_off(LT_IRQ, func, irq, __builtin_return_address(0));
}

void irqlatency_irq_on(const char *func, int irq)
{
	init_latency(LT_IRQ, irq_blacklist);
	latency_on(LT_IRQ, func, irq, __builtin_return_address(0));
}

EXPORT_SYMBOL(irqlatency_irq_off);
EXPORT_SYMBOL(irqlatency_irq_on);

#endif

#ifdef CONFIG_SNSC_PREEMPTION_LATENCY
static const char *prmpt_blacklist[] = {
	"cpu_idle",
	NULL
};

DEFINE_PER_CPU(latency_t, latencies_prmpt);

static void *prelatency_start(struct seq_file *m, loff_t *pos)
{
	loff_t n = *pos;

	if (!n) {
		int i;
		for_each_online_cpu(i) {
			latency_t *lt;
			seq_printf(m, "preemption latency worst %d @ CPU %d\n", MAX_NUM, i);
			lt = &per_cpu(latencies_prmpt, i);
			if (LT_INITED(lt))
				latency_start(lt, m, pos);
		}
	}

	return (void *)1;
}

static struct seq_operations prelatency_op = {
	.start	= prelatency_start,
	.next	= latency_next,
	.stop	= latency_stop,
	.show	= latency_show,
};

static int prelatency_open(struct inode *inode, struct file *file)
{
	return seq_open(file, &prelatency_op);
}

static ssize_t prelatency_write(struct file *f, const char __user *buf, size_t size, loff_t *pos)
{
	int i;
	for_each_online_cpu(i) {
		latency_t *lt;
		lt = &per_cpu(latencies_prmpt, i);
		if (LT_INITED(lt))
			latency_write(lt, f, buf, size, pos);
	}

	return size;
}

static struct file_operations prelatency_operations = {
	.open		= prelatency_open,
	.read		= seq_read,
	.llseek		= seq_lseek,
	.release	= seq_release,
	.write          = prelatency_write,
};

void prelatency_off(const char *func, void *caller)
{
	init_latency(LT_PRMPT, prmpt_blacklist);
	latency_off(LT_PRMPT, func, -1, caller);
}

void prelatency_on(const char *func, void *caller)
{
	init_latency(LT_PRMPT, prmpt_blacklist);
	latency_on(LT_PRMPT, func, -1, caller);
}

EXPORT_SYMBOL(prelatency_off);
EXPORT_SYMBOL(prelatency_on);

#endif

static int __init irqlatency_init(void)
{
#ifdef CONFIG_SNSC_IRQ_LATENCY
	create_seq_entry("irq_latency", S_IRUGO, &irqlatency_operations);
#endif
#ifdef CONFIG_SNSC_PREEMPTION_LATENCY
	create_seq_entry("preemption_latency", S_IRUGO, &prelatency_operations);
#endif
	return 0;
}

late_initcall(irqlatency_init);
