From 7ee5becf00110913a10b2a7b78f3496e7ba33649 Mon Sep 17 00:00:00 2001 From: exer <98556122+ekkusa@users.noreply.github.com> Date: Mon, 16 Jan 2023 18:43:32 +0000 Subject: [PATCH] kernel/power: Import Sony Wakelock *52.1.A.3.74 Signed-off-by: Sorayukii --- drivers/base/power/main.c | 103 +++++++++ include/linux/pm.h | 5 + include/linux/suspend.h | 28 +++ kernel/power/Kconfig | 28 +++ kernel/power/Makefile | 1 + kernel/power/main.c | 294 +++++++++++++++++++++++++- kernel/power/suspend.c | 13 +- kernel/power/wakeup_irq_debug.c | 357 ++++++++++++++++++++++++++++++++ 8 files changed, 825 insertions(+), 4 deletions(-) create mode 100644 kernel/power/wakeup_irq_debug.c diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c index 9b66c0b53b09..de12783a5e92 100644 --- a/drivers/base/power/main.c +++ b/drivers/base/power/main.c @@ -34,6 +34,10 @@ #include #include #include +#ifdef CONFIG_PM_WAKEUP_TIMES +#include +#include +#endif #include "../base.h" #include "power.h" @@ -57,6 +61,11 @@ static LIST_HEAD(dpm_late_early_list); static LIST_HEAD(dpm_noirq_list); struct suspend_stats suspend_stats; +#ifdef CONFIG_PM_WAKEUP_TIMES +struct suspend_stats_queue suspend_stats_queue; +static ktime_t suspend_start_time; +static ktime_t resume_start_time; +#endif static DEFINE_MUTEX(dpm_list_mtx); static pm_message_t pm_transition; @@ -383,6 +392,100 @@ static void dpm_show_time(ktime_t starttime, pm_message_t state, char *info) usecs / USEC_PER_MSEC, usecs % USEC_PER_MSEC); } +#ifdef CONFIG_PM_WAKEUP_TIMES +void dpm_log_start_time(pm_message_t state) +{ + switch (state.event) { + case PM_EVENT_RESUME: + resume_start_time = ktime_get_boottime(); + break; + case PM_EVENT_SUSPEND: + suspend_start_time = ktime_get_boottime(); + break; + default: + break; + } +} +EXPORT_SYMBOL_GPL(dpm_log_start_time); + +void dpm_log_wakeup_stats(pm_message_t state) +{ + ktime_t *start_time, *avg_time, end_time, duration, prev_duration, sum; + struct stats_wakeup_time *min_time, *max_time, *last_time, prev; + u64 avg_ns; + char buf[32] = {0}; + unsigned int nr = 0; + + switch (state.event) { + case PM_EVENT_RESUME: + snprintf(buf, sizeof(buf), "%s", "resume time:"); + start_time = &resume_start_time; + min_time = &suspend_stats.resume_min_time; + max_time = &suspend_stats.resume_max_time; + last_time = &suspend_stats.resume_last_time; + avg_time = &suspend_stats.resume_avg_time; + break; + case PM_EVENT_SUSPEND: + snprintf(buf, sizeof(buf), "%s", "suspend time:"); + start_time = &suspend_start_time; + min_time = &suspend_stats.suspend_min_time; + max_time = &suspend_stats.suspend_max_time; + last_time = &suspend_stats.suspend_last_time; + avg_time = &suspend_stats.suspend_avg_time; + break; + default: + return; + } + + if (!ktime_to_ns(*start_time)) + return; + + /* Calculate duration and update last time */ + end_time = ktime_get_boottime(); + prev = *last_time; + prev_duration = ktime_sub(prev.end, prev.start); + last_time->end = end_time; + last_time->start = *start_time; + duration = ktime_sub(end_time, *start_time); + + /* Update max time */ + if (ktime_compare(duration, + ktime_sub(max_time->end, max_time->start)) > 0) + *max_time = *last_time; + + /* Update min time */ + if (!ktime_to_ns(ktime_sub(min_time->end, min_time->start))) + *min_time = *last_time; + + if (ktime_compare(duration, + ktime_sub(min_time->end, min_time->start)) < 0) + *min_time = *last_time; + + /* Compute the avg of current, previous and previous average times */ + if (ktime_to_ns(prev_duration)) + nr++; + + if (ktime_to_ns(*avg_time)) + nr++; + + sum = ktime_add(ktime_add(*avg_time, prev_duration), duration); + avg_ns = div_u64(ktime_to_ns(sum), (nr + 1)); + *avg_time = ktime_set(0, avg_ns); + *start_time = ktime_set(0, 0); + + pr_debug("%s\n%s %llums\n%s %llums\n %s %llums\n%s %llums\n", buf, + " min:", + ktime_to_ms(ktime_sub(min_time->end, min_time->start)), + " max:", + ktime_to_ms(ktime_sub(max_time->end, max_time->start)), + " last:", ktime_to_ms(duration), + " avg:", ktime_to_ms(*avg_time)); + suspend_stats_queue.resume_done = 1; + wake_up(&suspend_stats_queue.wait_queue); +} +EXPORT_SYMBOL_GPL(dpm_log_wakeup_stats); +#endif + static int dpm_run_callback(pm_callback_t cb, struct device *dev, pm_message_t state, char *info) { diff --git a/include/linux/pm.h b/include/linux/pm.h index 278f4cf02a43..b4309dcf343a 100644 --- a/include/linux/pm.h +++ b/include/linux/pm.h @@ -704,6 +704,11 @@ extern int dpm_suspend_late(pm_message_t state); extern int dpm_suspend(pm_message_t state); extern int dpm_prepare(pm_message_t state); +#ifdef CONFIG_PM_WAKEUP_TIMES +extern void dpm_log_start_time(pm_message_t state); +extern void dpm_log_wakeup_stats(pm_message_t state); +#endif + extern void __suspend_report_result(const char *function, void *fn, int ret); #define suspend_report_result(fn, ret) \ diff --git a/include/linux/suspend.h b/include/linux/suspend.h index 90d85690ab7b..274746996c82 100644 --- a/include/linux/suspend.h +++ b/include/linux/suspend.h @@ -7,6 +7,10 @@ #include #include #include +#ifdef CONFIG_PM_WAKEUP_TIMES +#include +#include +#endif #include #ifdef CONFIG_VT @@ -50,6 +54,20 @@ enum suspend_stat_step { SUSPEND_RESUME }; +#ifdef CONFIG_PM_WAKEUP_TIMES +struct stats_wakeup_time { + ktime_t start; + ktime_t end; +}; + +struct suspend_stats_queue { + int resume_done; + wait_queue_head_t wait_queue; +}; + +extern struct suspend_stats_queue suspend_stats_queue; +#endif + struct suspend_stats { int success; int fail; @@ -68,6 +86,16 @@ struct suspend_stats { int errno[REC_FAILED_NUM]; int last_failed_step; enum suspend_stat_step failed_steps[REC_FAILED_NUM]; +#ifdef CONFIG_PM_WAKEUP_TIMES + struct stats_wakeup_time suspend_min_time; + struct stats_wakeup_time suspend_max_time; + struct stats_wakeup_time suspend_last_time; + ktime_t suspend_avg_time; + struct stats_wakeup_time resume_min_time; + struct stats_wakeup_time resume_max_time; + struct stats_wakeup_time resume_last_time; + ktime_t resume_avg_time; +#endif }; extern struct suspend_stats suspend_stats; diff --git a/kernel/power/Kconfig b/kernel/power/Kconfig index fac423faa397..bbe7539f974a 100644 --- a/kernel/power/Kconfig +++ b/kernel/power/Kconfig @@ -29,6 +29,15 @@ config SUSPEND_SKIP_SYNC of suspend, or they are content with invoking sync() from user-space before invoking suspend. Say Y if that's your case. +config WAKELOCK + bool "Android's method of preventing suspend" + default y + ---help--- + This allows applications to prevent the CPU from suspending while + they need it. + + Say Y if you are running an android userspace. + config HIBERNATE_CALLBACKS bool @@ -229,6 +238,18 @@ config DPM_WATCHDOG_TIMEOUT default 120 depends on DPM_WATCHDOG +config PM_WAKEUP_TIMES + bool "Log time taken for device wakeup" + def_bool y + depends on PM && PM_SLEEP && DEBUG_FS + ---help--- + This enables code that calculates the total time spent for device + suspend and resume. These statistics are then logged to + /suspend_stats. + + The statistics include minimum, maximum, average and the most recent + times for resume and suspend. + config PM_TRACE bool help @@ -340,3 +361,10 @@ config PM_GENERIC_DOMAINS_OF config CPU_PM bool + +config WAKEUP_IRQ_DEBUG + bool "Debug info of wakeup irq" + default n + ---help--- + This enables debug information about wakeup interrupts using + sysfs (/sys/kernel/). diff --git a/kernel/power/Makefile b/kernel/power/Makefile index 80578f272be4..84d42b15890f 100644 --- a/kernel/power/Makefile +++ b/kernel/power/Makefile @@ -16,3 +16,4 @@ obj-$(CONFIG_PM_WAKELOCKS) += wakelock.o obj-$(CONFIG_MAGIC_SYSRQ) += poweroff.o obj-$(CONFIG_SUSPEND) += wakeup_reason.o +obj-$(CONFIG_WAKEUP_IRQ_DEBUG) += wakeup_irq_debug.o diff --git a/kernel/power/main.c b/kernel/power/main.c index 281a697fd458..9876400d9b93 100644 --- a/kernel/power/main.c +++ b/kernel/power/main.c @@ -15,12 +15,48 @@ #include #include #include +#include +#ifdef CONFIG_PM_WAKEUP_TIMES +#include +#endif #include "power.h" DEFINE_MUTEX(pm_mutex); #ifdef CONFIG_PM_SLEEP +/* + * Enable/Disable suspend back off logic attribute + */ +static bool sbo_enabled = true; + +/* + * For how many percent an alive time is decayed + * if suspend back-off keeps ongoing. + */ +static u32 sbo_decay_value = 20; + +/* + * Initial max back-off alive time + */ +static u32 sbo_initial_alive_time_msecs = 10000; + +/* + * A time in milliseconds, before which a short + * sleep counter is increased to trigger a back-off. + */ +static u32 sbo_short_sleep_msecs = 1100; + +/* + * This variable regulates starting of suspend + * back off functionality, after counter is reached + * specified value. + */ +static u32 sbo_short_sleep_count = 10; + +static u32 decay_alive_time_ms; +static u32 suspend_short_count; +static struct wakeup_source *ws; /* Routines for PM-transition notifications */ @@ -215,6 +251,51 @@ static int suspend_stats_show(struct seq_file *s, void *unused) suspend_stats.failed_steps[index])); } +#ifdef CONFIG_PM_WAKEUP_TIMES + seq_printf(s, "%s\n%s %lldms (%s %lldms %s %lldms)\n" \ + "%s %lldms (%s %lldms %s %lldms)\n" \ + "%s %lldms (%s %lldms %s %lldms)\n" \ + "%s %lldms\n", + "suspend time:", + " min:", ktime_to_ms(ktime_sub( + suspend_stats.suspend_min_time.end, + suspend_stats.suspend_min_time.start)), + "start:", ktime_to_ms(suspend_stats.suspend_min_time.start), + "end:", ktime_to_ms(suspend_stats.suspend_min_time.end), + " max:", ktime_to_ms(ktime_sub( + suspend_stats.suspend_max_time.end, + suspend_stats.suspend_max_time.start)), + "start:", ktime_to_ms(suspend_stats.suspend_max_time.start), + "end:", ktime_to_ms(suspend_stats.suspend_max_time.end), + " last:", ktime_to_ms(ktime_sub( + suspend_stats.suspend_last_time.end, + suspend_stats.suspend_last_time.start)), + "start:", ktime_to_ms(suspend_stats.suspend_last_time.start), + "end:", ktime_to_ms(suspend_stats.suspend_last_time.end), + " avg:", ktime_to_ms(suspend_stats.suspend_avg_time)); + + seq_printf(s, "%s\n%s %lldms (%s %lldms %s %lldms)\n" \ + "%s %lldms (%s %lldms %s %lldms)\n" \ + "%s %lldms (%s %lldms %s %lldms)\n" \ + "%s %lldms\n", + "resume time:", + " min:", ktime_to_ms(ktime_sub( + suspend_stats.resume_min_time.end, + suspend_stats.resume_min_time.start)), + "start:", ktime_to_ms(suspend_stats.resume_min_time.start), + "end:", ktime_to_ms(suspend_stats.resume_min_time.end), + " max:", ktime_to_ms(ktime_sub( + suspend_stats.resume_max_time.end, + suspend_stats.resume_max_time.start)), + "start:", ktime_to_ms(suspend_stats.resume_max_time.start), + "end:", ktime_to_ms(suspend_stats.resume_max_time.end), + " last:", ktime_to_ms(ktime_sub( + suspend_stats.resume_last_time.end, + suspend_stats.resume_last_time.start)), + "start:", ktime_to_ms(suspend_stats.resume_last_time.start), + "end:", ktime_to_ms(suspend_stats.resume_last_time.end), + " avg:", ktime_to_ms(suspend_stats.resume_avg_time)); +#endif return 0; } @@ -223,10 +304,29 @@ static int suspend_stats_open(struct inode *inode, struct file *file) return single_open(file, suspend_stats_show, NULL); } +#ifdef CONFIG_PM_WAKEUP_TIMES +static unsigned int suspend_stats_poll(struct file *filp, + struct poll_table_struct *wait) +{ + unsigned int mask = 0; + + poll_wait(filp, &suspend_stats_queue.wait_queue, wait); + if (suspend_stats_queue.resume_done) { + mask |= (POLLIN | POLLRDNORM); + suspend_stats_queue.resume_done = 0; + } + + return mask; +} +#endif + static const struct file_operations suspend_stats_operations = { .open = suspend_stats_open, .read = seq_read, .llseek = seq_lseek, +#ifdef CONFIG_PM_WAKEUP_TIMES + .poll = suspend_stats_poll, +#endif .release = single_release, }; @@ -234,6 +334,9 @@ static int __init pm_debugfs_init(void) { debugfs_create_file("suspend_stats", S_IFREG | S_IRUGO, NULL, NULL, &suspend_stats_operations); +#ifdef CONFIG_PM_WAKEUP_TIMES + init_waitqueue_head(&suspend_stats_queue.wait_queue); +#endif return 0; } @@ -352,10 +455,147 @@ static suspend_state_t decode_state(const char *buf, size_t n) return PM_SUSPEND_ON; } +static inline u32 +decay_val(u32 val, u32 percent) +{ + u32 ratio; + + percent = clamp(percent, 0U, 100U); + ratio = 1024 - ((1024 * percent) / 100); + + return (val * ratio) / 1024; +} + +static ssize_t sbo_decay_value_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t n) +{ + unsigned long val; + + if (kstrtoul(buf, 10, &val)) + return -EINVAL; + + if (val > 100) + return -EINVAL; + + sbo_decay_value = val; + return n; +} + +static ssize_t sbo_decay_value_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%u\n", sbo_decay_value); +} + +power_attr(sbo_decay_value); + +static ssize_t sbo_short_sleep_msecs_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t n) +{ + unsigned long val; + + if (kstrtoul(buf, 10, &val)) + return -EINVAL; + + sbo_short_sleep_msecs = val; + return n; +} + +static ssize_t sbo_short_sleep_msecs_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%u\n", sbo_short_sleep_msecs); +} + +power_attr(sbo_short_sleep_msecs); + +static ssize_t sbo_short_sleep_count_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t n) +{ + unsigned long val; + + if (kstrtoul(buf, 10, &val)) + return -EINVAL; + + sbo_short_sleep_count = val; + return n; +} + +static ssize_t sbo_short_sleep_count_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%u\n", sbo_short_sleep_count); +} + +power_attr(sbo_short_sleep_count); + +static ssize_t sbo_initial_alive_time_msecs_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t n) +{ + unsigned long val; + + if (kstrtoul(buf, 10, &val)) + return -EINVAL; + + sbo_initial_alive_time_msecs = val; + return n; +} + +static ssize_t sbo_initial_alive_time_msecs_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%u\n", sbo_initial_alive_time_msecs); +} + +power_attr(sbo_initial_alive_time_msecs); + +static ssize_t sbo_enabled_store(struct kobject *kobj, + struct kobj_attribute *attr, + const char *buf, size_t n) +{ + unsigned long val; + + if (kstrtoul(buf, 10, &val)) + return -EINVAL; + + if (val > 1) + return -EINVAL; + + sbo_enabled = !!val; + return n; +} + +static ssize_t sbo_enabled_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + return snprintf(buf, PAGE_SIZE, "%d\n", sbo_enabled); +} + +power_attr(sbo_enabled); + +static void +suspend_backoff(u32 timeout_msecs) +{ + if (!sbo_enabled) + return; + + pr_info("suspend: too many immediate wakeups, back off (%u msecs)\n", + timeout_msecs); + + __pm_wakeup_event(ws, timeout_msecs); +} + static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr, const char *buf, size_t n) { suspend_state_t state; + struct timespec ts_entry, ts_exit; + u64 elapsed_msecs64; + u32 elapsed_msecs32; int error; error = pm_autosleep_lock(); @@ -368,9 +608,48 @@ static ssize_t state_store(struct kobject *kobj, struct kobj_attribute *attr, } state = decode_state(buf, n); - if (state < PM_SUSPEND_MAX) + if (state < PM_SUSPEND_MAX) { + /* + * We want to prevent system from frequent periodic wake-ups + * when sleeping time is less or equal certain interval. + * It's done in order to save power in certain cases, one of + * the examples is GPS tracking, but not only. + */ + getnstimeofday(&ts_entry); error = pm_suspend(state); - else if (state == PM_SUSPEND_MAX) + getnstimeofday(&ts_exit); + + elapsed_msecs64 = timespec_to_ns(&ts_exit) - + timespec_to_ns(&ts_entry); + do_div(elapsed_msecs64, NSEC_PER_MSEC); + elapsed_msecs32 = elapsed_msecs64; + + if (elapsed_msecs32 <= sbo_short_sleep_msecs) { + if (suspend_short_count == sbo_short_sleep_count) { + if (decay_alive_time_ms >= MSEC_PER_SEC) { + suspend_backoff(decay_alive_time_ms); + decay_alive_time_ms = + decay_val(decay_alive_time_ms, + sbo_decay_value); + goto out; + } + } else { + suspend_short_count++; + goto out; + } + } + + /* Start from scratch */ + suspend_short_count = 0; + + /* + * Randomize a bit an initial alive time value to be + * not synced with any wake up sources, for example + * IRQs. + */ + decay_alive_time_ms = sbo_initial_alive_time_msecs - + get_random_int() % (sbo_initial_alive_time_msecs / 10); + } else if (state == PM_SUSPEND_MAX) error = hibernate(); else error = -EINVAL; @@ -602,6 +881,11 @@ static struct attribute * g[] = { #ifdef CONFIG_PM_SLEEP &pm_async_attr.attr, &wakeup_count_attr.attr, + &sbo_enabled_attr.attr, + &sbo_decay_value_attr.attr, + &sbo_short_sleep_msecs_attr.attr, + &sbo_short_sleep_count_attr.attr, + &sbo_initial_alive_time_msecs_attr.attr, #ifdef CONFIG_PM_AUTOSLEEP &autosleep_attr.attr, #endif @@ -632,7 +916,7 @@ EXPORT_SYMBOL_GPL(pm_wq); static int __init pm_start_workqueue(void) { - pm_wq = alloc_workqueue("pm", WQ_FREEZABLE, 0); + pm_wq = alloc_workqueue("pm", WQ_FREEZABLE | WQ_MEM_RECLAIM, 0); return pm_wq ? 0 : -ENOMEM; } @@ -652,6 +936,10 @@ static int __init pm_init(void) if (error) return error; pm_print_times_init(); + +#ifdef CONFIG_PM_SLEEP + ws = wakeup_source_register("suspend_backoff"); +#endif return pm_autosleep_init(); } diff --git a/kernel/power/suspend.c b/kernel/power/suspend.c index 2d0c99b3f34c..3bda98380279 100644 --- a/kernel/power/suspend.c +++ b/kernel/power/suspend.c @@ -381,7 +381,9 @@ static int suspend_enter(suspend_state_t state, bool *wakeup) arch_suspend_disable_irqs(); BUG_ON(!irqs_disabled()); - +#ifdef CONFIG_PM_WAKEUP_TIMES + dpm_log_wakeup_stats(PMSG_SUSPEND); +#endif error = syscore_suspend(); if (!error) { *wakeup = pm_wakeup_pending(); @@ -401,6 +403,9 @@ static int suspend_enter(suspend_state_t state, bool *wakeup) syscore_resume(); } +#ifdef CONFIG_PM_WAKEUP_TIMES + dpm_log_start_time(PMSG_RESUME); +#endif arch_suspend_enable_irqs(); BUG_ON(irqs_disabled()); @@ -440,6 +445,9 @@ int suspend_devices_and_enter(suspend_state_t state) suspend_console(); suspend_test_start(); +#ifdef CONFIG_PM_WAKEUP_TIMES + dpm_log_start_time(PMSG_SUSPEND); +#endif error = dpm_suspend_start(PMSG_SUSPEND); if (error) { pr_err("PM: Some devices failed to suspend, or early wake event detected\n"); @@ -457,6 +465,9 @@ int suspend_devices_and_enter(suspend_state_t state) Resume_devices: suspend_test_start(); dpm_resume_end(PMSG_RESUME); +#ifdef CONFIG_PM_WAKEUP_TIMES + dpm_log_wakeup_stats(PMSG_RESUME); +#endif suspend_test_finish("resume devices"); trace_suspend_resume(TPS("resume_console"), state, true); resume_console(); diff --git a/kernel/power/wakeup_irq_debug.c b/kernel/power/wakeup_irq_debug.c new file mode 100644 index 000000000000..c570204f2ca1 --- /dev/null +++ b/kernel/power/wakeup_irq_debug.c @@ -0,0 +1,357 @@ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define MODULE_NAME "wakeup_debug" + +struct irq_info { + struct list_head list; + unsigned int wakeup_count; + unsigned int irq_count; + char irq_name[128]; + int irq; +}; + +struct wakeup_irq { + struct list_head head; + bool enabled; +}; + +static int update_irq_table(struct wakeup_irq *p, int irq) +{ + struct irq_info *i; + + list_for_each_entry(i, &p->head, list) { + if (i->irq == irq) { + i->irq_count = kstat_irqs_cpu(i->irq, 0); + return 1; + } + } + + return 0; +} + +static void add_irq_to_table(struct wakeup_irq *p, int irq) +{ + struct irq_info *info; + struct irq_desc *desc; + + /* + * TODO: add here a filter for root IRQs + */ + info = kzalloc(sizeof(*info), GFP_KERNEL); + info->irq_count = kstat_irqs_cpu(irq, 0); + info->irq = irq; + + desc = irq_to_desc(irq); + + /* + * IRQ can be disabled and freed, thus + * we save its name because of history + */ + if (desc && desc->action && desc->action->name) + (void) scnprintf( + info->irq_name, sizeof(info->irq_name), + "%s", desc->action->name); + + list_add_tail(&info->list, &p->head); +} + +static int wakeup_irq_suspend(struct device *dev) +{ + struct irq_desc *desc; + struct wakeup_irq *p; + int irq, rv; + + /* get private data */ + p = dev_get_drvdata(dev); + if (!p->enabled) + goto leave; + + /* + * we want to add IRQs to list by reverse way, + * therefore all potential triggered IRQ (nested) + * will be in the beginning of the list + */ + for_each_irq_desc_reverse(irq, desc) { + raw_spin_lock_irq(&desc->lock); + rv = irqd_is_wakeup_set(&desc->irq_data); + raw_spin_unlock_irq(&desc->lock); + + if (rv == 0) + continue; + + rv = update_irq_table(p, irq); + if (rv) + continue; + + add_irq_to_table(p, irq); + dev_dbg(dev, "IRQ %d was added to the list\n", irq); + } + +leave: + return 0; +} + +static int wakeup_irq_resume(struct device *dev) +{ + struct irq_info *i; + struct wakeup_irq *p; + struct irq_desc *desc; + unsigned long flags; + int diff; + + /* get private data */ + p = dev_get_drvdata(dev); + if (!p->enabled) + goto leave; + + /* + * go through all registered IRQs in our list + */ + list_for_each_entry(i, &p->head, list) { + desc = irq_to_desc(i->irq); + if (desc) { + raw_spin_lock_irqsave(&desc->lock, flags); + diff = kstat_irqs_cpu(i->irq, 0) - i->irq_count; + raw_spin_unlock_irqrestore(&desc->lock, flags); + + if (diff > 0) { + i->wakeup_count++; + + dev_dbg(dev, "%d, wake-up count: %d, active: %d (%s)\n", + i->irq, i->wakeup_count, + irqd_is_wakeup_set(&desc->irq_data), + desc->action && desc->action->name ? + desc->action->name : ""); + + log_wakeup_reason(i->irq); + update_irq_table(p, i->irq); + } + } + } + +leave: + return 0; +} + +static ssize_t irq_stat_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct wakeup_irq *p; + struct irq_info *i; + int len = 0; + + p = dev_get_drvdata(dev); + + /* build header of the output */ + len += scnprintf(buf + len, PAGE_SIZE - len, + "IRQ\tCount\tName\n"); + + list_for_each_entry(i, &p->head, list) { + if (i->wakeup_count) { + len += scnprintf(buf + len, PAGE_SIZE - len, + "%d\t%d\t%s\n", + i->irq, i->wakeup_count, i->irq_name); + /* + * Ensure we do not write more than PAGE_SIZE + * bytes of data including null terminator and + * add '\n' symbol to the buffer if it's full + */ + if (len >= PAGE_SIZE - 1) { + buf[PAGE_SIZE - 2] = '\n'; + dev_err(dev, "%s:%d: buffer is full\n", + __func__, __LINE__); + break; + } + } + } + + return len; +} + +static ssize_t enable_show(struct device *dev, + struct device_attribute *attr, + char *buf) +{ + struct wakeup_irq *p; + + p = dev_get_drvdata(dev); + return scnprintf(buf, PAGE_SIZE, "%d\n", p->enabled); +} + +static ssize_t enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t count) +{ + struct wakeup_irq *p; + int value, ret; + + p = dev_get_drvdata(dev); + + ret = kstrtoint(buf, 10, &value); + if (ret) { + dev_err(dev, "kstrtoint failed: %d\n", ret); + goto error; + } + + if (value == 1) + p->enabled = true; + else if (value == 0) + p->enabled = false; + else + goto error; + + return count; + +error: + return -EINVAL; +} + +static const struct dev_pm_ops wakeup_irq_pm_ops = { + .suspend_noirq = wakeup_irq_suspend, + .resume_early = wakeup_irq_resume, +}; + +static struct platform_device wakeup_irq_device = { + .name = MODULE_NAME, + .id = 0, +}; + +static struct platform_driver wakeup_irq_driver = { + .driver = { + .name = MODULE_NAME, + .owner = THIS_MODULE, + .pm = &wakeup_irq_pm_ops, + }, +}; + +static struct device_attribute wakeup_irq_attrs[] = { + __ATTR(wakeup_irq_stat, + S_IRUSR | S_IRGRP | S_IROTH, irq_stat_show, NULL), + __ATTR(enable, + S_IRUSR | S_IWUSR, enable_show, enable_store), +}; + +static int create_sysfs_entries(struct device *dev) +{ + int i, rc; + + for (i = 0; i < ARRAY_SIZE(wakeup_irq_attrs); i++) { + rc = device_create_file(dev, &wakeup_irq_attrs[i]); + if (rc < 0) + goto revert; + } + + return 0; + +revert: + for (; --i >= 0;) + device_remove_file(dev, &wakeup_irq_attrs[i]); + + return rc; +} + +static int wakeup_irq_debug_init(void) +{ + struct wakeup_irq *p; + int rv = -1; + + p = kzalloc(sizeof(struct wakeup_irq), GFP_KERNEL); + if (p == NULL) + goto out; + + INIT_LIST_HEAD(&p->head); + + /* + * we need it for PM works correctly + */ + rv = platform_device_register(&wakeup_irq_device); + if (rv) { + pr_err("%s: wakeup_irq_device register failed %d\n", + __func__, rv); + goto free_mem; + } + + rv = platform_driver_register(&wakeup_irq_driver); + if (rv) { + pr_err("%s: wakeup_irq_driver register failed %d\n", + __func__, rv); + goto unregister_device; + } + + rv = create_sysfs_entries(&wakeup_irq_device.dev); + if (rv < 0) { + pr_err("%s: create_sysfs_entries failed %d\n", + __func__, rv); + goto unregister_driver; + } + + /* + * save private data + */ + platform_set_drvdata(&wakeup_irq_device, p); + + /* + * by default it's disabled + */ + p->enabled = false; + return 0; + +unregister_driver: + platform_driver_unregister(&wakeup_irq_driver); +unregister_device: + platform_device_unregister(&wakeup_irq_device); +free_mem: + kzfree(p); +out: + return rv; +} + +static void wakeup_irq_debug_exit(void) +{ + struct irq_info *info, *tmp_info; + struct wakeup_irq *p; + int i; + + p = dev_get_drvdata(&wakeup_irq_device.dev); + + for (i = 0; i < ARRAY_SIZE(wakeup_irq_attrs); i++) + device_remove_file(&wakeup_irq_device.dev, + &wakeup_irq_attrs[i]); + + list_for_each_entry_safe(info, tmp_info, &p->head, list) { + list_del(&info->list); + kzfree(info); + } + + platform_driver_unregister(&wakeup_irq_driver); + platform_device_unregister(&wakeup_irq_device); + kzfree(p); +} + +module_init(wakeup_irq_debug_init); +module_exit(wakeup_irq_debug_exit); + +MODULE_DESCRIPTION("Wakeup IRQ Debug Module"); +MODULE_AUTHOR("Uladzislau Rezki"); +MODULE_LICENSE("GPL v2");