diff options
Diffstat (limited to 'drivers/power/qcom/lpm-stats.c')
-rw-r--r-- | drivers/power/qcom/lpm-stats.c | 871 |
1 files changed, 871 insertions, 0 deletions
diff --git a/drivers/power/qcom/lpm-stats.c b/drivers/power/qcom/lpm-stats.c new file mode 100644 index 000000000000..d3cafc411a77 --- /dev/null +++ b/drivers/power/qcom/lpm-stats.c @@ -0,0 +1,871 @@ +/* Copyright (c) 2012-2016, The Linux Foundation. All rights reserved. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2 and + * only version 2 as published by the Free Software Foundation. + * + * 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/module.h> +#include <linux/kernel.h> +#include <linux/init.h> +#include <linux/spinlock.h> +#include <linux/uaccess.h> +#include <linux/proc_fs.h> +#include <linux/seq_file.h> +#include <linux/debugfs.h> +#include <linux/sched.h> +#include <linux/slab.h> +#include <linux/suspend.h> +#include <soc/qcom/spm.h> +#include <soc/qcom/pm.h> +#include <soc/qcom/lpm-stats.h> + +#define MAX_STR_LEN 256 +#define MAX_TIME_LEN 20 +const char *lpm_stats_reset = "reset"; +const char *lpm_stats_suspend = "suspend"; + +struct lpm_sleep_time { + struct kobj_attribute ts_attr; + unsigned int cpu; +}; + +struct level_stats { + const char *name; + struct lpm_stats *owner; + int64_t first_bucket_time; + int bucket[CONFIG_MSM_IDLE_STATS_BUCKET_COUNT]; + int64_t min_time[CONFIG_MSM_IDLE_STATS_BUCKET_COUNT]; + int64_t max_time[CONFIG_MSM_IDLE_STATS_BUCKET_COUNT]; + int success_count; + int failed_count; + int64_t total_time; + uint64_t enter_time; +}; + +static struct level_stats suspend_time_stats; + +static DEFINE_PER_CPU_SHARED_ALIGNED(struct lpm_stats, cpu_stats); + +static uint64_t get_total_sleep_time(unsigned int cpu_id) +{ + struct lpm_stats *stats = &per_cpu(cpu_stats, cpu_id); + int i; + uint64_t ret = 0; + + for (i = 0; i < stats->num_levels; i++) + ret += stats->time_stats[i].total_time; + + return ret; +} + +static void update_level_stats(struct level_stats *stats, uint64_t t, + bool success) +{ + uint64_t bt; + int i; + + if (!success) { + stats->failed_count++; + return; + } + + stats->success_count++; + stats->total_time += t; + bt = t; + do_div(bt, stats->first_bucket_time); + + if (bt < 1ULL << (CONFIG_MSM_IDLE_STATS_BUCKET_SHIFT * + (CONFIG_MSM_IDLE_STATS_BUCKET_COUNT - 1))) + i = DIV_ROUND_UP(fls((uint32_t)bt), + CONFIG_MSM_IDLE_STATS_BUCKET_SHIFT); + else + i = CONFIG_MSM_IDLE_STATS_BUCKET_COUNT - 1; + + if (i >= CONFIG_MSM_IDLE_STATS_BUCKET_COUNT) + i = CONFIG_MSM_IDLE_STATS_BUCKET_COUNT - 1; + + stats->bucket[i]++; + + if (t < stats->min_time[i] || !stats->max_time[i]) + stats->min_time[i] = t; + if (t > stats->max_time[i]) + stats->max_time[i] = t; + return; +} + +static void level_stats_print(struct seq_file *m, struct level_stats *stats) +{ + int i = 0; + int64_t bucket_time = 0; + char seqs[MAX_STR_LEN] = {0}; + int64_t s = stats->total_time; + uint32_t ns = do_div(s, NSEC_PER_SEC); + + snprintf(seqs, MAX_STR_LEN, + "[%s] %s:\n" + " success count: %7d\n" + " total success time: %lld.%09u\n", + stats->owner->name, + stats->name, + stats->success_count, + s, ns); + seq_puts(m, seqs); + + if (stats->failed_count) { + snprintf(seqs, MAX_STR_LEN, " failed count: %7d\n", + stats->failed_count); + seq_puts(m, seqs); + } + + bucket_time = stats->first_bucket_time; + for (i = 0; + i < CONFIG_MSM_IDLE_STATS_BUCKET_COUNT - 1; + i++) { + s = bucket_time; + ns = do_div(s, NSEC_PER_SEC); + snprintf(seqs, MAX_STR_LEN, + "\t<%6lld.%09u: %7d (%lld-%lld)\n", + s, ns, stats->bucket[i], + stats->min_time[i], + stats->max_time[i]); + seq_puts(m, seqs); + bucket_time <<= CONFIG_MSM_IDLE_STATS_BUCKET_SHIFT; + } + snprintf(seqs, MAX_STR_LEN, + "\t>=%5lld.%09u:%8d (%lld-%lld)\n", + s, ns, stats->bucket[i], + stats->min_time[i], + stats->max_time[i]); + seq_puts(m, seqs); +} + +static int level_stats_file_show(struct seq_file *m, void *v) +{ + struct level_stats *stats = NULL; + + if (!m->private) + return -EINVAL; + + stats = (struct level_stats *) m->private; + + level_stats_print(m, stats); + + return 0; +} + +static int level_stats_file_open(struct inode *inode, struct file *file) +{ + return single_open(file, level_stats_file_show, inode->i_private); +} + +static void level_stats_print_all(struct seq_file *m, struct lpm_stats *stats) +{ + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + int i = 0; + + for (i = 0; i < stats->num_levels; i++) + level_stats_print(m, &stats->time_stats[i]); + + if (list_empty(&stats->child)) + return; + + centry = &stats->child; + list_for_each_entry(pos, centry, sibling) { + level_stats_print_all(m, pos); + } +} + +static void level_stats_reset(struct level_stats *stats) +{ + memset(stats->bucket, 0, sizeof(stats->bucket)); + memset(stats->min_time, 0, sizeof(stats->min_time)); + memset(stats->max_time, 0, sizeof(stats->max_time)); + stats->success_count = 0; + stats->failed_count = 0; + stats->total_time = 0; +} + +static void level_stats_reset_all(struct lpm_stats *stats) +{ + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + int i = 0; + + for (i = 0; i < stats->num_levels; i++) + level_stats_reset(&stats->time_stats[i]); + + if (list_empty(&stats->child)) + return; + + centry = &stats->child; + list_for_each_entry(pos, centry, sibling) { + level_stats_reset_all(pos); + } +} + +static int lpm_stats_file_show(struct seq_file *m, void *v) +{ + struct lpm_stats *stats = (struct lpm_stats *)m->private; + + if (!m->private) { + pr_err("%s: Invalid pdata, Cannot print stats\n", __func__); + return -EINVAL; + } + + level_stats_print_all(m, stats); + level_stats_print(m, &suspend_time_stats); + + return 0; +} + +static int lpm_stats_file_open(struct inode *inode, struct file *file) +{ + return single_open(file, lpm_stats_file_show, inode->i_private); +} + +static ssize_t level_stats_file_write(struct file *file, + const char __user *buffer, size_t count, loff_t *off) +{ + char buf[MAX_STR_LEN] = {0}; + struct inode *in = file->f_inode; + struct level_stats *stats = (struct level_stats *)in->i_private; + size_t len = strnlen(lpm_stats_reset, MAX_STR_LEN); + + if (!stats) + return -EINVAL; + + if (count != len+1) + return -EINVAL; + + if (copy_from_user(buf, buffer, len)) + return -EFAULT; + + if (strcmp(buf, lpm_stats_reset)) + return -EINVAL; + + level_stats_reset(stats); + + return count; +} + +static ssize_t lpm_stats_file_write(struct file *file, + const char __user *buffer, size_t count, loff_t *off) +{ + char buf[MAX_STR_LEN] = {0}; + struct inode *in = file->f_inode; + struct lpm_stats *stats = (struct lpm_stats *)in->i_private; + size_t len = strnlen(lpm_stats_reset, MAX_STR_LEN); + + if (!stats) + return -EINVAL; + + if (count != len+1) + return -EINVAL; + + if (copy_from_user(buf, buffer, len)) + return -EFAULT; + + if (strcmp(buf, lpm_stats_reset)) + return -EINVAL; + + level_stats_reset_all(stats); + + return count; +} + +int lifo_stats_file_show(struct seq_file *m, void *v) +{ + struct lpm_stats *stats = NULL; + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + char seqs[MAX_STR_LEN] = {0}; + + if (!m->private) + return -EINVAL; + + stats = (struct lpm_stats *)m->private; + + if (list_empty(&stats->child)) { + pr_err("%s: ERROR: Lifo level with no children.\n", + __func__); + return -EINVAL; + } + + centry = &stats->child; + list_for_each_entry(pos, centry, sibling) { + snprintf(seqs, MAX_STR_LEN, + "%s:\n" + "\tLast-In:%u\n" + "\tFirst-Out:%u\n", + pos->name, + pos->lifo.last_in, + pos->lifo.first_out); + seq_puts(m, seqs); + } + return 0; +} + +static int lifo_stats_file_open(struct inode *inode, struct file *file) +{ + return single_open(file, lifo_stats_file_show, inode->i_private); +} + +static void lifo_stats_reset_all(struct lpm_stats *stats) +{ + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + + centry = &stats->child; + list_for_each_entry(pos, centry, sibling) { + pos->lifo.last_in = 0; + pos->lifo.first_out = 0; + if (!list_empty(&pos->child)) + lifo_stats_reset_all(pos); + } +} + +static ssize_t lifo_stats_file_write(struct file *file, + const char __user *buffer, size_t count, loff_t *off) +{ + char buf[MAX_STR_LEN] = {0}; + struct inode *in = file->f_inode; + struct lpm_stats *stats = (struct lpm_stats *)in->i_private; + size_t len = strnlen(lpm_stats_reset, MAX_STR_LEN); + + if (!stats) + return -EINVAL; + + if (count != len+1) + return -EINVAL; + + if (copy_from_user(buf, buffer, len)) + return -EFAULT; + + if (strcmp(buf, lpm_stats_reset)) + return -EINVAL; + + lifo_stats_reset_all(stats); + + return count; +} + +static const struct file_operations level_stats_fops = { + .owner = THIS_MODULE, + .open = level_stats_file_open, + .read = seq_read, + .release = single_release, + .llseek = no_llseek, + .write = level_stats_file_write, +}; + +static const struct file_operations lpm_stats_fops = { + .owner = THIS_MODULE, + .open = lpm_stats_file_open, + .read = seq_read, + .release = single_release, + .llseek = no_llseek, + .write = lpm_stats_file_write, +}; + +static const struct file_operations lifo_stats_fops = { + .owner = THIS_MODULE, + .open = lifo_stats_file_open, + .read = seq_read, + .release = single_release, + .llseek = no_llseek, + .write = lifo_stats_file_write, +}; + +static void update_last_in_stats(struct lpm_stats *stats) +{ + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + + if (list_empty(&stats->child)) + return; + + centry = &stats->child; + list_for_each_entry(pos, centry, sibling) { + if (cpumask_test_cpu(smp_processor_id(), &pos->mask)) { + pos->lifo.last_in++; + return; + } + } + WARN(1, "Should not reach here\n"); +} + +static void update_first_out_stats(struct lpm_stats *stats) +{ + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + + if (list_empty(&stats->child)) + return; + + centry = &stats->child; + list_for_each_entry(pos, centry, sibling) { + if (cpumask_test_cpu(smp_processor_id(), &pos->mask)) { + pos->lifo.first_out++; + return; + } + } + WARN(1, "Should not reach here\n"); +} + +static inline void update_exit_stats(struct lpm_stats *stats, uint32_t index, + bool success) +{ + uint64_t exit_time = 0; + + /* Update time stats only when exit is preceded by enter */ + exit_time = stats->sleep_time; + update_level_stats(&stats->time_stats[index], exit_time, + success); +} + +static int config_level(const char *name, const char **levels, + int num_levels, struct lpm_stats *parent, struct lpm_stats *stats) +{ + int i = 0; + struct dentry *directory = NULL; + const char *rootname = "lpm_stats"; + const char *dirname = rootname; + + strlcpy(stats->name, name, MAX_STR_LEN); + stats->num_levels = num_levels; + stats->parent = parent; + INIT_LIST_HEAD(&stats->sibling); + INIT_LIST_HEAD(&stats->child); + + stats->time_stats = kzalloc(sizeof(struct level_stats) * + num_levels, GFP_KERNEL); + if (!stats->time_stats) { + pr_err("%s: Insufficient memory for %s level time stats\n", + __func__, name); + return -ENOMEM; + } + + if (parent) { + list_add_tail(&stats->sibling, &parent->child); + directory = parent->directory; + dirname = name; + } + + stats->directory = debugfs_create_dir(dirname, directory); + if (!stats->directory) { + pr_err("%s: Unable to create %s debugfs directory\n", + __func__, dirname); + kfree(stats->time_stats); + return -EPERM; + } + + for (i = 0; i < num_levels; i++) { + stats->time_stats[i].name = levels[i]; + stats->time_stats[i].owner = stats; + stats->time_stats[i].first_bucket_time = + CONFIG_MSM_IDLE_STATS_FIRST_BUCKET; + stats->time_stats[i].enter_time = 0; + + if (!debugfs_create_file(stats->time_stats[i].name, S_IRUGO, + stats->directory, (void *)&stats->time_stats[i], + &level_stats_fops)) { + pr_err("%s: Unable to create %s %s level-stats file\n", + __func__, stats->name, + stats->time_stats[i].name); + kfree(stats->time_stats); + return -EPERM; + } + } + + if (!debugfs_create_file("stats", S_IRUGO, stats->directory, + (void *)stats, &lpm_stats_fops)) { + pr_err("%s: Unable to create %s's overall 'stats' file\n", + __func__, stats->name); + kfree(stats->time_stats); + return -EPERM; + } + + return 0; +} + +static ssize_t total_sleep_time_show(struct kobject *kobj, + struct kobj_attribute *attr, char *buf) +{ + struct lpm_sleep_time *cpu_sleep_time = container_of(attr, + struct lpm_sleep_time, ts_attr); + unsigned int cpu = cpu_sleep_time->cpu; + uint64_t total_time = get_total_sleep_time(cpu); + + return snprintf(buf, MAX_TIME_LEN, "%llu.%09u\n", total_time, + do_div(total_time, NSEC_PER_SEC)); +} + +static struct kobject *local_module_kobject(void) +{ + struct kobject *kobj; + + kobj = kset_find_obj(module_kset, KBUILD_MODNAME); + + if (!kobj) { + int err; + struct module_kobject *mk; + + mk = kzalloc(sizeof(*mk), GFP_KERNEL); + if (!mk) + return ERR_PTR(-ENOMEM); + + mk->mod = THIS_MODULE; + mk->kobj.kset = module_kset; + + err = kobject_init_and_add(&mk->kobj, &module_ktype, NULL, + "%s", KBUILD_MODNAME); + + if (err) { + kobject_put(&mk->kobj); + kfree(mk); + pr_err("%s: cannot create kobject for %s\n", + __func__, KBUILD_MODNAME); + return ERR_PTR(err); + } + + kobject_get(&mk->kobj); + kobj = &mk->kobj; + } + + return kobj; +} + +static int create_sysfs_node(unsigned int cpu, struct lpm_stats *stats) +{ + struct kobject *cpu_kobj = NULL; + struct lpm_sleep_time *ts = NULL; + struct kobject *stats_kobj; + char cpu_name[] = "cpuXX"; + int ret = -ENOMEM; + + stats_kobj = local_module_kobject(); + + if (IS_ERR_OR_NULL(stats_kobj)) + return PTR_ERR(stats_kobj); + + snprintf(cpu_name, sizeof(cpu_name), "cpu%u", cpu); + cpu_kobj = kobject_create_and_add(cpu_name, stats_kobj); + if (!cpu_kobj) + return -ENOMEM; + + ts = kzalloc(sizeof(*ts), GFP_KERNEL); + if (!ts) + goto failed; + + sysfs_attr_init(&ts->ts_attr.attr); + ts->ts_attr.attr.name = "total_sleep_time_secs"; + ts->ts_attr.attr.mode = 0444; + ts->ts_attr.show = total_sleep_time_show; + ts->ts_attr.store = NULL; + ts->cpu = cpu; + + ret = sysfs_create_file(cpu_kobj, &ts->ts_attr.attr); + if (ret) + goto failed; + + return 0; + +failed: + kfree(ts); + kobject_put(cpu_kobj); + return ret; +} + +static struct lpm_stats *config_cpu_level(const char *name, + const char **levels, int num_levels, struct lpm_stats *parent, + struct cpumask *mask) +{ + int cpu = 0; + struct lpm_stats *pstats = NULL; + struct lpm_stats *stats = NULL; + + for (pstats = parent; pstats; pstats = pstats->parent) + cpumask_or(&pstats->mask, &pstats->mask, mask); + + for_each_cpu(cpu, mask) { + int ret = 0; + char cpu_name[MAX_STR_LEN] = {0}; + + stats = &per_cpu(cpu_stats, cpu); + snprintf(cpu_name, MAX_STR_LEN, "%s%d", name, cpu); + cpumask_set_cpu(cpu, &stats->mask); + + stats->is_cpu = true; + + ret = config_level(cpu_name, levels, num_levels, parent, + stats); + if (ret) { + pr_err("%s: Unable to create %s stats\n", + __func__, cpu_name); + return ERR_PTR(ret); + } + + ret = create_sysfs_node(cpu, stats); + + if (ret) { + pr_err("Could not create the sysfs node\n"); + return ERR_PTR(ret); + } + } + + return stats; +} + +static void config_suspend_level(struct lpm_stats *stats) +{ + suspend_time_stats.name = lpm_stats_suspend; + suspend_time_stats.owner = stats; + suspend_time_stats.first_bucket_time = + CONFIG_MSM_SUSPEND_STATS_FIRST_BUCKET; + suspend_time_stats.enter_time = 0; + suspend_time_stats.success_count = 0; + suspend_time_stats.failed_count = 0; + + if (!debugfs_create_file(suspend_time_stats.name, S_IRUGO, + stats->directory, (void *)&suspend_time_stats, + &level_stats_fops)) + pr_err("%s: Unable to create %s Suspend stats file\n", + __func__, stats->name); +} + +static struct lpm_stats *config_cluster_level(const char *name, + const char **levels, int num_levels, struct lpm_stats *parent) +{ + struct lpm_stats *stats = NULL; + int ret = 0; + + stats = kzalloc(sizeof(struct lpm_stats), GFP_KERNEL); + if (!stats) { + pr_err("%s: Insufficient memory for %s stats\n", + __func__, name); + return ERR_PTR(-ENOMEM); + } + + stats->is_cpu = false; + + ret = config_level(name, levels, num_levels, parent, stats); + if (ret) { + pr_err("%s: Unable to create %s stats\n", __func__, + name); + kfree(stats); + return ERR_PTR(ret); + } + + if (!debugfs_create_file("lifo", S_IRUGO, stats->directory, + (void *)stats, &lifo_stats_fops)) { + pr_err("%s: Unable to create %s lifo stats file\n", + __func__, stats->name); + kfree(stats); + return ERR_PTR(-EPERM); + } + + if (!parent) + config_suspend_level(stats); + + return stats; +} + +static void cleanup_stats(struct lpm_stats *stats) +{ + struct list_head *centry = NULL; + struct lpm_stats *pos = NULL; + + centry = &stats->child; + list_for_each_entry_reverse(pos, centry, sibling) { + if (!list_empty(&pos->child)) + cleanup_stats(pos); + + list_del_init(&pos->child); + + kfree(pos->time_stats); + if (!pos->is_cpu) + kfree(pos); + } + kfree(stats->time_stats); + kfree(stats); +} + +static void lpm_stats_cleanup(struct lpm_stats *stats) +{ + struct lpm_stats *pstats = stats; + + if (!pstats) + return; + + while (pstats->parent) + pstats = pstats->parent; + + debugfs_remove_recursive(pstats->directory); + + cleanup_stats(pstats); +} + +/** + * lpm_stats_config_level() - API to configure levels stats. + * + * @name: Name of the cluster/cpu. + * @levels: Low power mode level names. + * @num_levels: Number of leves supported. + * @parent: Pointer to the parent's lpm_stats object. + * @mask: cpumask, if configuring cpu stats, else NULL. + * + * Function to communicate the low power mode levels supported by + * cpus or a cluster. + * + * Return: Pointer to the lpm_stats object or ERR_PTR(-ERRNO) + */ +struct lpm_stats *lpm_stats_config_level(const char *name, + const char **levels, int num_levels, struct lpm_stats *parent, + struct cpumask *mask) +{ + struct lpm_stats *stats = NULL; + + if (!levels || num_levels <= 0 || IS_ERR(parent)) { + pr_err("%s: Invalid input\n\t\tlevels = %p\n\t\t" + "num_levels = %d\n\t\tparent = %ld\n", + __func__, levels, num_levels, PTR_ERR(parent)); + return ERR_PTR(-EINVAL); + } + + if (mask) + stats = config_cpu_level(name, levels, num_levels, parent, + mask); + else + stats = config_cluster_level(name, levels, num_levels, + parent); + + if (IS_ERR(stats)) { + lpm_stats_cleanup(parent); + return stats; + } + + return stats; +} +EXPORT_SYMBOL(lpm_stats_config_level); + +/** + * lpm_stats_cluster_enter() - API to communicate the lpm level a cluster + * is prepared to enter. + * + * @stats: Pointer to the cluster's lpm_stats object. + * @index: Index of the lpm level that the cluster is going to enter. + * + * Function to communicate the low power mode level that the cluster is + * prepared to enter. + */ +void lpm_stats_cluster_enter(struct lpm_stats *stats, uint32_t index) +{ + if (IS_ERR_OR_NULL(stats)) + return; + + update_last_in_stats(stats); +} +EXPORT_SYMBOL(lpm_stats_cluster_enter); + +/** + * lpm_stats_cluster_exit() - API to communicate the lpm level a cluster + * exited. + * + * @stats: Pointer to the cluster's lpm_stats object. + * @index: Index of the cluster lpm level. + * @success: Success/Failure of the low power mode execution. + * + * Function to communicate the low power mode level that the cluster + * exited. + */ +void lpm_stats_cluster_exit(struct lpm_stats *stats, uint32_t index, + bool success) +{ + if (IS_ERR_OR_NULL(stats)) + return; + + update_exit_stats(stats, index, success); + + update_first_out_stats(stats); +} +EXPORT_SYMBOL(lpm_stats_cluster_exit); + +/** + * lpm_stats_cpu_enter() - API to communicate the lpm level a cpu + * is prepared to enter. + * + * @index: cpu's lpm level index. + * + * Function to communicate the low power mode level that the cpu is + * prepared to enter. + */ +void lpm_stats_cpu_enter(uint32_t index, uint64_t time) +{ + struct lpm_stats *stats = &(*this_cpu_ptr(&(cpu_stats))); + + stats->sleep_time = time; + + if (!stats->time_stats) + return; + +} +EXPORT_SYMBOL(lpm_stats_cpu_enter); + +/** + * lpm_stats_cpu_exit() - API to communicate the lpm level that the cpu exited. + * + * @index: cpu's lpm level index. + * @success: Success/Failure of the low power mode execution. + * + * Function to communicate the low power mode level that the cpu exited. + */ +void lpm_stats_cpu_exit(uint32_t index, uint64_t time, bool success) +{ + struct lpm_stats *stats = &(*this_cpu_ptr(&(cpu_stats))); + + if (!stats->time_stats) + return; + + stats->sleep_time = time - stats->sleep_time; + + update_exit_stats(stats, index, success); +} +EXPORT_SYMBOL(lpm_stats_cpu_exit); + +/** + * lpm_stats_suspend_enter() - API to communicate system entering suspend. + * + * Function to communicate that the system is ready to enter suspend. + */ +void lpm_stats_suspend_enter(void) +{ + struct timespec ts; + + getnstimeofday(&ts); + suspend_time_stats.enter_time = timespec_to_ns(&ts); +} +EXPORT_SYMBOL(lpm_stats_suspend_enter); + +/** + * lpm_stats_suspend_exit() - API to communicate system exiting suspend. + * + * Function to communicate that the system exited suspend. + */ +void lpm_stats_suspend_exit(void) +{ + struct timespec ts; + uint64_t exit_time = 0; + + getnstimeofday(&ts); + exit_time = timespec_to_ns(&ts) - suspend_time_stats.enter_time; + update_level_stats(&suspend_time_stats, exit_time, true); +} +EXPORT_SYMBOL(lpm_stats_suspend_exit); |