From: Chad Goodman Date: Thu, 20 Dec 2012 07:14:58 +0000 (-0800) Subject: MPDECISION: updated mpdecision, 8 steps, optimized for 8960 devices X-Git-Url: https://www.ziggy471.com/git/gitweb.cgi?p=ziggy471-sgs3-jb.git;a=commitdiff;h=54acf263127f8db11a0b9069e06b4c8f00e83834 MPDECISION: updated mpdecision, 8 steps, optimized for 8960 devices Signed-off-by: Ziggy --- --- a/arch/arm/mach-msm/msm_mpdecision.c +++ b/arch/arm/mach-msm/msm_mpdecision.c @@ -1,10 +1,12 @@ /* * arch/arm/mach-msm/msm_mpdecision.c * - * cpu auto-hotplug/unplug based on system load for MSM dualcore cpus - * single core while screen is off + * This program features: + * -cpu auto-hotplug/unplug based on system load for MSM multicore cpus + * -single core while screen is off + * -extensive sysfs tuneables * - * Copyright (c) 2011, Chad Goodman - All Rights Reserved + * Copyright (c) 2011-2012, Chad Goodman - 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 as published by @@ -31,12 +33,13 @@ #include #include #include - #include "acpuclock.h" -#define MPDEC_TAG "[AnthraX-MPD]: " -#define MSM_MPDEC_STARTDELAY 70000 -#define MSM_MPDEC_DELAY 500 +#define DEBUG 0 + +#define MPDEC_TAG "[AnThRaX MPDEC]: " +#define MSM_MPDEC_STARTDELAY 20000 +#define MSM_MPDEC_DELAY 70 #define MSM_MPDEC_PAUSE 10000 #define MSM_MPDEC_IDLE_FREQ 486000 @@ -56,7 +59,8 @@ struct msm_mpdec_cpudata_t { static DEFINE_PER_CPU(struct msm_mpdec_cpudata_t, msm_mpdec_cpudata); static struct delayed_work msm_mpdec_work; -static DEFINE_MUTEX(msm_cpu_lock); +static struct workqueue_struct *msm_mpdec_workq; +static DEFINE_MUTEX(mpdec_msm_cpu_lock); static struct msm_mpdec_tuners { unsigned int startdelay; @@ -64,22 +68,75 @@ static struct msm_mpdec_tuners { unsigned int pause; bool scroff_single_core; unsigned long int idle_freq; + unsigned int max_cpus; + unsigned int min_cpus; } msm_mpdec_tuners_ins = { .startdelay = MSM_MPDEC_STARTDELAY, .delay = MSM_MPDEC_DELAY, .pause = MSM_MPDEC_PAUSE, .scroff_single_core = true, .idle_freq = MSM_MPDEC_IDLE_FREQ, + .max_cpus = CONFIG_NR_CPUS, + .min_cpus = 1, }; -static unsigned int NwNs_Threshold[4] = {35, 0, 0, 5}; -static unsigned int TwTs_Threshold[4] = {250, 0, 0, 250}; +static unsigned int NwNs_Threshold[8] = {19, 10, 28, 30, 32, 30, 0, 35}; +static unsigned int TwTs_Threshold[8] = {140, 0, 140, 190, 140, 190, 0, 190}; extern unsigned int get_rq_info(void); +extern unsigned long acpuclk_get_rate(int); unsigned int state = MSM_MPDEC_IDLE; bool was_paused = false; +static unsigned long get_rate(int cpu) +{ + return acpuclk_get_rate(cpu); +} + +static int get_slowest_cpu(void) +{ + int i, cpu = 0; + unsigned long rate, slow_rate = 0; + + for (i = 1; i < CONFIG_NR_CPUS; i++) { + if (!cpu_online(i)) + continue; + + rate = get_rate(i); + if (slow_rate == 0) { + slow_rate = rate; + } + + if ((rate <= slow_rate) && (slow_rate != 0)) { + cpu = i; + slow_rate = rate; + } + } + + return cpu; +} + +static unsigned long get_slowest_cpu_rate(void) +{ + int i = 0; + unsigned long rate, slow_rate = 0; + + for (i = 0; i < CONFIG_NR_CPUS; i++) { + if (!cpu_online(i)) + continue; + rate = get_rate(i); + if ((rate < slow_rate) && (slow_rate != 0)) { + slow_rate = rate; + } + if (slow_rate == 0) { + slow_rate = rate; + } + } + + return slow_rate; +} + static int mp_decision(void) { static bool first_call = true; @@ -111,20 +168,19 @@ static int mp_decision(void) if (nr_cpu_online) { index = (nr_cpu_online - 1) * 2; - if ((nr_cpu_online < 2) && (rq_depth >= NwNs_Threshold[index])) { - if (total_time >= TwTs_Threshold[index]) { + if ((nr_cpu_online < CONFIG_NR_CPUS) && (rq_depth >= NwNs_Threshold[index])) { + if ((total_time >= TwTs_Threshold[index]) && + (nr_cpu_online < msm_mpdec_tuners_ins.max_cpus)) { new_state = MSM_MPDEC_UP; - if (acpuclk_get_rate((CONFIG_NR_CPUS - 2)) <= - msm_mpdec_tuners_ins.idle_freq) + if (get_slowest_cpu_rate() <= msm_mpdec_tuners_ins.idle_freq) new_state = MSM_MPDEC_IDLE; } - } else if (rq_depth <= NwNs_Threshold[index+1]) { - if (total_time >= TwTs_Threshold[index+1] ) { + } else if ((nr_cpu_online > 1) && (rq_depth <= NwNs_Threshold[index+1])) { + if ((total_time >= TwTs_Threshold[index+1]) && + (nr_cpu_online > msm_mpdec_tuners_ins.min_cpus)) { new_state = MSM_MPDEC_DOWN; - if (cpu_online((CONFIG_NR_CPUS - 1))) - if (acpuclk_get_rate((CONFIG_NR_CPUS - 1)) > - msm_mpdec_tuners_ins.idle_freq) - new_state = MSM_MPDEC_IDLE; + if (get_slowest_cpu_rate() > msm_mpdec_tuners_ins.idle_freq) + new_state = MSM_MPDEC_IDLE; } } else { new_state = MSM_MPDEC_IDLE; @@ -139,7 +195,10 @@ static int mp_decision(void) } last_time = ktime_to_ms(ktime_get()); - +#if DEBUG + pr_info(MPDEC_TAG"[DEBUG] rq: %u, new_state: %i | Mask=[%d%d%d%d]\n", + rq_depth, new_state, cpu_online(0), cpu_online(1), cpu_online(2), cpu_online(3)); +#endif return new_state; } @@ -147,13 +206,24 @@ static void msm_mpdec_work_thread(struct { unsigned int cpu = nr_cpu_ids; cputime64_t on_time = 0; + bool suspended = false; - if (per_cpu(msm_mpdec_cpudata, (CONFIG_NR_CPUS - 1)).device_suspended == true) + if (ktime_to_ms(ktime_get()) <= msm_mpdec_tuners_ins.startdelay) + goto out; + + for_each_possible_cpu(cpu) { + if ((per_cpu(msm_mpdec_cpudata, cpu).device_suspended == true)) { + suspended = true; + break; + } + } + if (suspended == true) goto out; - if (!mutex_trylock(&msm_cpu_lock)) + if (!mutex_trylock(&mpdec_msm_cpu_lock)) goto out; + /* if sth messed with the cpus, update the check vars so we can proceed */ if (was_paused) { for_each_possible_cpu(cpu) { if (cpu_online(cpu)) @@ -170,14 +240,14 @@ static void msm_mpdec_work_thread(struct case MSM_MPDEC_IDLE: break; case MSM_MPDEC_DOWN: - cpu = (CONFIG_NR_CPUS - 1); + cpu = get_slowest_cpu(); if (cpu < nr_cpu_ids) { if ((per_cpu(msm_mpdec_cpudata, cpu).online == true) && (cpu_online(cpu))) { cpu_down(cpu); per_cpu(msm_mpdec_cpudata, cpu).online = false; on_time = ktime_to_ms(ktime_get()) - per_cpu(msm_mpdec_cpudata, cpu).on_time; - pr_info(MPDEC_TAG"CPU[%d] on->off | Mask=[%d%d] | time online: %llu\n", - cpu, cpu_online(0), cpu_online(1), on_time); + pr_info(MPDEC_TAG"CPU[%d] on->off | Mask=[%d%d%d%d] | time online: %llu\n", + cpu, cpu_online(0), cpu_online(1), cpu_online(2), cpu_online(3), on_time); } else if (per_cpu(msm_mpdec_cpudata, cpu).online != cpu_online(cpu)) { pr_info(MPDEC_TAG"CPU[%d] was controlled outside of mpdecision! | pausing [%d]ms\n", cpu, msm_mpdec_tuners_ins.pause); @@ -187,14 +257,14 @@ static void msm_mpdec_work_thread(struct } break; case MSM_MPDEC_UP: - cpu = (CONFIG_NR_CPUS - 1); + cpu = cpumask_next_zero(0, cpu_online_mask); if (cpu < nr_cpu_ids) { if ((per_cpu(msm_mpdec_cpudata, cpu).online == false) && (!cpu_online(cpu))) { cpu_up(cpu); per_cpu(msm_mpdec_cpudata, cpu).online = true; per_cpu(msm_mpdec_cpudata, cpu).on_time = ktime_to_ms(ktime_get()); - pr_info(MPDEC_TAG"CPU[%d] off->on | Mask=[%d%d]\n", - cpu, cpu_online(0), cpu_online(1)); + pr_info(MPDEC_TAG"CPU[%d] off->on | Mask=[%d%d%d%d]\n", + cpu, cpu_online(0), cpu_online(1), cpu_online(2), cpu_online(3)); } else if (per_cpu(msm_mpdec_cpudata, cpu).online != cpu_online(cpu)) { pr_info(MPDEC_TAG"CPU[%d] was controlled outside of mpdecision! | pausing [%d]ms\n", cpu, msm_mpdec_tuners_ins.pause); @@ -207,47 +277,47 @@ static void msm_mpdec_work_thread(struct pr_err(MPDEC_TAG"%s: invalid mpdec hotplug state %d\n", __func__, state); } - mutex_unlock(&msm_cpu_lock); + mutex_unlock(&mpdec_msm_cpu_lock); out: if (state != MSM_MPDEC_DISABLED) - schedule_delayed_work(&msm_mpdec_work, + queue_delayed_work(msm_mpdec_workq, &msm_mpdec_work, msecs_to_jiffies(msm_mpdec_tuners_ins.delay)); return; } static void msm_mpdec_early_suspend(struct early_suspend *h) { - int cpu = 0; + int cpu = nr_cpu_ids; for_each_possible_cpu(cpu) { mutex_lock(&per_cpu(msm_mpdec_cpudata, cpu).suspend_mutex); - if (((cpu >= (CONFIG_NR_CPUS - 1)) && (num_online_cpus() > 1)) && (msm_mpdec_tuners_ins.scroff_single_core)) { - cpu_down(cpu); - pr_info(MPDEC_TAG"Screen -> off. Suspended CPU%d | Mask=[%d%d]\n", - cpu, cpu_online(0), cpu_online(1)); + if ((cpu >= 1) && (cpu_online(cpu))) { + cpu_down(cpu); + pr_info(MPDEC_TAG"Screen -> off. Suspended CPU[%d] | Mask=[%d%d%d%d]\n", + cpu, cpu_online(0), cpu_online(1), cpu_online(2), cpu_online(3)); per_cpu(msm_mpdec_cpudata, cpu).online = false; } per_cpu(msm_mpdec_cpudata, cpu).device_suspended = true; mutex_unlock(&per_cpu(msm_mpdec_cpudata, cpu).suspend_mutex); } + /* main work thread can sleep now */ + cancel_delayed_work_sync(&msm_mpdec_work); + + pr_info(MPDEC_TAG"Screen -> off. Deactivated mpdecision.\n"); } static void msm_mpdec_late_resume(struct early_suspend *h) { - int cpu = 0; - for_each_possible_cpu(cpu) { - mutex_lock(&per_cpu(msm_mpdec_cpudata, cpu).suspend_mutex); - if ((cpu >= (CONFIG_NR_CPUS - 1)) && (num_online_cpus() < CONFIG_NR_CPUS)) { - /* Enable cpus when screen comes online. */ - cpu_up(cpu); - per_cpu(msm_mpdec_cpudata, cpu).on_time = ktime_to_ms(ktime_get()); - per_cpu(msm_mpdec_cpudata, cpu).online = true; - pr_info(MPDEC_TAG"Screen -> on. Hot plugged CPU%d | Mask=[%d%d]\n", - cpu, cpu_online(0), cpu_online(1)); - } + int cpu = nr_cpu_ids; + for_each_possible_cpu(cpu) per_cpu(msm_mpdec_cpudata, cpu).device_suspended = false; - mutex_unlock(&per_cpu(msm_mpdec_cpudata, cpu).suspend_mutex); - } + + /* wake up main work thread */ + queue_delayed_work(msm_mpdec_workq, &msm_mpdec_work, + msecs_to_jiffies(msm_mpdec_tuners_ins.delay)); + + pr_info(MPDEC_TAG"Screen -> on. Activated mpdecision. | Mask=[%d%d%d%d]\n", + cpu_online(0), cpu_online(1), cpu_online(2), cpu_online(3)); } static struct early_suspend msm_mpdec_early_suspend_handler = { @@ -270,8 +340,85 @@ show_one(startdelay, startdelay); show_one(delay, delay); show_one(pause, pause); show_one(scroff_single_core, scroff_single_core); +show_one(min_cpus, min_cpus); +show_one(max_cpus, max_cpus); + +#define show_one_twts(file_name, arraypos) \ +static ssize_t show_##file_name \ +(struct kobject *kobj, struct attribute *attr, char *buf) \ +{ \ + return sprintf(buf, "%u\n", TwTs_Threshold[arraypos]); \ +} +show_one_twts(twts_threshold_0, 0); +show_one_twts(twts_threshold_1, 1); +show_one_twts(twts_threshold_2, 2); +show_one_twts(twts_threshold_3, 3); +show_one_twts(twts_threshold_4, 4); +show_one_twts(twts_threshold_5, 5); +show_one_twts(twts_threshold_6, 6); +show_one_twts(twts_threshold_7, 7); + +#define store_one_twts(file_name, arraypos) \ +static ssize_t store_##file_name \ +(struct kobject *a, struct attribute *b, const char *buf, size_t count) \ +{ \ + unsigned int input; \ + int ret; \ + ret = sscanf(buf, "%u", &input); \ + if (ret != 1) \ + return -EINVAL; \ + TwTs_Threshold[arraypos] = input; \ + return count; \ +} \ +define_one_global_rw(file_name); +store_one_twts(twts_threshold_0, 0); +store_one_twts(twts_threshold_1, 1); +store_one_twts(twts_threshold_2, 2); +store_one_twts(twts_threshold_3, 3); +store_one_twts(twts_threshold_4, 4); +store_one_twts(twts_threshold_5, 5); +store_one_twts(twts_threshold_6, 6); +store_one_twts(twts_threshold_7, 7); + +#define show_one_nwns(file_name, arraypos) \ +static ssize_t show_##file_name \ +(struct kobject *kobj, struct attribute *attr, char *buf) \ +{ \ + return sprintf(buf, "%u\n", NwNs_Threshold[arraypos]); \ +} +show_one_nwns(nwns_threshold_0, 0); +show_one_nwns(nwns_threshold_1, 1); +show_one_nwns(nwns_threshold_2, 2); +show_one_nwns(nwns_threshold_3, 3); +show_one_nwns(nwns_threshold_4, 4); +show_one_nwns(nwns_threshold_5, 5); +show_one_nwns(nwns_threshold_6, 6); +show_one_nwns(nwns_threshold_7, 7); + +#define store_one_nwns(file_name, arraypos) \ +static ssize_t store_##file_name \ +(struct kobject *a, struct attribute *b, const char *buf, size_t count) \ +{ \ + unsigned int input; \ + int ret; \ + ret = sscanf(buf, "%u", &input); \ + if (ret != 1) \ + return -EINVAL; \ + NwNs_Threshold[arraypos] = input; \ + return count; \ +} \ +define_one_global_rw(file_name); +store_one_nwns(nwns_threshold_0, 0); +store_one_nwns(nwns_threshold_1, 1); +store_one_nwns(nwns_threshold_2, 2); +store_one_nwns(nwns_threshold_3, 3); +store_one_nwns(nwns_threshold_4, 4); +store_one_nwns(nwns_threshold_5, 5); +store_one_nwns(nwns_threshold_6, 6); +store_one_nwns(nwns_threshold_7, 7); + static ssize_t show_idle_freq (struct kobject *kobj, struct attribute *attr, - char *buf) + char *buf) { return sprintf(buf, "%lu\n", msm_mpdec_tuners_ins.idle_freq); } @@ -295,30 +442,6 @@ static ssize_t show_enabled(struct kobje return sprintf(buf, "%u\n", enabled); } -static ssize_t show_nwns_threshold_up(struct kobject *kobj, struct attribute *attr, - char *buf) -{ - return sprintf(buf, "%u\n", NwNs_Threshold[0]); -} - -static ssize_t show_nwns_threshold_down(struct kobject *kobj, struct attribute *attr, - char *buf) -{ - return sprintf(buf, "%u\n", NwNs_Threshold[3]); -} - -static ssize_t show_twts_threshold_up(struct kobject *kobj, struct attribute *attr, - char *buf) -{ - return sprintf(buf, "%u\n", TwTs_Threshold[0]); -} - -static ssize_t show_twts_threshold_down(struct kobject *kobj, struct attribute *attr, - char *buf) -{ - return sprintf(buf, "%u\n", TwTs_Threshold[3]); -} - static ssize_t store_startdelay(struct kobject *a, struct attribute *b, const char *buf, size_t count) { @@ -362,13 +485,14 @@ static ssize_t store_pause(struct kobjec } static ssize_t store_idle_freq(struct kobject *a, struct attribute *b, - const char *buf, size_t count) + const char *buf, size_t count) { long unsigned int input; int ret; ret = sscanf(buf, "%lu", &input); if (ret != 1) return -EINVAL; + msm_mpdec_tuners_ins.idle_freq = input; return count; @@ -395,6 +519,34 @@ static ssize_t store_scroff_single_core( return count; } +static ssize_t store_max_cpus(struct kobject *a, struct attribute *b, + const char *buf, size_t count) +{ + unsigned int input; + int ret; + ret = sscanf(buf, "%u", &input); + if ((ret != 1) || input > CONFIG_NR_CPUS) + return -EINVAL; + + msm_mpdec_tuners_ins.max_cpus = input; + + return count; +} + +static ssize_t store_min_cpus(struct kobject *a, struct attribute *b, + const char *buf, size_t count) +{ + unsigned int input; + int ret; + ret = sscanf(buf, "%u", &input); + if ((ret != 1) || input < 1) + return -EINVAL; + + msm_mpdec_tuners_ins.min_cpus = input; + + return count; +} + static ssize_t store_enabled(struct kobject *a, struct attribute *b, const char *buf, size_t count) { @@ -423,22 +575,24 @@ static ssize_t store_enabled(struct kobj switch (buf[0]) { case '0': state = MSM_MPDEC_DISABLED; - cpu = (CONFIG_NR_CPUS - 1); - if (!cpu_online(cpu)) { - per_cpu(msm_mpdec_cpudata, cpu).on_time = ktime_to_ms(ktime_get()); - per_cpu(msm_mpdec_cpudata, cpu).online = true; - cpu_up(cpu); - pr_info(MPDEC_TAG"disabled... Hot plugged CPU[%d] | Mask=[%d%d]\n", - cpu, cpu_online(0), cpu_online(1)); - } else { - pr_info(MPDEC_TAG"disabled...\n"); - } + pr_info(MPDEC_TAG"nap time... Hot plugging offline CPUs...\n"); + + for (cpu = 1; cpu < CONFIG_NR_CPUS; cpu++) { + if (!cpu_online(cpu)) { + per_cpu(msm_mpdec_cpudata, cpu).on_time = ktime_to_ms(ktime_get()); + per_cpu(msm_mpdec_cpudata, cpu).online = true; + cpu_up(cpu); + pr_info(MPDEC_TAG" DISABLED... Hot plugged CPU[%d] | Mask=[%d%d%d%d]\n", + cpu, cpu_online(0), cpu_online(1), cpu_online(2), cpu_online(3)); + } + } break; case '1': state = MSM_MPDEC_IDLE; was_paused = true; - schedule_delayed_work(&msm_mpdec_work, 0); - pr_info(MPDEC_TAG" mpdecision enabled...\n"); + queue_delayed_work(msm_mpdec_workq, &msm_mpdec_work, + msecs_to_jiffies(msm_mpdec_tuners_ins.delay)); + pr_info(MPDEC_TAG" ENABLED mpdecision...\n"); break; default: ret = -EINVAL; @@ -446,72 +600,14 @@ static ssize_t store_enabled(struct kobj return count; } -static ssize_t store_nwns_threshold_up(struct kobject *a, struct attribute *b, - const char *buf, size_t count) -{ - unsigned int input; - int ret; - ret = sscanf(buf, "%u", &input); - if (ret != 1) - return -EINVAL; - - NwNs_Threshold[0] = input; - - return count; -} - -static ssize_t store_nwns_threshold_down(struct kobject *a, struct attribute *b, - const char *buf, size_t count) -{ - unsigned int input; - int ret; - ret = sscanf(buf, "%u", &input); - if (ret != 1) - return -EINVAL; - - NwNs_Threshold[3] = input; - - return count; -} - -static ssize_t store_twts_threshold_up(struct kobject *a, struct attribute *b, - const char *buf, size_t count) -{ - unsigned int input; - int ret; - ret = sscanf(buf, "%u", &input); - if (ret != 1) - return -EINVAL; - - TwTs_Threshold[0] = input; - - return count; -} - -static ssize_t store_twts_threshold_down(struct kobject *a, struct attribute *b, - const char *buf, size_t count) -{ - unsigned int input; - int ret; - ret = sscanf(buf, "%u", &input); - if (ret != 1) - return -EINVAL; - - TwTs_Threshold[3] = input; - - return count; -} - define_one_global_rw(startdelay); define_one_global_rw(delay); define_one_global_rw(pause); define_one_global_rw(scroff_single_core); define_one_global_rw(idle_freq); +define_one_global_rw(min_cpus); +define_one_global_rw(max_cpus); define_one_global_rw(enabled); -define_one_global_rw(nwns_threshold_up); -define_one_global_rw(nwns_threshold_down); -define_one_global_rw(twts_threshold_up); -define_one_global_rw(twts_threshold_down); static struct attribute *msm_mpdec_attributes[] = { &startdelay.attr, @@ -519,11 +615,25 @@ static struct attribute *msm_mpdec_attri &pause.attr, &scroff_single_core.attr, &idle_freq.attr, + &min_cpus.attr, + &max_cpus.attr, &enabled.attr, - &nwns_threshold_up.attr, - &nwns_threshold_down.attr, - &twts_threshold_up.attr, - &twts_threshold_down.attr, + &twts_threshold_0.attr, + &twts_threshold_1.attr, + &twts_threshold_2.attr, + &twts_threshold_3.attr, + &twts_threshold_4.attr, + &twts_threshold_5.attr, + &twts_threshold_6.attr, + &twts_threshold_7.attr, + &nwns_threshold_0.attr, + &nwns_threshold_1.attr, + &nwns_threshold_2.attr, + &nwns_threshold_3.attr, + &nwns_threshold_4.attr, + &nwns_threshold_5.attr, + &nwns_threshold_6.attr, + &nwns_threshold_7.attr, NULL }; @@ -534,7 +644,7 @@ static struct attribute_group msm_mpdec_ }; /**************************** SYSFS END ****************************/ -static int __init msm_mpdec(void) +static int __init msm_mpdec_init(void) { int cpu, rc, err = 0; @@ -544,9 +654,16 @@ static int __init msm_mpdec(void) per_cpu(msm_mpdec_cpudata, cpu).online = true; } + was_paused = true; + + msm_mpdec_workq = alloc_workqueue( + "mpdec", WQ_UNBOUND | WQ_RESCUER | WQ_FREEZABLE, 1); + if (!msm_mpdec_workq) + return -ENOMEM; INIT_DELAYED_WORK(&msm_mpdec_work, msm_mpdec_work_thread); if (state != MSM_MPDEC_DISABLED) - schedule_delayed_work(&msm_mpdec_work, 0); + queue_delayed_work(msm_mpdec_workq, &msm_mpdec_work, + msecs_to_jiffies(msm_mpdec_tuners_ins.delay)); register_early_suspend(&msm_mpdec_early_suspend_handler); @@ -564,9 +681,12 @@ static int __init msm_mpdec(void) return err; } +late_initcall(msm_mpdec_init); -late_initcall(msm_mpdec); - -MODULE_DESCRIPTION("Kernel based MPDECISION"); - +void msm_mpdec_exit(void) +{ + destroy_workqueue(msm_mpdec_workq); + destroy_workqueue(msm_mpdec_workq); +} +MODULE_DESCRIPTION("Kernel based MPDECISION (C) 2011-12 Chad Goodman"); \ No newline at end of file