/* Copyright (c) 2012-2016, 2018 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 #include #include #include #include #include #include #include #include #include #include #include #include #include #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; struct lpm_stats *n = NULL; centry = &stats->child; list_for_each_entry_safe_reverse(pos, n, centry, sibling) { if (!list_empty(&pos->child)) { cleanup_stats(pos); continue; } 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);