mirror of
https://mirrors.bfsu.edu.cn/git/linux.git
synced 2024-11-19 10:14:23 +08:00
5c5cd7b502
Retrieves and parses the 24x7 catalog on POWER systems that supply it (right now, only POWER 8). Events are exposed via sysfs in the standard fashion, and are all parameterized. $ cd /sys/bus/event_source/devices/hv_24x7/events $ cat HPM_CS_FROM_L4_LDATA__PHYS_CORE domain=0x2,offset=0xd58,core=?,lpar=0x0 $ cat HPM_TLBIE__VCPU_HOME_CHIP domain=0x4,offset=0x358,vcpu=?,lpar=? where user is required to specify values for the fields with '?' (like core, vcpu, lpar above), when specifying the event with the perf tool. Catalog is (at the moment) only parsed on boot. It needs re-parsing when a some hypervisor events occur. At that point we'll also need to prevent old events from continuing to function (counter that is passed in via spare space in the config values?). Signed-off-by: Cody P Schafer <cody@linux.vnet.ibm.com> Signed-off-by: Sukadev Bhattiprolu <sukadev@linux.vnet.ibm.com> Signed-off-by: Michael Ellerman <mpe@ellerman.id.au>
1234 lines
30 KiB
C
1234 lines
30 KiB
C
/*
|
|
* Hypervisor supplied "24x7" performance counter support
|
|
*
|
|
* Author: Cody P Schafer <cody@linux.vnet.ibm.com>
|
|
* Copyright 2014 IBM Corporation.
|
|
*
|
|
* 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 the Free Software Foundation; either version
|
|
* 2 of the License, or (at your option) any later version.
|
|
*/
|
|
|
|
#define pr_fmt(fmt) "hv-24x7: " fmt
|
|
|
|
#include <linux/perf_event.h>
|
|
#include <linux/rbtree.h>
|
|
#include <linux/module.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/vmalloc.h>
|
|
|
|
#include <asm/firmware.h>
|
|
#include <asm/hvcall.h>
|
|
#include <asm/io.h>
|
|
#include <linux/byteorder/generic.h>
|
|
|
|
#include "hv-24x7.h"
|
|
#include "hv-24x7-catalog.h"
|
|
#include "hv-common.h"
|
|
|
|
static const char *event_domain_suffix(unsigned domain)
|
|
{
|
|
switch (domain) {
|
|
#define DOMAIN(n, v, x, c) \
|
|
case HV_PERF_DOMAIN_##n: \
|
|
return "__" #n;
|
|
#include "hv-24x7-domains.h"
|
|
#undef DOMAIN
|
|
default:
|
|
WARN(1, "unknown domain %d\n", domain);
|
|
return "__UNKNOWN_DOMAIN_SUFFIX";
|
|
}
|
|
}
|
|
|
|
static bool domain_is_valid(unsigned domain)
|
|
{
|
|
switch (domain) {
|
|
#define DOMAIN(n, v, x, c) \
|
|
case HV_PERF_DOMAIN_##n: \
|
|
/* fall through */
|
|
#include "hv-24x7-domains.h"
|
|
#undef DOMAIN
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool is_physical_domain(unsigned domain)
|
|
{
|
|
switch (domain) {
|
|
#define DOMAIN(n, v, x, c) \
|
|
case HV_PERF_DOMAIN_##n: \
|
|
return c;
|
|
#include "hv-24x7-domains.h"
|
|
#undef DOMAIN
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static bool catalog_entry_domain_is_valid(unsigned domain)
|
|
{
|
|
return is_physical_domain(domain);
|
|
}
|
|
|
|
/*
|
|
* TODO: Merging events:
|
|
* - Think of the hcall as an interface to a 4d array of counters:
|
|
* - x = domains
|
|
* - y = indexes in the domain (core, chip, vcpu, node, etc)
|
|
* - z = offset into the counter space
|
|
* - w = lpars (guest vms, "logical partitions")
|
|
* - A single request is: x,y,y_last,z,z_last,w,w_last
|
|
* - this means we can retrieve a rectangle of counters in y,z for a single x.
|
|
*
|
|
* - Things to consider (ignoring w):
|
|
* - input cost_per_request = 16
|
|
* - output cost_per_result(ys,zs) = 8 + 8 * ys + ys * zs
|
|
* - limited number of requests per hcall (must fit into 4K bytes)
|
|
* - 4k = 16 [buffer header] - 16 [request size] * request_count
|
|
* - 255 requests per hcall
|
|
* - sometimes it will be more efficient to read extra data and discard
|
|
*/
|
|
|
|
/*
|
|
* Example usage:
|
|
* perf stat -e 'hv_24x7/domain=2,offset=8,vcpu=0,lpar=0xffffffff/'
|
|
*/
|
|
|
|
/* u3 0-6, one of HV_24X7_PERF_DOMAIN */
|
|
EVENT_DEFINE_RANGE_FORMAT(domain, config, 0, 3);
|
|
/* u16 */
|
|
EVENT_DEFINE_RANGE_FORMAT(core, config, 16, 31);
|
|
EVENT_DEFINE_RANGE_FORMAT(vcpu, config, 16, 31);
|
|
/* u32, see "data_offset" */
|
|
EVENT_DEFINE_RANGE_FORMAT(offset, config, 32, 63);
|
|
/* u16 */
|
|
EVENT_DEFINE_RANGE_FORMAT(lpar, config1, 0, 15);
|
|
|
|
EVENT_DEFINE_RANGE(reserved1, config, 4, 15);
|
|
EVENT_DEFINE_RANGE(reserved2, config1, 16, 63);
|
|
EVENT_DEFINE_RANGE(reserved3, config2, 0, 63);
|
|
|
|
static struct attribute *format_attrs[] = {
|
|
&format_attr_domain.attr,
|
|
&format_attr_offset.attr,
|
|
&format_attr_core.attr,
|
|
&format_attr_vcpu.attr,
|
|
&format_attr_lpar.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group format_group = {
|
|
.name = "format",
|
|
.attrs = format_attrs,
|
|
};
|
|
|
|
static struct attribute_group event_group = {
|
|
.name = "events",
|
|
/* .attrs is set in init */
|
|
};
|
|
|
|
static struct attribute_group event_desc_group = {
|
|
.name = "event_descs",
|
|
/* .attrs is set in init */
|
|
};
|
|
|
|
static struct attribute_group event_long_desc_group = {
|
|
.name = "event_long_descs",
|
|
/* .attrs is set in init */
|
|
};
|
|
|
|
static struct kmem_cache *hv_page_cache;
|
|
|
|
static char *event_name(struct hv_24x7_event_data *ev, int *len)
|
|
{
|
|
*len = be16_to_cpu(ev->event_name_len) - 2;
|
|
return (char *)ev->remainder;
|
|
}
|
|
|
|
static char *event_desc(struct hv_24x7_event_data *ev, int *len)
|
|
{
|
|
unsigned nl = be16_to_cpu(ev->event_name_len);
|
|
__be16 *desc_len = (__be16 *)(ev->remainder + nl - 2);
|
|
*len = be16_to_cpu(*desc_len) - 2;
|
|
return (char *)ev->remainder + nl;
|
|
}
|
|
|
|
static char *event_long_desc(struct hv_24x7_event_data *ev, int *len)
|
|
{
|
|
unsigned nl = be16_to_cpu(ev->event_name_len);
|
|
__be16 *desc_len_ = (__be16 *)(ev->remainder + nl - 2);
|
|
unsigned desc_len = be16_to_cpu(*desc_len_);
|
|
__be16 *long_desc_len = (__be16 *)(ev->remainder + nl + desc_len - 2);
|
|
*len = be16_to_cpu(*long_desc_len) - 2;
|
|
return (char *)ev->remainder + nl + desc_len;
|
|
}
|
|
|
|
static bool event_fixed_portion_is_within(struct hv_24x7_event_data *ev,
|
|
void *end)
|
|
{
|
|
void *start = ev;
|
|
|
|
return (start + offsetof(struct hv_24x7_event_data, remainder)) < end;
|
|
}
|
|
|
|
/*
|
|
* Things we don't check:
|
|
* - padding for desc, name, and long/detailed desc is required to be '\0'
|
|
* bytes.
|
|
*
|
|
* Return NULL if we pass end,
|
|
* Otherwise return the address of the byte just following the event.
|
|
*/
|
|
static void *event_end(struct hv_24x7_event_data *ev, void *end)
|
|
{
|
|
void *start = ev;
|
|
__be16 *dl_, *ldl_;
|
|
unsigned dl, ldl;
|
|
unsigned nl = be16_to_cpu(ev->event_name_len);
|
|
|
|
if (nl < 2) {
|
|
pr_debug("%s: name length too short: %d", __func__, nl);
|
|
return NULL;
|
|
}
|
|
|
|
if (start + nl > end) {
|
|
pr_debug("%s: start=%p + nl=%u > end=%p",
|
|
__func__, start, nl, end);
|
|
return NULL;
|
|
}
|
|
|
|
dl_ = (__be16 *)(ev->remainder + nl - 2);
|
|
if (!IS_ALIGNED((uintptr_t)dl_, 2))
|
|
pr_warn("desc len not aligned %p", dl_);
|
|
dl = be16_to_cpu(*dl_);
|
|
if (dl < 2) {
|
|
pr_debug("%s: desc len too short: %d", __func__, dl);
|
|
return NULL;
|
|
}
|
|
|
|
if (start + nl + dl > end) {
|
|
pr_debug("%s: (start=%p + nl=%u + dl=%u)=%p > end=%p",
|
|
__func__, start, nl, dl, start + nl + dl, end);
|
|
return NULL;
|
|
}
|
|
|
|
ldl_ = (__be16 *)(ev->remainder + nl + dl - 2);
|
|
if (!IS_ALIGNED((uintptr_t)ldl_, 2))
|
|
pr_warn("long desc len not aligned %p", ldl_);
|
|
ldl = be16_to_cpu(*ldl_);
|
|
if (ldl < 2) {
|
|
pr_debug("%s: long desc len too short (ldl=%u)",
|
|
__func__, ldl);
|
|
return NULL;
|
|
}
|
|
|
|
if (start + nl + dl + ldl > end) {
|
|
pr_debug("%s: start=%p + nl=%u + dl=%u + ldl=%u > end=%p",
|
|
__func__, start, nl, dl, ldl, end);
|
|
return NULL;
|
|
}
|
|
|
|
return start + nl + dl + ldl;
|
|
}
|
|
|
|
static unsigned long h_get_24x7_catalog_page_(unsigned long phys_4096,
|
|
unsigned long version,
|
|
unsigned long index)
|
|
{
|
|
pr_devel("h_get_24x7_catalog_page(0x%lx, %lu, %lu)",
|
|
phys_4096,
|
|
version,
|
|
index);
|
|
WARN_ON(!IS_ALIGNED(phys_4096, 4096));
|
|
return plpar_hcall_norets(H_GET_24X7_CATALOG_PAGE,
|
|
phys_4096,
|
|
version,
|
|
index);
|
|
}
|
|
|
|
static unsigned long h_get_24x7_catalog_page(char page[],
|
|
u64 version, u32 index)
|
|
{
|
|
return h_get_24x7_catalog_page_(virt_to_phys(page),
|
|
version, index);
|
|
}
|
|
|
|
static unsigned core_domains[] = {
|
|
HV_PERF_DOMAIN_PHYS_CORE,
|
|
HV_PERF_DOMAIN_VCPU_HOME_CORE,
|
|
HV_PERF_DOMAIN_VCPU_HOME_CHIP,
|
|
HV_PERF_DOMAIN_VCPU_HOME_NODE,
|
|
HV_PERF_DOMAIN_VCPU_REMOTE_NODE,
|
|
};
|
|
/* chip event data always yeilds a single event, core yeilds multiple */
|
|
#define MAX_EVENTS_PER_EVENT_DATA ARRAY_SIZE(core_domains)
|
|
|
|
static char *event_fmt(struct hv_24x7_event_data *event, unsigned domain)
|
|
{
|
|
const char *sindex;
|
|
const char *lpar;
|
|
|
|
if (is_physical_domain(domain)) {
|
|
lpar = "0x0";
|
|
sindex = "core";
|
|
} else {
|
|
lpar = "?";
|
|
sindex = "vcpu";
|
|
}
|
|
|
|
return kasprintf(GFP_KERNEL,
|
|
"domain=0x%x,offset=0x%x,%s=?,lpar=%s",
|
|
domain,
|
|
be16_to_cpu(event->event_counter_offs) +
|
|
be16_to_cpu(event->event_group_record_offs),
|
|
sindex,
|
|
lpar);
|
|
}
|
|
|
|
/* Avoid trusting fw to NUL terminate strings */
|
|
static char *memdup_to_str(char *maybe_str, int max_len, gfp_t gfp)
|
|
{
|
|
return kasprintf(gfp, "%.*s", max_len, maybe_str);
|
|
}
|
|
|
|
static ssize_t device_show_string(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
struct dev_ext_attribute *d;
|
|
|
|
d = container_of(attr, struct dev_ext_attribute, attr);
|
|
return sprintf(buf, "%s\n", (char *)d->var);
|
|
}
|
|
|
|
static struct attribute *device_str_attr_create_(char *name, char *str)
|
|
{
|
|
struct dev_ext_attribute *attr = kzalloc(sizeof(*attr), GFP_KERNEL);
|
|
|
|
if (!attr)
|
|
return NULL;
|
|
|
|
attr->var = str;
|
|
attr->attr.attr.name = name;
|
|
attr->attr.attr.mode = 0444;
|
|
attr->attr.show = device_show_string;
|
|
return &attr->attr.attr;
|
|
}
|
|
|
|
static struct attribute *device_str_attr_create(char *name, int name_max,
|
|
int name_nonce,
|
|
char *str, size_t str_max)
|
|
{
|
|
char *n;
|
|
char *s = memdup_to_str(str, str_max, GFP_KERNEL);
|
|
struct attribute *a;
|
|
|
|
if (!s)
|
|
return NULL;
|
|
|
|
if (!name_nonce)
|
|
n = kasprintf(GFP_KERNEL, "%.*s", name_max, name);
|
|
else
|
|
n = kasprintf(GFP_KERNEL, "%.*s__%d", name_max, name,
|
|
name_nonce);
|
|
if (!n)
|
|
goto out_s;
|
|
|
|
a = device_str_attr_create_(n, s);
|
|
if (!a)
|
|
goto out_n;
|
|
|
|
return a;
|
|
out_n:
|
|
kfree(n);
|
|
out_s:
|
|
kfree(s);
|
|
return NULL;
|
|
}
|
|
|
|
static void device_str_attr_destroy(struct attribute *attr)
|
|
{
|
|
struct dev_ext_attribute *d;
|
|
|
|
d = container_of(attr, struct dev_ext_attribute, attr.attr);
|
|
kfree(d->var);
|
|
kfree(d->attr.attr.name);
|
|
kfree(d);
|
|
}
|
|
|
|
static struct attribute *event_to_attr(unsigned ix,
|
|
struct hv_24x7_event_data *event,
|
|
unsigned domain,
|
|
int nonce)
|
|
{
|
|
int event_name_len;
|
|
char *ev_name, *a_ev_name, *val;
|
|
const char *ev_suffix;
|
|
struct attribute *attr;
|
|
|
|
if (!domain_is_valid(domain)) {
|
|
pr_warn("catalog event %u has invalid domain %u\n",
|
|
ix, domain);
|
|
return NULL;
|
|
}
|
|
|
|
val = event_fmt(event, domain);
|
|
if (!val)
|
|
return NULL;
|
|
|
|
ev_suffix = event_domain_suffix(domain);
|
|
ev_name = event_name(event, &event_name_len);
|
|
if (!nonce)
|
|
a_ev_name = kasprintf(GFP_KERNEL, "%.*s%s",
|
|
(int)event_name_len, ev_name, ev_suffix);
|
|
else
|
|
a_ev_name = kasprintf(GFP_KERNEL, "%.*s%s__%d",
|
|
(int)event_name_len, ev_name, ev_suffix, nonce);
|
|
|
|
|
|
if (!a_ev_name)
|
|
goto out_val;
|
|
|
|
attr = device_str_attr_create_(a_ev_name, val);
|
|
if (!attr)
|
|
goto out_name;
|
|
|
|
return attr;
|
|
out_name:
|
|
kfree(a_ev_name);
|
|
out_val:
|
|
kfree(val);
|
|
return NULL;
|
|
}
|
|
|
|
static struct attribute *event_to_desc_attr(struct hv_24x7_event_data *event,
|
|
int nonce)
|
|
{
|
|
int nl, dl;
|
|
char *name = event_name(event, &nl);
|
|
char *desc = event_desc(event, &dl);
|
|
|
|
/* If there isn't a description, don't create the sysfs file */
|
|
if (!dl)
|
|
return NULL;
|
|
|
|
return device_str_attr_create(name, nl, nonce, desc, dl);
|
|
}
|
|
|
|
static struct attribute *
|
|
event_to_long_desc_attr(struct hv_24x7_event_data *event, int nonce)
|
|
{
|
|
int nl, dl;
|
|
char *name = event_name(event, &nl);
|
|
char *desc = event_long_desc(event, &dl);
|
|
|
|
/* If there isn't a description, don't create the sysfs file */
|
|
if (!dl)
|
|
return NULL;
|
|
|
|
return device_str_attr_create(name, nl, nonce, desc, dl);
|
|
}
|
|
|
|
static ssize_t event_data_to_attrs(unsigned ix, struct attribute **attrs,
|
|
struct hv_24x7_event_data *event, int nonce)
|
|
{
|
|
unsigned i;
|
|
|
|
switch (event->domain) {
|
|
case HV_PERF_DOMAIN_PHYS_CHIP:
|
|
*attrs = event_to_attr(ix, event, event->domain, nonce);
|
|
return 1;
|
|
case HV_PERF_DOMAIN_PHYS_CORE:
|
|
for (i = 0; i < ARRAY_SIZE(core_domains); i++) {
|
|
attrs[i] = event_to_attr(ix, event, core_domains[i],
|
|
nonce);
|
|
if (!attrs[i]) {
|
|
pr_warn("catalog event %u: individual attr %u "
|
|
"creation failure\n", ix, i);
|
|
for (; i; i--)
|
|
device_str_attr_destroy(attrs[i - 1]);
|
|
return -1;
|
|
}
|
|
}
|
|
return i;
|
|
default:
|
|
pr_warn("catalog event %u: domain %u is not allowed in the "
|
|
"catalog\n", ix, event->domain);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
static size_t event_to_attr_ct(struct hv_24x7_event_data *event)
|
|
{
|
|
switch (event->domain) {
|
|
case HV_PERF_DOMAIN_PHYS_CHIP:
|
|
return 1;
|
|
case HV_PERF_DOMAIN_PHYS_CORE:
|
|
return ARRAY_SIZE(core_domains);
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
static unsigned long vmalloc_to_phys(void *v)
|
|
{
|
|
struct page *p = vmalloc_to_page(v);
|
|
|
|
BUG_ON(!p);
|
|
return page_to_phys(p) + offset_in_page(v);
|
|
}
|
|
|
|
/* */
|
|
struct event_uniq {
|
|
struct rb_node node;
|
|
const char *name;
|
|
int nl;
|
|
unsigned ct;
|
|
unsigned domain;
|
|
};
|
|
|
|
static int memord(const void *d1, size_t s1, const void *d2, size_t s2)
|
|
{
|
|
if (s1 < s2)
|
|
return 1;
|
|
if (s2 > s1)
|
|
return -1;
|
|
|
|
return memcmp(d1, d2, s1);
|
|
}
|
|
|
|
static int ev_uniq_ord(const void *v1, size_t s1, unsigned d1, const void *v2,
|
|
size_t s2, unsigned d2)
|
|
{
|
|
int r = memord(v1, s1, v2, s2);
|
|
|
|
if (r)
|
|
return r;
|
|
if (d1 > d2)
|
|
return 1;
|
|
if (d2 > d1)
|
|
return -1;
|
|
return 0;
|
|
}
|
|
|
|
static int event_uniq_add(struct rb_root *root, const char *name, int nl,
|
|
unsigned domain)
|
|
{
|
|
struct rb_node **new = &(root->rb_node), *parent = NULL;
|
|
struct event_uniq *data;
|
|
|
|
/* Figure out where to put new node */
|
|
while (*new) {
|
|
struct event_uniq *it;
|
|
int result;
|
|
|
|
it = container_of(*new, struct event_uniq, node);
|
|
result = ev_uniq_ord(name, nl, domain, it->name, it->nl,
|
|
it->domain);
|
|
|
|
parent = *new;
|
|
if (result < 0)
|
|
new = &((*new)->rb_left);
|
|
else if (result > 0)
|
|
new = &((*new)->rb_right);
|
|
else {
|
|
it->ct++;
|
|
pr_info("found a duplicate event %.*s, ct=%u\n", nl,
|
|
name, it->ct);
|
|
return it->ct;
|
|
}
|
|
}
|
|
|
|
data = kmalloc(sizeof(*data), GFP_KERNEL);
|
|
if (!data)
|
|
return -ENOMEM;
|
|
|
|
*data = (struct event_uniq) {
|
|
.name = name,
|
|
.nl = nl,
|
|
.ct = 0,
|
|
.domain = domain,
|
|
};
|
|
|
|
/* Add new node and rebalance tree. */
|
|
rb_link_node(&data->node, parent, new);
|
|
rb_insert_color(&data->node, root);
|
|
|
|
/* data->ct */
|
|
return 0;
|
|
}
|
|
|
|
static void event_uniq_destroy(struct rb_root *root)
|
|
{
|
|
/*
|
|
* the strings we point to are in the giant block of memory filled by
|
|
* the catalog, and are freed separately.
|
|
*/
|
|
struct event_uniq *pos, *n;
|
|
|
|
rbtree_postorder_for_each_entry_safe(pos, n, root, node)
|
|
kfree(pos);
|
|
}
|
|
|
|
|
|
/*
|
|
* ensure the event structure's sizes are self consistent and don't cause us to
|
|
* read outside of the event
|
|
*
|
|
* On success, return the event length in bytes.
|
|
* Otherwise, return -1 (and print as appropriate).
|
|
*/
|
|
static ssize_t catalog_event_len_validate(struct hv_24x7_event_data *event,
|
|
size_t event_idx,
|
|
size_t event_data_bytes,
|
|
size_t event_entry_count,
|
|
size_t offset, void *end)
|
|
{
|
|
ssize_t ev_len;
|
|
void *ev_end, *calc_ev_end;
|
|
|
|
if (offset >= event_data_bytes)
|
|
return -1;
|
|
|
|
if (event_idx >= event_entry_count) {
|
|
pr_devel("catalog event data has %zu bytes of padding after last event\n",
|
|
event_data_bytes - offset);
|
|
return -1;
|
|
}
|
|
|
|
if (!event_fixed_portion_is_within(event, end)) {
|
|
pr_warn("event %zu fixed portion is not within range\n",
|
|
event_idx);
|
|
return -1;
|
|
}
|
|
|
|
ev_len = be16_to_cpu(event->length);
|
|
|
|
if (ev_len % 16)
|
|
pr_info("event %zu has length %zu not divisible by 16: event=%pK\n",
|
|
event_idx, ev_len, event);
|
|
|
|
ev_end = (__u8 *)event + ev_len;
|
|
if (ev_end > end) {
|
|
pr_warn("event %zu has .length=%zu, ends after buffer end: ev_end=%pK > end=%pK, offset=%zu\n",
|
|
event_idx, ev_len, ev_end, end,
|
|
offset);
|
|
return -1;
|
|
}
|
|
|
|
calc_ev_end = event_end(event, end);
|
|
if (!calc_ev_end) {
|
|
pr_warn("event %zu has a calculated length which exceeds buffer length %zu: event=%pK end=%pK, offset=%zu\n",
|
|
event_idx, event_data_bytes, event, end,
|
|
offset);
|
|
return -1;
|
|
}
|
|
|
|
if (calc_ev_end > ev_end) {
|
|
pr_warn("event %zu exceeds it's own length: event=%pK, end=%pK, offset=%zu, calc_ev_end=%pK\n",
|
|
event_idx, event, ev_end, offset, calc_ev_end);
|
|
return -1;
|
|
}
|
|
|
|
return ev_len;
|
|
}
|
|
|
|
#define MAX_4K (SIZE_MAX / 4096)
|
|
|
|
static void create_events_from_catalog(struct attribute ***events_,
|
|
struct attribute ***event_descs_,
|
|
struct attribute ***event_long_descs_)
|
|
{
|
|
unsigned long hret;
|
|
size_t catalog_len, catalog_page_len, event_entry_count,
|
|
event_data_len, event_data_offs,
|
|
event_data_bytes, junk_events, event_idx, event_attr_ct, i,
|
|
attr_max, event_idx_last, desc_ct, long_desc_ct;
|
|
ssize_t ct, ev_len;
|
|
uint32_t catalog_version_num;
|
|
struct attribute **events, **event_descs, **event_long_descs;
|
|
struct hv_24x7_catalog_page_0 *page_0 =
|
|
kmem_cache_alloc(hv_page_cache, GFP_KERNEL);
|
|
void *page = page_0;
|
|
void *event_data, *end;
|
|
struct hv_24x7_event_data *event;
|
|
struct rb_root ev_uniq = RB_ROOT;
|
|
|
|
if (!page)
|
|
goto e_out;
|
|
|
|
hret = h_get_24x7_catalog_page(page, 0, 0);
|
|
if (hret)
|
|
goto e_free;
|
|
|
|
catalog_version_num = be64_to_cpu(page_0->version);
|
|
catalog_page_len = be32_to_cpu(page_0->length);
|
|
|
|
if (MAX_4K < catalog_page_len) {
|
|
pr_err("invalid page count: %zu\n", catalog_page_len);
|
|
goto e_free;
|
|
}
|
|
|
|
catalog_len = catalog_page_len * 4096;
|
|
|
|
event_entry_count = be16_to_cpu(page_0->event_entry_count);
|
|
event_data_offs = be16_to_cpu(page_0->event_data_offs);
|
|
event_data_len = be16_to_cpu(page_0->event_data_len);
|
|
|
|
pr_devel("cv %zu cl %zu eec %zu edo %zu edl %zu\n",
|
|
(size_t)catalog_version_num, catalog_len,
|
|
event_entry_count, event_data_offs, event_data_len);
|
|
|
|
if ((MAX_4K < event_data_len)
|
|
|| (MAX_4K < event_data_offs)
|
|
|| (MAX_4K - event_data_offs < event_data_len)) {
|
|
pr_err("invalid event data offs %zu and/or len %zu\n",
|
|
event_data_offs, event_data_len);
|
|
goto e_free;
|
|
}
|
|
|
|
if ((event_data_offs + event_data_len) > catalog_page_len) {
|
|
pr_err("event data %zu-%zu does not fit inside catalog 0-%zu\n",
|
|
event_data_offs,
|
|
event_data_offs + event_data_len,
|
|
catalog_page_len);
|
|
goto e_free;
|
|
}
|
|
|
|
if (SIZE_MAX / MAX_EVENTS_PER_EVENT_DATA - 1 < event_entry_count) {
|
|
pr_err("event_entry_count %zu is invalid\n",
|
|
event_entry_count);
|
|
goto e_free;
|
|
}
|
|
|
|
event_data_bytes = event_data_len * 4096;
|
|
|
|
/*
|
|
* event data can span several pages, events can cross between these
|
|
* pages. Use vmalloc to make this easier.
|
|
*/
|
|
event_data = vmalloc(event_data_bytes);
|
|
if (!event_data) {
|
|
pr_err("could not allocate event data\n");
|
|
goto e_free;
|
|
}
|
|
|
|
end = event_data + event_data_bytes;
|
|
|
|
/*
|
|
* using vmalloc_to_phys() like this only works if PAGE_SIZE is
|
|
* divisible by 4096
|
|
*/
|
|
BUILD_BUG_ON(PAGE_SIZE % 4096);
|
|
|
|
for (i = 0; i < event_data_len; i++) {
|
|
hret = h_get_24x7_catalog_page_(
|
|
vmalloc_to_phys(event_data + i * 4096),
|
|
catalog_version_num,
|
|
i + event_data_offs);
|
|
if (hret) {
|
|
pr_err("failed to get event data in page %zu\n",
|
|
i + event_data_offs);
|
|
goto e_event_data;
|
|
}
|
|
}
|
|
|
|
/*
|
|
* scan the catalog to determine the number of attributes we need, and
|
|
* verify it at the same time.
|
|
*/
|
|
for (junk_events = 0, event = event_data, event_idx = 0, attr_max = 0;
|
|
;
|
|
event_idx++, event = (void *)event + ev_len) {
|
|
size_t offset = (void *)event - (void *)event_data;
|
|
char *name;
|
|
int nl;
|
|
|
|
ev_len = catalog_event_len_validate(event, event_idx,
|
|
event_data_bytes,
|
|
event_entry_count,
|
|
offset, end);
|
|
if (ev_len < 0)
|
|
break;
|
|
|
|
name = event_name(event, &nl);
|
|
|
|
if (event->event_group_record_len == 0) {
|
|
pr_devel("invalid event %zu (%.*s): group_record_len == 0, skipping\n",
|
|
event_idx, nl, name);
|
|
junk_events++;
|
|
continue;
|
|
}
|
|
|
|
if (!catalog_entry_domain_is_valid(event->domain)) {
|
|
pr_info("event %zu (%.*s) has invalid domain %d\n",
|
|
event_idx, nl, name, event->domain);
|
|
junk_events++;
|
|
continue;
|
|
}
|
|
|
|
attr_max += event_to_attr_ct(event);
|
|
}
|
|
|
|
event_idx_last = event_idx;
|
|
if (event_idx_last != event_entry_count)
|
|
pr_warn("event buffer ended before listed # of events were parsed (got %zu, wanted %zu, junk %zu)\n",
|
|
event_idx_last, event_entry_count, junk_events);
|
|
|
|
events = kmalloc_array(attr_max + 1, sizeof(*events), GFP_KERNEL);
|
|
if (!events)
|
|
goto e_event_data;
|
|
|
|
event_descs = kmalloc_array(event_idx + 1, sizeof(*event_descs),
|
|
GFP_KERNEL);
|
|
if (!event_descs)
|
|
goto e_event_attrs;
|
|
|
|
event_long_descs = kmalloc_array(event_idx + 1,
|
|
sizeof(*event_long_descs), GFP_KERNEL);
|
|
if (!event_long_descs)
|
|
goto e_event_descs;
|
|
|
|
/* Iterate over the catalog filling in the attribute vector */
|
|
for (junk_events = 0, event_attr_ct = 0, desc_ct = 0, long_desc_ct = 0,
|
|
event = event_data, event_idx = 0;
|
|
event_idx < event_idx_last;
|
|
event_idx++, ev_len = be16_to_cpu(event->length),
|
|
event = (void *)event + ev_len) {
|
|
char *name;
|
|
int nl;
|
|
int nonce;
|
|
/*
|
|
* these are the only "bad" events that are intermixed and that
|
|
* we can ignore without issue. make sure to skip them here
|
|
*/
|
|
if (event->event_group_record_len == 0)
|
|
continue;
|
|
if (!catalog_entry_domain_is_valid(event->domain))
|
|
continue;
|
|
|
|
name = event_name(event, &nl);
|
|
nonce = event_uniq_add(&ev_uniq, name, nl, event->domain);
|
|
ct = event_data_to_attrs(event_idx, events + event_attr_ct,
|
|
event, nonce);
|
|
if (ct <= 0) {
|
|
pr_warn("event %zu (%.*s) creation failure, skipping\n",
|
|
event_idx, nl, name);
|
|
junk_events++;
|
|
} else {
|
|
event_attr_ct += ct;
|
|
event_descs[desc_ct] = event_to_desc_attr(event, nonce);
|
|
if (event_descs[desc_ct])
|
|
desc_ct++;
|
|
event_long_descs[long_desc_ct] =
|
|
event_to_long_desc_attr(event, nonce);
|
|
if (event_long_descs[long_desc_ct])
|
|
long_desc_ct++;
|
|
}
|
|
}
|
|
|
|
pr_info("read %zu catalog entries, created %zu event attrs (%zu failures), %zu descs\n",
|
|
event_idx, event_attr_ct, junk_events, desc_ct);
|
|
|
|
events[event_attr_ct] = NULL;
|
|
event_descs[desc_ct] = NULL;
|
|
event_long_descs[long_desc_ct] = NULL;
|
|
|
|
event_uniq_destroy(&ev_uniq);
|
|
vfree(event_data);
|
|
kmem_cache_free(hv_page_cache, page);
|
|
|
|
*events_ = events;
|
|
*event_descs_ = event_descs;
|
|
*event_long_descs_ = event_long_descs;
|
|
return;
|
|
|
|
e_event_descs:
|
|
kfree(event_descs);
|
|
e_event_attrs:
|
|
kfree(events);
|
|
e_event_data:
|
|
vfree(event_data);
|
|
e_free:
|
|
kmem_cache_free(hv_page_cache, page);
|
|
e_out:
|
|
*events_ = NULL;
|
|
*event_descs_ = NULL;
|
|
*event_long_descs_ = NULL;
|
|
}
|
|
|
|
static ssize_t catalog_read(struct file *filp, struct kobject *kobj,
|
|
struct bin_attribute *bin_attr, char *buf,
|
|
loff_t offset, size_t count)
|
|
{
|
|
unsigned long hret;
|
|
ssize_t ret = 0;
|
|
size_t catalog_len = 0, catalog_page_len = 0;
|
|
loff_t page_offset = 0;
|
|
loff_t offset_in_page;
|
|
size_t copy_len;
|
|
uint64_t catalog_version_num = 0;
|
|
void *page = kmem_cache_alloc(hv_page_cache, GFP_USER);
|
|
struct hv_24x7_catalog_page_0 *page_0 = page;
|
|
if (!page)
|
|
return -ENOMEM;
|
|
|
|
hret = h_get_24x7_catalog_page(page, 0, 0);
|
|
if (hret) {
|
|
ret = -EIO;
|
|
goto e_free;
|
|
}
|
|
|
|
catalog_version_num = be64_to_cpu(page_0->version);
|
|
catalog_page_len = be32_to_cpu(page_0->length);
|
|
catalog_len = catalog_page_len * 4096;
|
|
|
|
page_offset = offset / 4096;
|
|
offset_in_page = offset % 4096;
|
|
|
|
if (page_offset >= catalog_page_len)
|
|
goto e_free;
|
|
|
|
if (page_offset != 0) {
|
|
hret = h_get_24x7_catalog_page(page, catalog_version_num,
|
|
page_offset);
|
|
if (hret) {
|
|
ret = -EIO;
|
|
goto e_free;
|
|
}
|
|
}
|
|
|
|
copy_len = 4096 - offset_in_page;
|
|
if (copy_len > count)
|
|
copy_len = count;
|
|
|
|
memcpy(buf, page+offset_in_page, copy_len);
|
|
ret = copy_len;
|
|
|
|
e_free:
|
|
if (hret)
|
|
pr_err("h_get_24x7_catalog_page(ver=%lld, page=%lld) failed:"
|
|
" rc=%ld\n",
|
|
catalog_version_num, page_offset, hret);
|
|
kmem_cache_free(hv_page_cache, page);
|
|
|
|
pr_devel("catalog_read: offset=%lld(%lld) count=%zu "
|
|
"catalog_len=%zu(%zu) => %zd\n", offset, page_offset,
|
|
count, catalog_len, catalog_page_len, ret);
|
|
|
|
return ret;
|
|
}
|
|
|
|
#define PAGE_0_ATTR(_name, _fmt, _expr) \
|
|
static ssize_t _name##_show(struct device *dev, \
|
|
struct device_attribute *dev_attr, \
|
|
char *buf) \
|
|
{ \
|
|
unsigned long hret; \
|
|
ssize_t ret = 0; \
|
|
void *page = kmem_cache_alloc(hv_page_cache, GFP_USER); \
|
|
struct hv_24x7_catalog_page_0 *page_0 = page; \
|
|
if (!page) \
|
|
return -ENOMEM; \
|
|
hret = h_get_24x7_catalog_page(page, 0, 0); \
|
|
if (hret) { \
|
|
ret = -EIO; \
|
|
goto e_free; \
|
|
} \
|
|
ret = sprintf(buf, _fmt, _expr); \
|
|
e_free: \
|
|
kmem_cache_free(hv_page_cache, page); \
|
|
return ret; \
|
|
} \
|
|
static DEVICE_ATTR_RO(_name)
|
|
|
|
PAGE_0_ATTR(catalog_version, "%lld\n",
|
|
(unsigned long long)be64_to_cpu(page_0->version));
|
|
PAGE_0_ATTR(catalog_len, "%lld\n",
|
|
(unsigned long long)be32_to_cpu(page_0->length) * 4096);
|
|
static BIN_ATTR_RO(catalog, 0/* real length varies */);
|
|
|
|
static struct bin_attribute *if_bin_attrs[] = {
|
|
&bin_attr_catalog,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute *if_attrs[] = {
|
|
&dev_attr_catalog_len.attr,
|
|
&dev_attr_catalog_version.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group if_group = {
|
|
.name = "interface",
|
|
.bin_attrs = if_bin_attrs,
|
|
.attrs = if_attrs,
|
|
};
|
|
|
|
static const struct attribute_group *attr_groups[] = {
|
|
&format_group,
|
|
&event_group,
|
|
&event_desc_group,
|
|
&event_long_desc_group,
|
|
&if_group,
|
|
NULL,
|
|
};
|
|
|
|
DEFINE_PER_CPU(char, hv_24x7_reqb[4096]) __aligned(4096);
|
|
DEFINE_PER_CPU(char, hv_24x7_resb[4096]) __aligned(4096);
|
|
|
|
static unsigned long single_24x7_request(u8 domain, u32 offset, u16 ix,
|
|
u16 lpar, u64 *res,
|
|
bool success_expected)
|
|
{
|
|
unsigned long ret;
|
|
|
|
/*
|
|
* request_buffer and result_buffer are not required to be 4k aligned,
|
|
* but are not allowed to cross any 4k boundary. Aligning them to 4k is
|
|
* the simplest way to ensure that.
|
|
*/
|
|
struct reqb {
|
|
struct hv_24x7_request_buffer buf;
|
|
struct hv_24x7_request req;
|
|
} __packed *request_buffer;
|
|
|
|
struct {
|
|
struct hv_24x7_data_result_buffer buf;
|
|
struct hv_24x7_result res;
|
|
struct hv_24x7_result_element elem;
|
|
__be64 result;
|
|
} __packed *result_buffer;
|
|
|
|
BUILD_BUG_ON(sizeof(*request_buffer) > 4096);
|
|
BUILD_BUG_ON(sizeof(*result_buffer) > 4096);
|
|
|
|
request_buffer = (void *)get_cpu_var(hv_24x7_reqb);
|
|
result_buffer = (void *)get_cpu_var(hv_24x7_resb);
|
|
|
|
memset(request_buffer, 0, 4096);
|
|
memset(result_buffer, 0, 4096);
|
|
|
|
*request_buffer = (struct reqb) {
|
|
.buf = {
|
|
.interface_version = HV_24X7_IF_VERSION_CURRENT,
|
|
.num_requests = 1,
|
|
},
|
|
.req = {
|
|
.performance_domain = domain,
|
|
.data_size = cpu_to_be16(8),
|
|
.data_offset = cpu_to_be32(offset),
|
|
.starting_lpar_ix = cpu_to_be16(lpar),
|
|
.max_num_lpars = cpu_to_be16(1),
|
|
.starting_ix = cpu_to_be16(ix),
|
|
.max_ix = cpu_to_be16(1),
|
|
}
|
|
};
|
|
|
|
ret = plpar_hcall_norets(H_GET_24X7_DATA,
|
|
virt_to_phys(request_buffer), sizeof(*request_buffer),
|
|
virt_to_phys(result_buffer), sizeof(*result_buffer));
|
|
|
|
if (ret) {
|
|
if (success_expected)
|
|
pr_err_ratelimited("hcall failed: %d %#x %#x %d => "
|
|
"0x%lx (%ld) detail=0x%x failing ix=%x\n",
|
|
domain, offset, ix, lpar, ret, ret,
|
|
result_buffer->buf.detailed_rc,
|
|
result_buffer->buf.failing_request_ix);
|
|
goto out;
|
|
}
|
|
|
|
*res = be64_to_cpu(result_buffer->result);
|
|
|
|
out:
|
|
return ret;
|
|
}
|
|
|
|
static unsigned long event_24x7_request(struct perf_event *event, u64 *res,
|
|
bool success_expected)
|
|
{
|
|
u16 idx;
|
|
unsigned domain = event_get_domain(event);
|
|
|
|
if (is_physical_domain(domain))
|
|
idx = event_get_core(event);
|
|
else
|
|
idx = event_get_vcpu(event);
|
|
|
|
return single_24x7_request(event_get_domain(event),
|
|
event_get_offset(event),
|
|
idx,
|
|
event_get_lpar(event),
|
|
res,
|
|
success_expected);
|
|
}
|
|
|
|
static int h_24x7_event_init(struct perf_event *event)
|
|
{
|
|
struct hv_perf_caps caps;
|
|
unsigned domain;
|
|
unsigned long hret;
|
|
u64 ct;
|
|
|
|
/* Not our event */
|
|
if (event->attr.type != event->pmu->type)
|
|
return -ENOENT;
|
|
|
|
/* Unused areas must be 0 */
|
|
if (event_get_reserved1(event) ||
|
|
event_get_reserved2(event) ||
|
|
event_get_reserved3(event)) {
|
|
pr_devel("reserved set when forbidden 0x%llx(0x%llx) 0x%llx(0x%llx) 0x%llx(0x%llx)\n",
|
|
event->attr.config,
|
|
event_get_reserved1(event),
|
|
event->attr.config1,
|
|
event_get_reserved2(event),
|
|
event->attr.config2,
|
|
event_get_reserved3(event));
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* unsupported modes and filters */
|
|
if (event->attr.exclude_user ||
|
|
event->attr.exclude_kernel ||
|
|
event->attr.exclude_hv ||
|
|
event->attr.exclude_idle ||
|
|
event->attr.exclude_host ||
|
|
event->attr.exclude_guest)
|
|
return -EINVAL;
|
|
|
|
/* no branch sampling */
|
|
if (has_branch_stack(event))
|
|
return -EOPNOTSUPP;
|
|
|
|
/* offset must be 8 byte aligned */
|
|
if (event_get_offset(event) % 8) {
|
|
pr_devel("bad alignment\n");
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* Domains above 6 are invalid */
|
|
domain = event_get_domain(event);
|
|
if (domain > 6) {
|
|
pr_devel("invalid domain %d\n", domain);
|
|
return -EINVAL;
|
|
}
|
|
|
|
hret = hv_perf_caps_get(&caps);
|
|
if (hret) {
|
|
pr_devel("could not get capabilities: rc=%ld\n", hret);
|
|
return -EIO;
|
|
}
|
|
|
|
/* Physical domains & other lpars require extra capabilities */
|
|
if (!caps.collect_privileged && (is_physical_domain(domain) ||
|
|
(event_get_lpar(event) != event_get_lpar_max()))) {
|
|
pr_devel("hv permisions disallow: is_physical_domain:%d, lpar=0x%llx\n",
|
|
is_physical_domain(domain),
|
|
event_get_lpar(event));
|
|
return -EACCES;
|
|
}
|
|
|
|
/* see if the event complains */
|
|
if (event_24x7_request(event, &ct, false)) {
|
|
pr_devel("test hcall failed\n");
|
|
return -EIO;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static u64 h_24x7_get_value(struct perf_event *event)
|
|
{
|
|
unsigned long ret;
|
|
u64 ct;
|
|
ret = event_24x7_request(event, &ct, true);
|
|
if (ret)
|
|
/* We checked this in event init, shouldn't fail here... */
|
|
return 0;
|
|
|
|
return ct;
|
|
}
|
|
|
|
static void h_24x7_event_update(struct perf_event *event)
|
|
{
|
|
s64 prev;
|
|
u64 now;
|
|
now = h_24x7_get_value(event);
|
|
prev = local64_xchg(&event->hw.prev_count, now);
|
|
local64_add(now - prev, &event->count);
|
|
}
|
|
|
|
static void h_24x7_event_start(struct perf_event *event, int flags)
|
|
{
|
|
if (flags & PERF_EF_RELOAD)
|
|
local64_set(&event->hw.prev_count, h_24x7_get_value(event));
|
|
}
|
|
|
|
static void h_24x7_event_stop(struct perf_event *event, int flags)
|
|
{
|
|
h_24x7_event_update(event);
|
|
}
|
|
|
|
static int h_24x7_event_add(struct perf_event *event, int flags)
|
|
{
|
|
if (flags & PERF_EF_START)
|
|
h_24x7_event_start(event, flags);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static struct pmu h_24x7_pmu = {
|
|
.task_ctx_nr = perf_invalid_context,
|
|
|
|
.name = "hv_24x7",
|
|
.attr_groups = attr_groups,
|
|
.event_init = h_24x7_event_init,
|
|
.add = h_24x7_event_add,
|
|
.del = h_24x7_event_stop,
|
|
.start = h_24x7_event_start,
|
|
.stop = h_24x7_event_stop,
|
|
.read = h_24x7_event_update,
|
|
};
|
|
|
|
static int hv_24x7_init(void)
|
|
{
|
|
int r;
|
|
unsigned long hret;
|
|
struct hv_perf_caps caps;
|
|
|
|
if (!firmware_has_feature(FW_FEATURE_LPAR)) {
|
|
pr_debug("not a virtualized system, not enabling\n");
|
|
return -ENODEV;
|
|
}
|
|
|
|
hret = hv_perf_caps_get(&caps);
|
|
if (hret) {
|
|
pr_debug("could not obtain capabilities, not enabling, rc=%ld\n",
|
|
hret);
|
|
return -ENODEV;
|
|
}
|
|
|
|
hv_page_cache = kmem_cache_create("hv-page-4096", 4096, 4096, 0, NULL);
|
|
if (!hv_page_cache)
|
|
return -ENOMEM;
|
|
|
|
/* sampling not supported */
|
|
h_24x7_pmu.capabilities |= PERF_PMU_CAP_NO_INTERRUPT;
|
|
|
|
create_events_from_catalog(&event_group.attrs,
|
|
&event_desc_group.attrs,
|
|
&event_long_desc_group.attrs);
|
|
|
|
r = perf_pmu_register(&h_24x7_pmu, h_24x7_pmu.name, -1);
|
|
if (r)
|
|
return r;
|
|
|
|
return 0;
|
|
}
|
|
|
|
device_initcall(hv_24x7_init);
|