/* Copyright (c) 2010, Code Aurora Forum. 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 #include "idle_stats.h" #include /****************************************************************************** * Debug Definitions *****************************************************************************/ enum { MSM_IDLE_STATS_DEBUG_API = BIT(0), MSM_IDLE_STATS_DEBUG_SIGNAL = BIT(1), MSM_IDLE_STATS_DEBUG_MIGRATION = BIT(2), }; static int msm_idle_stats_debug_mask; module_param_named( debug_mask, msm_idle_stats_debug_mask, int, S_IRUGO | S_IWUSR | S_IWGRP ); /****************************************************************************** * Driver Definitions *****************************************************************************/ #define MSM_IDLE_STATS_DRIVER_NAME "msm_idle_stats" static dev_t msm_idle_stats_dev_nr; static struct cdev msm_idle_stats_cdev; static struct class *msm_idle_stats_class; /****************************************************************************** * Device Definitions *****************************************************************************/ struct msm_idle_stats_device { unsigned int cpu; struct mutex mutex; struct notifier_block notifier; int64_t collection_expiration; struct msm_idle_stats stats; struct hrtimer timer; wait_queue_head_t wait_q; atomic_t collecting; }; static DEFINE_SPINLOCK(msm_idle_stats_devs_lock); static DEFINE_PER_CPU(struct msm_idle_stats_device *, msm_idle_stats_devs); /****************************************************************************** * *****************************************************************************/ static inline int64_t msm_idle_stats_bound_interval(int64_t interval) { if (interval <= 0) return 1; if (interval > UINT_MAX) return UINT_MAX; return interval; } static enum hrtimer_restart msm_idle_stats_timer(struct hrtimer *timer) { struct msm_idle_stats_device *stats_dev; unsigned int cpu; int64_t now; int64_t interval; stats_dev = container_of(timer, struct msm_idle_stats_device, timer); cpu = get_cpu(); if (cpu != stats_dev->cpu) { if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_MIGRATION) pr_info("%s: timer migrated from cpu%u to cpu%u\n", __func__, stats_dev->cpu, cpu); stats_dev->stats.event = MSM_IDLE_STATS_EVENT_TIMER_MIGRATED; goto timer_exit; } now = ktime_to_us(ktime_get()); interval = now - stats_dev->stats.last_busy_start; if (stats_dev->stats.busy_timer > 0 && interval >= stats_dev->stats.busy_timer - 1) stats_dev->stats.event = MSM_IDLE_STATS_EVENT_BUSY_TIMER_EXPIRED; else stats_dev->stats.event = MSM_IDLE_STATS_EVENT_COLLECTION_TIMER_EXPIRED; timer_exit: atomic_set(&stats_dev->collecting, 0); wake_up_interruptible(&stats_dev->wait_q); put_cpu(); return HRTIMER_NORESTART; } static void msm_idle_stats_pre_idle(struct msm_idle_stats_device *stats_dev) { int64_t now; int64_t interval; if (smp_processor_id() != stats_dev->cpu) { WARN_ON(1); return; } if (!atomic_read(&stats_dev->collecting)) return; hrtimer_cancel(&stats_dev->timer); now = ktime_to_us(ktime_get()); interval = now - stats_dev->stats.last_busy_start; interval = msm_idle_stats_bound_interval(interval); stats_dev->stats.busy_intervals[stats_dev->stats.nr_collected] = (__u32) interval; stats_dev->stats.last_idle_start = now; } static void msm_idle_stats_post_idle(struct msm_idle_stats_device *stats_dev) { int64_t now; int64_t interval; int64_t timer_interval; int rc; if (smp_processor_id() != stats_dev->cpu) { WARN_ON(1); return; } if (!atomic_read(&stats_dev->collecting)) return; now = ktime_to_us(ktime_get()); interval = now - stats_dev->stats.last_idle_start; interval = msm_idle_stats_bound_interval(interval); stats_dev->stats.idle_intervals[stats_dev->stats.nr_collected] = (__u32) interval; stats_dev->stats.nr_collected++; stats_dev->stats.last_busy_start = now; if (stats_dev->stats.nr_collected >= MSM_IDLE_STATS_NR_MAX_INTERVALS) { stats_dev->stats.event = MSM_IDLE_STATS_EVENT_COLLECTION_FULL; goto post_idle_collection_done; } timer_interval = stats_dev->collection_expiration - now; if (timer_interval <= 0) { stats_dev->stats.event = MSM_IDLE_STATS_EVENT_COLLECTION_TIMER_EXPIRED; goto post_idle_collection_done; } if (stats_dev->stats.busy_timer > 0 && timer_interval > stats_dev->stats.busy_timer) timer_interval = stats_dev->stats.busy_timer; rc = hrtimer_start(&stats_dev->timer, ktime_set(0, timer_interval * 1000), HRTIMER_MODE_REL_PINNED); WARN_ON(rc); return; post_idle_collection_done: atomic_set(&stats_dev->collecting, 0); wake_up_interruptible(&stats_dev->wait_q); } static int msm_idle_stats_notified(struct notifier_block *nb, unsigned long val, void *v) { struct msm_idle_stats_device *stats_dev = container_of( nb, struct msm_idle_stats_device, notifier); if (val == MSM_CPUIDLE_STATE_EXIT) msm_idle_stats_post_idle(stats_dev); else msm_idle_stats_pre_idle(stats_dev); return 0; } static int msm_idle_stats_collect(struct file *filp, unsigned int cmd, unsigned long arg) { struct msm_idle_stats_device *stats_dev; struct msm_idle_stats *stats; int rc; stats_dev = (struct msm_idle_stats_device *) filp->private_data; stats = &stats_dev->stats; rc = mutex_lock_interruptible(&stats_dev->mutex); if (rc) { if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_SIGNAL) pr_info("%s: interrupted while waiting on device " "mutex\n", __func__); rc = -EINTR; goto collect_exit; } if (atomic_read(&stats_dev->collecting)) { pr_err("%s: inconsistent state\n", __func__); rc = -EBUSY; goto collect_unlock_exit; } rc = copy_from_user(stats, (void *)arg, sizeof(*stats)); if (rc) { rc = -EFAULT; goto collect_unlock_exit; } if (stats->nr_collected >= MSM_IDLE_STATS_NR_MAX_INTERVALS || stats->busy_timer > MSM_IDLE_STATS_MAX_TIMER || stats->collection_timer > MSM_IDLE_STATS_MAX_TIMER) { rc = -EINVAL; goto collect_unlock_exit; } if (get_cpu() != stats_dev->cpu) { put_cpu(); rc = -EACCES; goto collect_unlock_exit; } /* * When collection_timer == 0, stop collecting at the next * post idle. */ stats_dev->collection_expiration = ktime_to_us(ktime_get()) + stats->collection_timer; /* * Enable collection before starting any timer. */ atomic_set(&stats_dev->collecting, 1); /* * When busy_timer == 0, do not set any busy timer. */ if (stats->busy_timer > 0) { rc = hrtimer_start(&stats_dev->timer, ktime_set(0, stats->busy_timer * 1000), HRTIMER_MODE_REL_PINNED); WARN_ON(rc); } put_cpu(); if (wait_event_interruptible(stats_dev->wait_q, !atomic_read(&stats_dev->collecting))) { if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_SIGNAL) pr_info("%s: interrupted while waiting on " "collection\n", __func__); hrtimer_cancel(&stats_dev->timer); atomic_set(&stats_dev->collecting, 0); rc = -EINTR; goto collect_unlock_exit; } stats->return_timestamp = ktime_to_us(ktime_get()); rc = copy_to_user((void *)arg, stats, sizeof(*stats)); if (rc) { rc = -EFAULT; goto collect_unlock_exit; } collect_unlock_exit: mutex_unlock(&stats_dev->mutex); collect_exit: return rc; } static int msm_idle_stats_open(struct inode *inode, struct file *filp) { struct msm_idle_stats_device *stats_dev; int rc; if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: enter\n", __func__); rc = nonseekable_open(inode, filp); if (rc) { pr_err("%s: failed to set nonseekable\n", __func__); goto open_bail; } stats_dev = (struct msm_idle_stats_device *) kzalloc(sizeof(*stats_dev), GFP_KERNEL); if (!stats_dev) { pr_err("%s: failed to allocate device struct\n", __func__); rc = -ENOMEM; goto open_bail; } stats_dev->cpu = MINOR(inode->i_rdev); mutex_init(&stats_dev->mutex); stats_dev->notifier.notifier_call = msm_idle_stats_notified; hrtimer_init(&stats_dev->timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL_PINNED); stats_dev->timer.function = msm_idle_stats_timer; init_waitqueue_head(&stats_dev->wait_q); atomic_set(&stats_dev->collecting, 0); filp->private_data = stats_dev; /* * Make sure only one device exists per cpu. */ spin_lock(&msm_idle_stats_devs_lock); if (per_cpu(msm_idle_stats_devs, stats_dev->cpu)) { spin_unlock(&msm_idle_stats_devs_lock); rc = -EBUSY; goto open_free_bail; } per_cpu(msm_idle_stats_devs, stats_dev->cpu) = stats_dev; spin_unlock(&msm_idle_stats_devs_lock); rc = msm_cpuidle_register_notifier(stats_dev->cpu, &stats_dev->notifier); if (rc) { pr_err("%s: failed to register idle notification\n", __func__); goto open_null_bail; } if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: done\n", __func__); return 0; open_null_bail: spin_lock(&msm_idle_stats_devs_lock); per_cpu(msm_idle_stats_devs, stats_dev->cpu) = NULL; spin_unlock(&msm_idle_stats_devs_lock); open_free_bail: kfree(stats_dev); open_bail: if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: exit, %d\n", __func__, rc); return rc; } static int msm_idle_stats_release(struct inode *inode, struct file *filp) { struct msm_idle_stats_device *stats_dev; int rc; if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: enter\n", __func__); stats_dev = (struct msm_idle_stats_device *) filp->private_data; rc = msm_cpuidle_unregister_notifier(stats_dev->cpu, &stats_dev->notifier); WARN_ON(rc); spin_lock(&msm_idle_stats_devs_lock); per_cpu(msm_idle_stats_devs, stats_dev->cpu) = NULL; spin_unlock(&msm_idle_stats_devs_lock); filp->private_data = NULL; hrtimer_cancel(&stats_dev->timer); kfree(stats_dev); if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: done\n", __func__); return 0; } static long msm_idle_stats_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { int rc; if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: enter\n", __func__); switch (cmd) { case MSM_IDLE_STATS_IOC_COLLECT: rc = msm_idle_stats_collect(filp, cmd, arg); break; default: rc = -ENOTTY; break; } if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: exit, %d\n", __func__, rc); return rc; } /****************************************************************************** * *****************************************************************************/ static const struct file_operations msm_idle_stats_fops = { .owner = THIS_MODULE, .open = msm_idle_stats_open, .release = msm_idle_stats_release, .unlocked_ioctl = msm_idle_stats_ioctl, }; static int __init msm_idle_stats_init(void) { unsigned int nr_cpus = num_possible_cpus(); struct device *dev; int rc; int i; if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: enter\n", __func__); rc = alloc_chrdev_region(&msm_idle_stats_dev_nr, 0, nr_cpus, MSM_IDLE_STATS_DRIVER_NAME); if (rc) { pr_err("%s: failed to allocate device number, rc %d\n", __func__, rc); goto init_bail; } msm_idle_stats_class = class_create(THIS_MODULE, MSM_IDLE_STATS_DRIVER_NAME); if (IS_ERR(msm_idle_stats_class)) { pr_err("%s: failed to create device class\n", __func__); rc = -ENOMEM; goto init_unreg_bail; } for (i = 0; i < nr_cpus; i++) { dev = device_create(msm_idle_stats_class, NULL, msm_idle_stats_dev_nr + i, NULL, MSM_IDLE_STATS_DRIVER_NAME "%d", i); if (!dev) { pr_err("%s: failed to create device %d\n", __func__, i); rc = -ENOMEM; goto init_remove_bail; } } cdev_init(&msm_idle_stats_cdev, &msm_idle_stats_fops); msm_idle_stats_cdev.owner = THIS_MODULE; /* * Call cdev_add() last, after everything else is initialized and * the driver is ready to accept system calls. */ rc = cdev_add(&msm_idle_stats_cdev, msm_idle_stats_dev_nr, nr_cpus); if (rc) { pr_err("%s: failed to register char device, rc %d\n", __func__, rc); goto init_remove_bail; } if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: done\n", __func__); return 0; init_remove_bail: for (i = i - 1; i >= 0; i--) device_destroy( msm_idle_stats_class, msm_idle_stats_dev_nr + i); class_destroy(msm_idle_stats_class); init_unreg_bail: unregister_chrdev_region(msm_idle_stats_dev_nr, nr_cpus); init_bail: if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: exit, %d\n", __func__, rc); return rc; } static void __exit msm_idle_stats_exit(void) { unsigned int nr_cpus = num_possible_cpus(); int i; if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: enter\n", __func__); cdev_del(&msm_idle_stats_cdev); for (i = nr_cpus - 1; i >= 0; i--) device_destroy( msm_idle_stats_class, msm_idle_stats_dev_nr + i); class_destroy(msm_idle_stats_class); unregister_chrdev_region(msm_idle_stats_dev_nr, nr_cpus); if (msm_idle_stats_debug_mask & MSM_IDLE_STATS_DEBUG_API) pr_info("%s: done\n", __func__); } module_init(msm_idle_stats_init); module_exit(msm_idle_stats_exit); MODULE_LICENSE("GPL v2"); MODULE_DESCRIPTION("idle stats driver"); MODULE_VERSION("1.0");