diff options
Diffstat (limited to 'drivers/devfreq/arm-memlat-mon.c')
-rw-r--r-- | drivers/devfreq/arm-memlat-mon.c | 368 |
1 files changed, 368 insertions, 0 deletions
diff --git a/drivers/devfreq/arm-memlat-mon.c b/drivers/devfreq/arm-memlat-mon.c new file mode 100644 index 000000000000..4fb0a5ffda50 --- /dev/null +++ b/drivers/devfreq/arm-memlat-mon.c @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2014-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. + */ + +#define pr_fmt(fmt) "arm-memlat-mon: " fmt + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/delay.h> +#include <linux/err.h> +#include <linux/errno.h> +#include <linux/interrupt.h> +#include <linux/platform_device.h> +#include <linux/of.h> +#include <linux/of_irq.h> +#include <linux/slab.h> +#include <linux/irq.h> +#include <linux/cpu_pm.h> +#include <linux/cpu.h> +#include "governor.h" +#include "governor_memlat.h" +#include <linux/perf_event.h> + +enum ev_index { + INST_IDX, + L2DM_IDX, + CYC_IDX, + NUM_EVENTS +}; +#define INST_EV 0x08 +#define L2DM_EV 0x17 +#define CYC_EV 0x11 + +struct event_data { + struct perf_event *pevent; + unsigned long prev_count; +}; + +struct memlat_hwmon_data { + struct event_data events[NUM_EVENTS]; + ktime_t prev_ts; + bool init_pending; +}; +static DEFINE_PER_CPU(struct memlat_hwmon_data, pm_data); + +struct cpu_grp_info { + cpumask_t cpus; + struct memlat_hwmon hw; + struct notifier_block arm_memlat_cpu_notif; +}; + +static unsigned long compute_freq(struct memlat_hwmon_data *hw_data, + unsigned long cyc_cnt) +{ + ktime_t ts; + unsigned int diff; + unsigned long freq = 0; + + ts = ktime_get(); + diff = ktime_to_us(ktime_sub(ts, hw_data->prev_ts)); + if (!diff) + diff = 1; + hw_data->prev_ts = ts; + freq = cyc_cnt; + do_div(freq, diff); + + return freq; +} + +#define MAX_COUNT_LIM 0xFFFFFFFFFFFFFFFF +static inline unsigned long read_event(struct event_data *event) +{ + unsigned long ev_count; + u64 total, enabled, running; + + total = perf_event_read_value(event->pevent, &enabled, &running); + if (total >= event->prev_count) + ev_count = total - event->prev_count; + else + ev_count = (MAX_COUNT_LIM - event->prev_count) + total; + + event->prev_count = total; + + return ev_count; +} + +static void read_perf_counters(int cpu, struct cpu_grp_info *cpu_grp) +{ + int cpu_idx; + struct memlat_hwmon_data *hw_data = &per_cpu(pm_data, cpu); + struct memlat_hwmon *hw = &cpu_grp->hw; + unsigned long cyc_cnt; + + if (hw_data->init_pending) + return; + + cpu_idx = cpu - cpumask_first(&cpu_grp->cpus); + + hw->core_stats[cpu_idx].inst_count = + read_event(&hw_data->events[INST_IDX]); + + hw->core_stats[cpu_idx].mem_count = + read_event(&hw_data->events[L2DM_IDX]); + + cyc_cnt = read_event(&hw_data->events[CYC_IDX]); + hw->core_stats[cpu_idx].freq = compute_freq(hw_data, cyc_cnt); +} + +static unsigned long get_cnt(struct memlat_hwmon *hw) +{ + int cpu; + struct cpu_grp_info *cpu_grp = container_of(hw, + struct cpu_grp_info, hw); + + for_each_cpu(cpu, &cpu_grp->cpus) + read_perf_counters(cpu, cpu_grp); + + return 0; +} + +static void delete_events(struct memlat_hwmon_data *hw_data) +{ + int i; + + for (i = 0; i < NUM_EVENTS; i++) { + hw_data->events[i].prev_count = 0; + perf_event_release_kernel(hw_data->events[i].pevent); + } +} + +static void stop_hwmon(struct memlat_hwmon *hw) +{ + int cpu, idx; + struct memlat_hwmon_data *hw_data; + struct cpu_grp_info *cpu_grp = container_of(hw, + struct cpu_grp_info, hw); + + get_online_cpus(); + for_each_cpu(cpu, &cpu_grp->cpus) { + hw_data = &per_cpu(pm_data, cpu); + if (hw_data->init_pending) + hw_data->init_pending = false; + else + delete_events(hw_data); + + /* Clear governor data */ + idx = cpu - cpumask_first(&cpu_grp->cpus); + hw->core_stats[idx].inst_count = 0; + hw->core_stats[idx].mem_count = 0; + hw->core_stats[idx].freq = 0; + } + put_online_cpus(); + + unregister_cpu_notifier(&cpu_grp->arm_memlat_cpu_notif); +} + +static struct perf_event_attr *alloc_attr(void) +{ + struct perf_event_attr *attr; + + attr = kzalloc(sizeof(struct perf_event_attr), GFP_KERNEL); + if (!attr) + return ERR_PTR(-ENOMEM); + + attr->type = PERF_TYPE_RAW; + attr->size = sizeof(struct perf_event_attr); + attr->pinned = 1; + attr->exclude_idle = 1; + + return attr; +} + +static int set_events(struct memlat_hwmon_data *hw_data, int cpu) +{ + struct perf_event *pevent; + struct perf_event_attr *attr; + int err; + + /* Allocate an attribute for event initialization */ + attr = alloc_attr(); + if (IS_ERR(attr)) + return PTR_ERR(attr); + + attr->config = INST_EV; + pevent = perf_event_create_kernel_counter(attr, cpu, NULL, NULL, NULL); + if (IS_ERR(pevent)) + goto err_out; + hw_data->events[INST_IDX].pevent = pevent; + perf_event_enable(hw_data->events[INST_IDX].pevent); + + attr->config = L2DM_EV; + pevent = perf_event_create_kernel_counter(attr, cpu, NULL, NULL, NULL); + if (IS_ERR(pevent)) + goto err_out; + hw_data->events[L2DM_IDX].pevent = pevent; + perf_event_enable(hw_data->events[L2DM_IDX].pevent); + + attr->config = CYC_EV; + pevent = perf_event_create_kernel_counter(attr, cpu, NULL, NULL, NULL); + if (IS_ERR(pevent)) + goto err_out; + hw_data->events[CYC_IDX].pevent = pevent; + perf_event_enable(hw_data->events[CYC_IDX].pevent); + + kfree(attr); + return 0; + +err_out: + err = PTR_ERR(pevent); + kfree(attr); + return err; +} + +static int arm_memlat_cpu_callback(struct notifier_block *nb, + unsigned long action, void *hcpu) +{ + unsigned long cpu = (unsigned long)hcpu; + struct memlat_hwmon_data *hw_data = &per_cpu(pm_data, cpu); + + if ((action != CPU_ONLINE) || !hw_data->init_pending) + return NOTIFY_OK; + + if (set_events(hw_data, cpu)) + pr_warn("Failed to create perf event for CPU%lu\n", cpu); + + hw_data->init_pending = false; + + return NOTIFY_OK; +} + +static int start_hwmon(struct memlat_hwmon *hw) +{ + int cpu, ret = 0; + struct memlat_hwmon_data *hw_data; + struct cpu_grp_info *cpu_grp = container_of(hw, + struct cpu_grp_info, hw); + + register_cpu_notifier(&cpu_grp->arm_memlat_cpu_notif); + + get_online_cpus(); + for_each_cpu(cpu, &cpu_grp->cpus) { + hw_data = &per_cpu(pm_data, cpu); + ret = set_events(hw_data, cpu); + if (ret) { + if (!cpu_online(cpu)) { + hw_data->init_pending = true; + ret = 0; + } else { + pr_warn("Perf event init failed on CPU%d\n", + cpu); + break; + } + } + } + + put_online_cpus(); + return ret; +} + +static int get_mask_from_dev_handle(struct platform_device *pdev, + cpumask_t *mask) +{ + struct device *dev = &pdev->dev; + struct device_node *dev_phandle; + struct device *cpu_dev; + int cpu, i = 0; + int ret = -ENOENT; + + dev_phandle = of_parse_phandle(dev->of_node, "qcom,cpulist", i++); + while (dev_phandle) { + for_each_possible_cpu(cpu) { + cpu_dev = get_cpu_device(cpu); + if (cpu_dev && cpu_dev->of_node == dev_phandle) { + cpumask_set_cpu(cpu, mask); + ret = 0; + break; + } + } + dev_phandle = of_parse_phandle(dev->of_node, + "qcom,cpulist", i++); + } + + return ret; +} + +static int arm_memlat_mon_driver_probe(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct memlat_hwmon *hw; + struct cpu_grp_info *cpu_grp; + int cpu, ret; + + cpu_grp = devm_kzalloc(dev, sizeof(*cpu_grp), GFP_KERNEL); + if (!cpu_grp) + return -ENOMEM; + cpu_grp->arm_memlat_cpu_notif.notifier_call = arm_memlat_cpu_callback; + hw = &cpu_grp->hw; + + hw->dev = dev; + hw->of_node = of_parse_phandle(dev->of_node, "qcom,target-dev", 0); + if (!hw->of_node) { + dev_err(dev, "Couldn't find a target device\n"); + return -ENODEV; + } + + if (get_mask_from_dev_handle(pdev, &cpu_grp->cpus)) { + dev_err(dev, "CPU list is empty\n"); + return -ENODEV; + } + + hw->num_cores = cpumask_weight(&cpu_grp->cpus); + hw->core_stats = devm_kzalloc(dev, hw->num_cores * + sizeof(*(hw->core_stats)), GFP_KERNEL); + if (!hw->core_stats) + return -ENOMEM; + + for_each_cpu(cpu, &cpu_grp->cpus) + hw->core_stats[cpu - cpumask_first(&cpu_grp->cpus)].id = cpu; + + hw->start_hwmon = &start_hwmon; + hw->stop_hwmon = &stop_hwmon; + hw->get_cnt = &get_cnt; + + ret = register_memlat(dev, hw); + if (ret) { + pr_err("Mem Latency Gov registration failed\n"); + return ret; + } + + return 0; +} + +static struct of_device_id match_table[] = { + { .compatible = "qcom,arm-memlat-mon" }, + {} +}; + +static struct platform_driver arm_memlat_mon_driver = { + .probe = arm_memlat_mon_driver_probe, + .driver = { + .name = "arm-memlat-mon", + .of_match_table = match_table, + .owner = THIS_MODULE, + }, +}; + +static int __init arm_memlat_mon_init(void) +{ + return platform_driver_register(&arm_memlat_mon_driver); +} +module_init(arm_memlat_mon_init); + +static void __exit arm_memlat_mon_exit(void) +{ + platform_driver_unregister(&arm_memlat_mon_driver); +} +module_exit(arm_memlat_mon_exit); |