| From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 |
| From: zhuguangqing <zhuguangqing@xiaomi.com> |
| Date: Fri, 23 Aug 2019 09:04:06 +0800 |
| Subject: REVISIT: ANDROID: power: wakeup_reason: add an API to log wakeup |
| reasons |
| |
| Add API log_wakeup_reason() and expose it to userspace via sysfs path |
| /sys/kernel/wakeup_reasons/last_resume_reason |
| |
| [CPNOTE: 17/06/21] Lee: Reached out to Rafael to gauge possible upstream acceptance |
| [CPNOTE: 30/09/21] Lee: Rafeal thinks the logging calls are too intrusive |
| Would need an upstream discussion and a rework to be considered |
| |
| Bug: 120445600 |
| Signed-off-by: Ruchi Kandoi <kandoiruchi@google.com> |
| [AmitP: Folded following android-4.9 commit changes into this patch |
| 1135122a192a ("ANDROID: POWER: fix compile warnings in log_wakeup_reason") |
| b4e6247778b0 ("ANDROID: Power: Changes the permission to read only for sysfs file /sys/kernel/wakeup_reasons/last_resume_reason") |
| e13dbc7c69cd ("ANDROID: power: wakeup_reason: rename irq_count to irqcount")] |
| Signed-off-by: Amit Pundir <amit.pundir@linaro.org> |
| [adelva: Folded the following changes into this patch: |
| 39d7c7fe91c0 ("ANDROID: power: wakeup_reason: Add guard condition for maximum wakeup reasons") |
| 0730434bdf49 ("ANDROID: power: wakeup_reason: Avoids bogus error messages for the suspend aborts.") |
| 4e42dceae54e ("ANDROID: power: wakeup_reason: Adds functionality to log the last suspend abort reason.") |
| f21313b70ac7 ("ANDROID: power: wakeup_reason: Report suspend times from last_suspend_time") |
| f97ec34442ac ("ANDROID: power: wakeup_reason: fix suspend time reporting") |
| cd92df73e504 ("ANDROID: power: wakeup: Add last wake up source logging for suspend abort reason.") |
| 546b6ae3c087 ("ANDROID: power: wakeup: Add the guard condition for len in pm_get_active_wakeup_sources") |
| 1453d9ffcdbe ("ANDROID: power: wakeup_reason: make logging work in an interrupt context.")] |
| Change-Id: I81addaf420f1338255c5d0638b0d244a99d777d1 |
| Signed-off-by: Alistair Delva <adelva@google.com> |
| [Lee: Squashed a few more subsequent patches into this one] |
| Signed-off-by: Lee Jones <lee.jones@linaro.org> |
| Signed-off-by: Lee Jones <joneslee@google.com> |
| --- |
| .../ABI/testing/sysfs-kernel-wakeup_reasons | 16 + |
| drivers/base/power/main.c | 11 + |
| drivers/base/power/wakeup.c | 54 ++- |
| drivers/base/syscore.c | 5 +- |
| include/linux/suspend.h | 1 + |
| include/linux/wakeup_reason.h | 37 ++ |
| kernel/irq/chip.c | 17 +- |
| kernel/power/Makefile | 1 + |
| kernel/power/process.c | 10 +- |
| kernel/power/suspend.c | 20 +- |
| kernel/power/wakeup_reason.c | 438 ++++++++++++++++++ |
| 11 files changed, 601 insertions(+), 9 deletions(-) |
| create mode 100644 Documentation/ABI/testing/sysfs-kernel-wakeup_reasons |
| create mode 100644 include/linux/wakeup_reason.h |
| create mode 100644 kernel/power/wakeup_reason.c |
| |
| diff --git a/Documentation/ABI/testing/sysfs-kernel-wakeup_reasons b/Documentation/ABI/testing/sysfs-kernel-wakeup_reasons |
| new file mode 100644 |
| --- /dev/null |
| +++ b/Documentation/ABI/testing/sysfs-kernel-wakeup_reasons |
| @@ -0,0 +1,16 @@ |
| +What: /sys/kernel/wakeup_reasons/last_resume_reason |
| +Date: February 2014 |
| +Contact: Ruchi Kandoi <kandoiruchi@google.com> |
| +Description: |
| + The /sys/kernel/wakeup_reasons/last_resume_reason is |
| + used to report wakeup reasons after system exited suspend. |
| + |
| +What: /sys/kernel/wakeup_reasons/last_suspend_time |
| +Date: March 2015 |
| +Contact: jinqian <jinqian@google.com> |
| +Description: |
| + The /sys/kernel/wakeup_reasons/last_suspend_time is |
| + used to report time spent in last suspend cycle. It contains |
| + two numbers (in seconds) separated by space. First number is |
| + the time spent in suspend and resume processes. Second number |
| + is the time spent in sleep state. |
| \ No newline at end of file |
| diff --git a/drivers/base/power/main.c b/drivers/base/power/main.c |
| --- a/drivers/base/power/main.c |
| +++ b/drivers/base/power/main.c |
| @@ -34,6 +34,7 @@ |
| #include <linux/cpufreq.h> |
| #include <linux/devfreq.h> |
| #include <linux/timer.h> |
| +#include <linux/wakeup_reason.h> |
| |
| #include "../base.h" |
| #include "power.h" |
| @@ -1241,6 +1242,8 @@ static int __device_suspend_noirq(struct device *dev, pm_message_t state, bool a |
| error = dpm_run_callback(callback, dev, state, info); |
| if (error) { |
| async_error = error; |
| + log_suspend_abort_reason("Callback failed on %s in %pS returned %d", |
| + dev_name(dev), callback, error); |
| goto Complete; |
| } |
| |
| @@ -1435,6 +1438,8 @@ static int __device_suspend_late(struct device *dev, pm_message_t state, bool as |
| error = dpm_run_callback(callback, dev, state, info); |
| if (error) { |
| async_error = error; |
| + log_suspend_abort_reason("Callback failed on %s in %pS returned %d", |
| + dev_name(dev), callback, error); |
| goto Complete; |
| } |
| dpm_propagate_wakeup_to_parent(dev); |
| @@ -1711,6 +1716,9 @@ static int __device_suspend(struct device *dev, pm_message_t state, bool async) |
| |
| dpm_propagate_wakeup_to_parent(dev); |
| dpm_clear_superiors_direct_complete(dev); |
| + } else { |
| + log_suspend_abort_reason("Callback failed on %s in %pS returned %d", |
| + dev_name(dev), callback, error); |
| } |
| |
| device_unlock(dev); |
| @@ -1924,6 +1932,9 @@ int dpm_prepare(pm_message_t state) |
| } else { |
| dev_info(dev, "not prepared for power transition: code %d\n", |
| error); |
| + log_suspend_abort_reason("Device %s not prepared for power transition: code %d", |
| + dev_name(dev), error); |
| + dpm_save_failed_dev(dev_name(dev)); |
| } |
| |
| mutex_unlock(&dpm_list_mtx); |
| diff --git a/drivers/base/power/wakeup.c b/drivers/base/power/wakeup.c |
| --- a/drivers/base/power/wakeup.c |
| +++ b/drivers/base/power/wakeup.c |
| @@ -15,6 +15,9 @@ |
| #include <linux/seq_file.h> |
| #include <linux/debugfs.h> |
| #include <linux/pm_wakeirq.h> |
| +#include <linux/irq.h> |
| +#include <linux/irqdesc.h> |
| +#include <linux/wakeup_reason.h> |
| #include <trace/events/power.h> |
| |
| #include "power.h" |
| @@ -844,6 +847,37 @@ void pm_wakeup_dev_event(struct device *dev, unsigned int msec, bool hard) |
| } |
| EXPORT_SYMBOL_GPL(pm_wakeup_dev_event); |
| |
| +void pm_get_active_wakeup_sources(char *pending_wakeup_source, size_t max) |
| +{ |
| + struct wakeup_source *ws, *last_active_ws = NULL; |
| + int len = 0; |
| + bool active = false; |
| + |
| + rcu_read_lock(); |
| + list_for_each_entry_rcu(ws, &wakeup_sources, entry) { |
| + if (ws->active && len < max) { |
| + if (!active) |
| + len += scnprintf(pending_wakeup_source, max, |
| + "Pending Wakeup Sources: "); |
| + len += scnprintf(pending_wakeup_source + len, max - len, |
| + "%s ", ws->name); |
| + active = true; |
| + } else if (!active && |
| + (!last_active_ws || |
| + ktime_to_ns(ws->last_time) > |
| + ktime_to_ns(last_active_ws->last_time))) { |
| + last_active_ws = ws; |
| + } |
| + } |
| + if (!active && last_active_ws) { |
| + scnprintf(pending_wakeup_source, max, |
| + "Last active Wakeup Source: %s", |
| + last_active_ws->name); |
| + } |
| + rcu_read_unlock(); |
| +} |
| +EXPORT_SYMBOL_GPL(pm_get_active_wakeup_sources); |
| + |
| void pm_print_active_wakeup_sources(void) |
| { |
| struct wakeup_source *ws; |
| @@ -882,6 +916,7 @@ bool pm_wakeup_pending(void) |
| { |
| unsigned long flags; |
| bool ret = false; |
| + char suspend_abort[MAX_SUSPEND_ABORT_LEN]; |
| |
| raw_spin_lock_irqsave(&events_lock, flags); |
| if (events_check_enabled) { |
| @@ -896,6 +931,10 @@ bool pm_wakeup_pending(void) |
| if (ret) { |
| pm_pr_dbg("Wakeup pending, aborting suspend\n"); |
| pm_print_active_wakeup_sources(); |
| + pm_get_active_wakeup_sources(suspend_abort, |
| + MAX_SUSPEND_ABORT_LEN); |
| + log_suspend_abort_reason(suspend_abort); |
| + pr_info("PM: %s\n", suspend_abort); |
| } |
| |
| return ret || atomic_read(&pm_abort_suspend) > 0; |
| @@ -948,8 +987,21 @@ void pm_system_irq_wakeup(unsigned int irq_number) |
| |
| raw_spin_unlock_irqrestore(&wakeup_irq_lock, flags); |
| |
| - if (irq_number) |
| + if (irq_number) { |
| + struct irq_desc *desc; |
| + const char *name = "null"; |
| + |
| + desc = irq_to_desc(irq_number); |
| + if (desc == NULL) |
| + name = "stray irq"; |
| + else if (desc->action && desc->action->name) |
| + name = desc->action->name; |
| + |
| + log_irq_wakeup_reason(irq_number); |
| + pr_warn("%s: %d triggered %s\n", __func__, irq_number, name); |
| + |
| pm_system_wakeup(); |
| + } |
| } |
| |
| unsigned int pm_wakeup_irq(void) |
| diff --git a/drivers/base/syscore.c b/drivers/base/syscore.c |
| --- a/drivers/base/syscore.c |
| +++ b/drivers/base/syscore.c |
| @@ -10,6 +10,7 @@ |
| #include <linux/module.h> |
| #include <linux/suspend.h> |
| #include <trace/events/power.h> |
| +#include <linux/wakeup_reason.h> |
| |
| static LIST_HEAD(syscore_ops_list); |
| static DEFINE_MUTEX(syscore_ops_lock); |
| @@ -73,7 +74,9 @@ int syscore_suspend(void) |
| return 0; |
| |
| err_out: |
| - pr_err("PM: System core suspend callback %pS failed.\n", ops->suspend); |
| + log_suspend_abort_reason("System core suspend callback %pS failed", |
| + ops->suspend); |
| + pr_err("PM: System core suspend callback %pF failed.\n", ops->suspend); |
| |
| list_for_each_entry_continue(ops, &syscore_ops_list, node) |
| if (ops->resume) |
| diff --git a/include/linux/suspend.h b/include/linux/suspend.h |
| --- a/include/linux/suspend.h |
| +++ b/include/linux/suspend.h |
| @@ -510,6 +510,7 @@ extern bool pm_get_wakeup_count(unsigned int *count, bool block); |
| extern bool pm_save_wakeup_count(unsigned int count); |
| extern void pm_wakep_autosleep_enabled(bool set); |
| extern void pm_print_active_wakeup_sources(void); |
| +extern void pm_get_active_wakeup_sources(char *pending_sources, size_t max); |
| |
| extern unsigned int lock_system_sleep(void); |
| extern void unlock_system_sleep(unsigned int); |
| diff --git a/include/linux/wakeup_reason.h b/include/linux/wakeup_reason.h |
| new file mode 100644 |
| --- /dev/null |
| +++ b/include/linux/wakeup_reason.h |
| @@ -0,0 +1,37 @@ |
| +/* |
| + * include/linux/wakeup_reason.h |
| + * |
| + * Logs the reason which caused the kernel to resume |
| + * from the suspend mode. |
| + * |
| + * Copyright (C) 2014 Google, Inc. |
| + * This software is licensed under the terms of the GNU General Public |
| + * License version 2, as published by the Free Software Foundation, and |
| + * may be copied, distributed, and modified under those terms. |
| + * |
| + * 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. |
| + */ |
| + |
| +#ifndef _LINUX_WAKEUP_REASON_H |
| +#define _LINUX_WAKEUP_REASON_H |
| + |
| +#define MAX_SUSPEND_ABORT_LEN 256 |
| + |
| +#ifdef CONFIG_SUSPEND |
| +void log_irq_wakeup_reason(int irq); |
| +void log_threaded_irq_wakeup_reason(int irq, int parent_irq); |
| +void log_suspend_abort_reason(const char *fmt, ...); |
| +void log_abnormal_wakeup_reason(const char *fmt, ...); |
| +void clear_wakeup_reasons(void); |
| +#else |
| +static inline void log_irq_wakeup_reason(int irq) { } |
| +static inline void log_threaded_irq_wakeup_reason(int irq, int parent_irq) { } |
| +static inline void log_suspend_abort_reason(const char *fmt, ...) { } |
| +static inline void log_abnormal_wakeup_reason(const char *fmt, ...) { } |
| +static inline void clear_wakeup_reasons(void) { } |
| +#endif |
| + |
| +#endif /* _LINUX_WAKEUP_REASON_H */ |
| diff --git a/kernel/irq/chip.c b/kernel/irq/chip.c |
| --- a/kernel/irq/chip.c |
| +++ b/kernel/irq/chip.c |
| @@ -14,6 +14,7 @@ |
| #include <linux/interrupt.h> |
| #include <linux/kernel_stat.h> |
| #include <linux/irqdomain.h> |
| +#include <linux/wakeup_reason.h> |
| |
| #include <trace/events/irq.h> |
| |
| @@ -509,8 +510,22 @@ static bool irq_may_run(struct irq_desc *desc) |
| * If the interrupt is not in progress and is not an armed |
| * wakeup interrupt, proceed. |
| */ |
| - if (!irqd_has_set(&desc->irq_data, mask)) |
| + if (!irqd_has_set(&desc->irq_data, mask)) { |
| +#ifdef CONFIG_PM_SLEEP |
| + if (unlikely(desc->no_suspend_depth && |
| + irqd_is_wakeup_set(&desc->irq_data))) { |
| + unsigned int irq = irq_desc_get_irq(desc); |
| + const char *name = "(unnamed)"; |
| + |
| + if (desc->action && desc->action->name) |
| + name = desc->action->name; |
| + |
| + log_abnormal_wakeup_reason("misconfigured IRQ %u %s", |
| + irq, name); |
| + } |
| +#endif |
| return true; |
| + } |
| |
| /* |
| * If the interrupt is an armed wakeup source, mark it pending |
| diff --git a/kernel/power/Makefile b/kernel/power/Makefile |
| --- a/kernel/power/Makefile |
| +++ b/kernel/power/Makefile |
| @@ -21,4 +21,5 @@ obj-$(CONFIG_PM_WAKELOCKS) += wakelock.o |
| |
| obj-$(CONFIG_MAGIC_SYSRQ) += poweroff.o |
| |
| +obj-$(CONFIG_SUSPEND) += wakeup_reason.o |
| obj-$(CONFIG_ENERGY_MODEL) += energy_model.o |
| diff --git a/kernel/power/process.c b/kernel/power/process.c |
| --- a/kernel/power/process.c |
| +++ b/kernel/power/process.c |
| @@ -85,17 +85,19 @@ static int try_to_freeze_tasks(bool user_only) |
| elapsed = ktime_sub(end, start); |
| elapsed_msecs = ktime_to_ms(elapsed); |
| |
| - if (todo) { |
| - pr_err("Freezing %s %s after %d.%03d seconds " |
| + if (wakeup) { |
| + pr_err("Freezing %s aborted after %d.%03d seconds\n", what, |
| + elapsed_msecs / 1000, elapsed_msecs % 1000); |
| + } else if (todo) { |
| + pr_err("Freezing %s failed after %d.%03d seconds " |
| "(%d tasks refusing to freeze, wq_busy=%d):\n", what, |
| - wakeup ? "aborted" : "failed", |
| elapsed_msecs / 1000, elapsed_msecs % 1000, |
| todo - wq_busy, wq_busy); |
| |
| if (wq_busy) |
| show_all_workqueues(); |
| |
| - if (!wakeup || pm_debug_messages_on) { |
| + if (pm_debug_messages_on) { |
| read_lock(&tasklist_lock); |
| for_each_process_thread(g, p) { |
| if (p != current && freezing(p) && !frozen(p)) |
| diff --git a/kernel/power/suspend.c b/kernel/power/suspend.c |
| --- a/kernel/power/suspend.c |
| +++ b/kernel/power/suspend.c |
| @@ -30,6 +30,7 @@ |
| #include <trace/events/power.h> |
| #include <linux/compiler.h> |
| #include <linux/moduleparam.h> |
| +#include <linux/wakeup_reason.h> |
| |
| #include "power.h" |
| |
| @@ -138,6 +139,8 @@ static void s2idle_loop(void) |
| break; |
| } |
| |
| + clear_wakeup_reasons(); |
| + |
| if (s2idle_ops && s2idle_ops->check) |
| s2idle_ops->check(); |
| |
| @@ -367,6 +370,7 @@ static int suspend_prepare(suspend_state_t state) |
| if (!error) |
| return 0; |
| |
| + log_suspend_abort_reason("One or more tasks refusing to freeze"); |
| suspend_stats.failed_freeze++; |
| dpm_save_failed_step(SUSPEND_FREEZE); |
| pm_notifier_call_chain(PM_POST_SUSPEND); |
| @@ -396,7 +400,7 @@ void __weak arch_suspend_enable_irqs(void) |
| */ |
| static int suspend_enter(suspend_state_t state, bool *wakeup) |
| { |
| - int error; |
| + int error, last_dev; |
| |
| error = platform_suspend_prepare(state); |
| if (error) |
| @@ -404,7 +408,11 @@ static int suspend_enter(suspend_state_t state, bool *wakeup) |
| |
| error = dpm_suspend_late(PMSG_SUSPEND); |
| if (error) { |
| + last_dev = suspend_stats.last_failed_dev + REC_FAILED_NUM - 1; |
| + last_dev %= REC_FAILED_NUM; |
| pr_err("late suspend of devices failed\n"); |
| + log_suspend_abort_reason("late suspend of %s device failed", |
| + suspend_stats.failed_devs[last_dev]); |
| goto Platform_finish; |
| } |
| error = platform_suspend_prepare_late(state); |
| @@ -413,7 +421,11 @@ static int suspend_enter(suspend_state_t state, bool *wakeup) |
| |
| error = dpm_suspend_noirq(PMSG_SUSPEND); |
| if (error) { |
| + last_dev = suspend_stats.last_failed_dev + REC_FAILED_NUM - 1; |
| + last_dev %= REC_FAILED_NUM; |
| pr_err("noirq suspend of devices failed\n"); |
| + log_suspend_abort_reason("noirq suspend of %s device failed", |
| + suspend_stats.failed_devs[last_dev]); |
| goto Platform_early_resume; |
| } |
| error = platform_suspend_prepare_noirq(state); |
| @@ -429,8 +441,10 @@ static int suspend_enter(suspend_state_t state, bool *wakeup) |
| } |
| |
| error = pm_sleep_disable_secondary_cpus(); |
| - if (error || suspend_test(TEST_CPUS)) |
| + if (error || suspend_test(TEST_CPUS)) { |
| + log_suspend_abort_reason("Disabling non-boot cpus failed"); |
| goto Enable_cpus; |
| + } |
| |
| arch_suspend_disable_irqs(); |
| BUG_ON(!irqs_disabled()); |
| @@ -501,6 +515,8 @@ int suspend_devices_and_enter(suspend_state_t state) |
| error = dpm_suspend_start(PMSG_SUSPEND); |
| if (error) { |
| pr_err("Some devices failed to suspend, or early wake event detected\n"); |
| + log_suspend_abort_reason( |
| + "Some devices failed to suspend, or early wake event detected"); |
| goto Recover_platform; |
| } |
| suspend_test_finish("suspend devices"); |
| diff --git a/kernel/power/wakeup_reason.c b/kernel/power/wakeup_reason.c |
| new file mode 100644 |
| --- /dev/null |
| +++ b/kernel/power/wakeup_reason.c |
| @@ -0,0 +1,438 @@ |
| +/* |
| + * kernel/power/wakeup_reason.c |
| + * |
| + * Logs the reasons which caused the kernel to resume from |
| + * the suspend mode. |
| + * |
| + * Copyright (C) 2020 Google, Inc. |
| + * This software is licensed under the terms of the GNU General Public |
| + * License version 2, as published by the Free Software Foundation, and |
| + * may be copied, distributed, and modified under those terms. |
| + * |
| + * 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 <linux/wakeup_reason.h> |
| +#include <linux/kernel.h> |
| +#include <linux/irq.h> |
| +#include <linux/interrupt.h> |
| +#include <linux/io.h> |
| +#include <linux/kobject.h> |
| +#include <linux/sysfs.h> |
| +#include <linux/init.h> |
| +#include <linux/spinlock.h> |
| +#include <linux/notifier.h> |
| +#include <linux/suspend.h> |
| +#include <linux/slab.h> |
| + |
| +/* |
| + * struct wakeup_irq_node - stores data and relationships for IRQs logged as |
| + * either base or nested wakeup reasons during suspend/resume flow. |
| + * @siblings - for membership on leaf or parent IRQ lists |
| + * @irq - the IRQ number |
| + * @irq_name - the name associated with the IRQ, or a default if none |
| + */ |
| +struct wakeup_irq_node { |
| + struct list_head siblings; |
| + int irq; |
| + const char *irq_name; |
| +}; |
| + |
| +enum wakeup_reason_flag { |
| + RESUME_NONE = 0, |
| + RESUME_IRQ, |
| + RESUME_ABORT, |
| + RESUME_ABNORMAL, |
| +}; |
| + |
| +static DEFINE_SPINLOCK(wakeup_reason_lock); |
| + |
| +static LIST_HEAD(leaf_irqs); /* kept in ascending IRQ sorted order */ |
| +static LIST_HEAD(parent_irqs); /* unordered */ |
| + |
| +static struct kmem_cache *wakeup_irq_nodes_cache; |
| + |
| +static const char *default_irq_name = "(unnamed)"; |
| + |
| +static struct kobject *kobj; |
| + |
| +static bool capture_reasons; |
| +static int wakeup_reason; |
| +static char non_irq_wake_reason[MAX_SUSPEND_ABORT_LEN]; |
| + |
| +static ktime_t last_monotime; /* monotonic time before last suspend */ |
| +static ktime_t curr_monotime; /* monotonic time after last suspend */ |
| +static ktime_t last_stime; /* monotonic boottime offset before last suspend */ |
| +static ktime_t curr_stime; /* monotonic boottime offset after last suspend */ |
| + |
| +static void init_node(struct wakeup_irq_node *p, int irq) |
| +{ |
| + struct irq_desc *desc; |
| + |
| + INIT_LIST_HEAD(&p->siblings); |
| + |
| + p->irq = irq; |
| + desc = irq_to_desc(irq); |
| + if (desc && desc->action && desc->action->name) |
| + p->irq_name = desc->action->name; |
| + else |
| + p->irq_name = default_irq_name; |
| +} |
| + |
| +static struct wakeup_irq_node *create_node(int irq) |
| +{ |
| + struct wakeup_irq_node *result; |
| + |
| + result = kmem_cache_alloc(wakeup_irq_nodes_cache, GFP_ATOMIC); |
| + if (unlikely(!result)) |
| + pr_warn("Failed to log wakeup IRQ %d\n", irq); |
| + else |
| + init_node(result, irq); |
| + |
| + return result; |
| +} |
| + |
| +static void delete_list(struct list_head *head) |
| +{ |
| + struct wakeup_irq_node *n; |
| + |
| + while (!list_empty(head)) { |
| + n = list_first_entry(head, struct wakeup_irq_node, siblings); |
| + list_del(&n->siblings); |
| + kmem_cache_free(wakeup_irq_nodes_cache, n); |
| + } |
| +} |
| + |
| +static bool add_sibling_node_sorted(struct list_head *head, int irq) |
| +{ |
| + struct wakeup_irq_node *n = NULL; |
| + struct list_head *predecessor = head; |
| + |
| + if (unlikely(WARN_ON(!head))) |
| + return NULL; |
| + |
| + if (!list_empty(head)) |
| + list_for_each_entry(n, head, siblings) { |
| + if (n->irq < irq) |
| + predecessor = &n->siblings; |
| + else if (n->irq == irq) |
| + return true; |
| + else |
| + break; |
| + } |
| + |
| + n = create_node(irq); |
| + if (n) { |
| + list_add(&n->siblings, predecessor); |
| + return true; |
| + } |
| + |
| + return false; |
| +} |
| + |
| +static struct wakeup_irq_node *find_node_in_list(struct list_head *head, |
| + int irq) |
| +{ |
| + struct wakeup_irq_node *n; |
| + |
| + if (unlikely(WARN_ON(!head))) |
| + return NULL; |
| + |
| + list_for_each_entry(n, head, siblings) |
| + if (n->irq == irq) |
| + return n; |
| + |
| + return NULL; |
| +} |
| + |
| +void log_irq_wakeup_reason(int irq) |
| +{ |
| + unsigned long flags; |
| + |
| + spin_lock_irqsave(&wakeup_reason_lock, flags); |
| + if (wakeup_reason == RESUME_ABNORMAL || wakeup_reason == RESUME_ABORT) { |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return; |
| + } |
| + |
| + if (!capture_reasons) { |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return; |
| + } |
| + |
| + if (find_node_in_list(&parent_irqs, irq) == NULL) |
| + add_sibling_node_sorted(&leaf_irqs, irq); |
| + |
| + wakeup_reason = RESUME_IRQ; |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| +} |
| + |
| +void log_threaded_irq_wakeup_reason(int irq, int parent_irq) |
| +{ |
| + struct wakeup_irq_node *parent; |
| + unsigned long flags; |
| + |
| + /* |
| + * Intentionally unsynchronized. Calls that come in after we have |
| + * resumed should have a fast exit path since there's no work to be |
| + * done, any any coherence issue that could cause a wrong value here is |
| + * both highly improbable - given the set/clear timing - and very low |
| + * impact (parent IRQ gets logged instead of the specific child). |
| + */ |
| + if (!capture_reasons) |
| + return; |
| + |
| + spin_lock_irqsave(&wakeup_reason_lock, flags); |
| + |
| + if (wakeup_reason == RESUME_ABNORMAL || wakeup_reason == RESUME_ABORT) { |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return; |
| + } |
| + |
| + if (!capture_reasons || (find_node_in_list(&leaf_irqs, irq) != NULL)) { |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return; |
| + } |
| + |
| + parent = find_node_in_list(&parent_irqs, parent_irq); |
| + if (parent != NULL) |
| + add_sibling_node_sorted(&leaf_irqs, irq); |
| + else { |
| + parent = find_node_in_list(&leaf_irqs, parent_irq); |
| + if (parent != NULL) { |
| + list_del_init(&parent->siblings); |
| + list_add_tail(&parent->siblings, &parent_irqs); |
| + add_sibling_node_sorted(&leaf_irqs, irq); |
| + } |
| + } |
| + |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| +} |
| +EXPORT_SYMBOL_GPL(log_threaded_irq_wakeup_reason); |
| + |
| +static void __log_abort_or_abnormal_wake(bool abort, const char *fmt, |
| + va_list args) |
| +{ |
| + unsigned long flags; |
| + |
| + spin_lock_irqsave(&wakeup_reason_lock, flags); |
| + |
| + /* Suspend abort or abnormal wake reason has already been logged. */ |
| + if (wakeup_reason != RESUME_NONE) { |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return; |
| + } |
| + |
| + if (abort) |
| + wakeup_reason = RESUME_ABORT; |
| + else |
| + wakeup_reason = RESUME_ABNORMAL; |
| + |
| + vsnprintf(non_irq_wake_reason, MAX_SUSPEND_ABORT_LEN, fmt, args); |
| + |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| +} |
| + |
| +void log_suspend_abort_reason(const char *fmt, ...) |
| +{ |
| + va_list args; |
| + |
| + va_start(args, fmt); |
| + __log_abort_or_abnormal_wake(true, fmt, args); |
| + va_end(args); |
| +} |
| +EXPORT_SYMBOL_GPL(log_suspend_abort_reason); |
| + |
| +void log_abnormal_wakeup_reason(const char *fmt, ...) |
| +{ |
| + va_list args; |
| + |
| + va_start(args, fmt); |
| + __log_abort_or_abnormal_wake(false, fmt, args); |
| + va_end(args); |
| +} |
| +EXPORT_SYMBOL_GPL(log_abnormal_wakeup_reason); |
| + |
| +void clear_wakeup_reasons(void) |
| +{ |
| + unsigned long flags; |
| + |
| + spin_lock_irqsave(&wakeup_reason_lock, flags); |
| + |
| + delete_list(&leaf_irqs); |
| + delete_list(&parent_irqs); |
| + wakeup_reason = RESUME_NONE; |
| + capture_reasons = true; |
| + |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| +} |
| + |
| +static void print_wakeup_sources(void) |
| +{ |
| + struct wakeup_irq_node *n; |
| + unsigned long flags; |
| + |
| + spin_lock_irqsave(&wakeup_reason_lock, flags); |
| + |
| + capture_reasons = false; |
| + |
| + if (wakeup_reason == RESUME_ABORT) { |
| + pr_info("Abort: %s\n", non_irq_wake_reason); |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return; |
| + } |
| + |
| + if (wakeup_reason == RESUME_IRQ && !list_empty(&leaf_irqs)) |
| + list_for_each_entry(n, &leaf_irqs, siblings) |
| + pr_info("Resume caused by IRQ %d, %s\n", n->irq, |
| + n->irq_name); |
| + else if (wakeup_reason == RESUME_ABNORMAL) |
| + pr_info("Resume caused by %s\n", non_irq_wake_reason); |
| + else |
| + pr_info("Resume cause unknown\n"); |
| + |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| +} |
| + |
| +static ssize_t last_resume_reason_show(struct kobject *kobj, |
| + struct kobj_attribute *attr, char *buf) |
| +{ |
| + ssize_t buf_offset = 0; |
| + struct wakeup_irq_node *n; |
| + unsigned long flags; |
| + |
| + spin_lock_irqsave(&wakeup_reason_lock, flags); |
| + |
| + if (wakeup_reason == RESUME_ABORT) { |
| + buf_offset = scnprintf(buf, PAGE_SIZE, "Abort: %s", |
| + non_irq_wake_reason); |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + return buf_offset; |
| + } |
| + |
| + if (wakeup_reason == RESUME_IRQ && !list_empty(&leaf_irqs)) |
| + list_for_each_entry(n, &leaf_irqs, siblings) |
| + buf_offset += scnprintf(buf + buf_offset, |
| + PAGE_SIZE - buf_offset, |
| + "%d %s\n", n->irq, n->irq_name); |
| + else if (wakeup_reason == RESUME_ABNORMAL) |
| + buf_offset = scnprintf(buf, PAGE_SIZE, "-1 %s", |
| + non_irq_wake_reason); |
| + |
| + spin_unlock_irqrestore(&wakeup_reason_lock, flags); |
| + |
| + return buf_offset; |
| +} |
| + |
| +static ssize_t last_suspend_time_show(struct kobject *kobj, |
| + struct kobj_attribute *attr, char *buf) |
| +{ |
| + struct timespec64 sleep_time; |
| + struct timespec64 total_time; |
| + struct timespec64 suspend_resume_time; |
| + |
| + /* |
| + * total_time is calculated from monotonic bootoffsets because |
| + * unlike CLOCK_MONOTONIC it include the time spent in suspend state. |
| + */ |
| + total_time = ktime_to_timespec64(ktime_sub(curr_stime, last_stime)); |
| + |
| + /* |
| + * suspend_resume_time is calculated as monotonic (CLOCK_MONOTONIC) |
| + * time interval before entering suspend and post suspend. |
| + */ |
| + suspend_resume_time = |
| + ktime_to_timespec64(ktime_sub(curr_monotime, last_monotime)); |
| + |
| + /* sleep_time = total_time - suspend_resume_time */ |
| + sleep_time = timespec64_sub(total_time, suspend_resume_time); |
| + |
| + /* Export suspend_resume_time and sleep_time in pair here. */ |
| + return sprintf(buf, "%llu.%09lu %llu.%09lu\n", |
| + (unsigned long long)suspend_resume_time.tv_sec, |
| + suspend_resume_time.tv_nsec, |
| + (unsigned long long)sleep_time.tv_sec, |
| + sleep_time.tv_nsec); |
| +} |
| + |
| +static struct kobj_attribute resume_reason = __ATTR_RO(last_resume_reason); |
| +static struct kobj_attribute suspend_time = __ATTR_RO(last_suspend_time); |
| + |
| +static struct attribute *attrs[] = { |
| + &resume_reason.attr, |
| + &suspend_time.attr, |
| + NULL, |
| +}; |
| +static struct attribute_group attr_group = { |
| + .attrs = attrs, |
| +}; |
| + |
| +/* Detects a suspend and clears all the previous wake up reasons*/ |
| +static int wakeup_reason_pm_event(struct notifier_block *notifier, |
| + unsigned long pm_event, void *unused) |
| +{ |
| + switch (pm_event) { |
| + case PM_SUSPEND_PREPARE: |
| + /* monotonic time since boot */ |
| + last_monotime = ktime_get(); |
| + /* monotonic time since boot including the time spent in suspend */ |
| + last_stime = ktime_get_boottime(); |
| + clear_wakeup_reasons(); |
| + break; |
| + case PM_POST_SUSPEND: |
| + /* monotonic time since boot */ |
| + curr_monotime = ktime_get(); |
| + /* monotonic time since boot including the time spent in suspend */ |
| + curr_stime = ktime_get_boottime(); |
| + print_wakeup_sources(); |
| + break; |
| + default: |
| + break; |
| + } |
| + return NOTIFY_DONE; |
| +} |
| + |
| +static struct notifier_block wakeup_reason_pm_notifier_block = { |
| + .notifier_call = wakeup_reason_pm_event, |
| +}; |
| + |
| +static int __init wakeup_reason_init(void) |
| +{ |
| + if (register_pm_notifier(&wakeup_reason_pm_notifier_block)) { |
| + pr_warn("[%s] failed to register PM notifier\n", __func__); |
| + goto fail; |
| + } |
| + |
| + kobj = kobject_create_and_add("wakeup_reasons", kernel_kobj); |
| + if (!kobj) { |
| + pr_warn("[%s] failed to create a sysfs kobject\n", __func__); |
| + goto fail_unregister_pm_notifier; |
| + } |
| + |
| + if (sysfs_create_group(kobj, &attr_group)) { |
| + pr_warn("[%s] failed to create a sysfs group\n", __func__); |
| + goto fail_kobject_put; |
| + } |
| + |
| + wakeup_irq_nodes_cache = |
| + kmem_cache_create("wakeup_irq_node_cache", |
| + sizeof(struct wakeup_irq_node), 0, 0, NULL); |
| + if (!wakeup_irq_nodes_cache) |
| + goto fail_remove_group; |
| + |
| + return 0; |
| + |
| +fail_remove_group: |
| + sysfs_remove_group(kobj, &attr_group); |
| +fail_kobject_put: |
| + kobject_put(kobj); |
| +fail_unregister_pm_notifier: |
| + unregister_pm_notifier(&wakeup_reason_pm_notifier_block); |
| +fail: |
| + return 1; |
| +} |
| + |
| +late_initcall(wakeup_reason_init); |