| // SPDX-License-Identifier: GPL-2.0 | |
| /* | |
| * Infrastructure for statistic tracing (histogram output). | |
| * | |
| * Copyright (C) 2008-2009 Frederic Weisbecker <fweisbec@gmail.com> | |
| * | |
| * Based on the code from trace_branch.c which is | |
| * Copyright (C) 2008 Steven Rostedt <srostedt@redhat.com> | |
| * | |
| */ | |
| #include <linux/security.h> | |
| #include <linux/list.h> | |
| #include <linux/slab.h> | |
| #include <linux/rbtree.h> | |
| #include <linux/tracefs.h> | |
| #include "trace_stat.h" | |
| #include "trace.h" | |
| /* | |
| * List of stat red-black nodes from a tracer | |
| * We use a such tree to sort quickly the stat | |
| * entries from the tracer. | |
| */ | |
| struct stat_node { | |
| struct rb_node node; | |
| void *stat; | |
| }; | |
| /* A stat session is the stats output in one file */ | |
| struct stat_session { | |
| struct list_head session_list; | |
| struct tracer_stat *ts; | |
| struct rb_root stat_root; | |
| struct mutex stat_mutex; | |
| struct dentry *file; | |
| }; | |
| /* All of the sessions currently in use. Each stat file embed one session */ | |
| static LIST_HEAD(all_stat_sessions); | |
| static DEFINE_MUTEX(all_stat_sessions_mutex); | |
| /* The root directory for all stat files */ | |
| static struct dentry *stat_dir; | |
| static void __reset_stat_session(struct stat_session *session) | |
| { | |
| struct stat_node *snode, *n; | |
| rbtree_postorder_for_each_entry_safe(snode, n, &session->stat_root, node) { | |
| if (session->ts->stat_release) | |
| session->ts->stat_release(snode->stat); | |
| kfree(snode); | |
| } | |
| session->stat_root = RB_ROOT; | |
| } | |
| static void reset_stat_session(struct stat_session *session) | |
| { | |
| mutex_lock(&session->stat_mutex); | |
| __reset_stat_session(session); | |
| mutex_unlock(&session->stat_mutex); | |
| } | |
| static void destroy_session(struct stat_session *session) | |
| { | |
| tracefs_remove(session->file); | |
| __reset_stat_session(session); | |
| mutex_destroy(&session->stat_mutex); | |
| kfree(session); | |
| } | |
| static int insert_stat(struct rb_root *root, void *stat, cmp_func_t cmp) | |
| { | |
| struct rb_node **new = &(root->rb_node), *parent = NULL; | |
| struct stat_node *data; | |
| data = kzalloc_obj(*data); | |
| if (!data) | |
| return -ENOMEM; | |
| data->stat = stat; | |
| /* | |
| * Figure out where to put new node | |
| * This is a descendent sorting | |
| */ | |
| while (*new) { | |
| struct stat_node *this; | |
| int result; | |
| this = container_of(*new, struct stat_node, node); | |
| result = cmp(data->stat, this->stat); | |
| parent = *new; | |
| if (result >= 0) | |
| new = &((*new)->rb_left); | |
| else | |
| new = &((*new)->rb_right); | |
| } | |
| rb_link_node(&data->node, parent, new); | |
| rb_insert_color(&data->node, root); | |
| return 0; | |
| } | |
| /* | |
| * For tracers that don't provide a stat_cmp callback. | |
| * This one will force an insertion as right-most node | |
| * in the rbtree. | |
| */ | |
| static int dummy_cmp(const void *p1, const void *p2) | |
| { | |
| return -1; | |
| } | |
| /* | |
| * Initialize the stat rbtree at each trace_stat file opening. | |
| * All of these copies and sorting are required on all opening | |
| * since the stats could have changed between two file sessions. | |
| */ | |
| static int stat_seq_init(struct stat_session *session) | |
| { | |
| struct tracer_stat *ts = session->ts; | |
| struct rb_root *root = &session->stat_root; | |
| void *stat; | |
| int ret = 0; | |
| int i; | |
| guard(mutex)(&session->stat_mutex); | |
| __reset_stat_session(session); | |
| if (!ts->stat_cmp) | |
| ts->stat_cmp = dummy_cmp; | |
| stat = ts->stat_start(ts); | |
| if (!stat) | |
| return 0; | |
| ret = insert_stat(root, stat, ts->stat_cmp); | |
| if (ret) | |
| return ret; | |
| /* | |
| * Iterate over the tracer stat entries and store them in an rbtree. | |
| */ | |
| for (i = 1; ; i++) { | |
| stat = ts->stat_next(stat, i); | |
| /* End of insertion */ | |
| if (!stat) | |
| break; | |
| ret = insert_stat(root, stat, ts->stat_cmp); | |
| if (ret) | |
| goto exit_free_rbtree; | |
| } | |
| return ret; | |
| exit_free_rbtree: | |
| __reset_stat_session(session); | |
| return ret; | |
| } | |
| static void *stat_seq_start(struct seq_file *s, loff_t *pos) | |
| { | |
| struct stat_session *session = s->private; | |
| struct rb_node *node; | |
| int n = *pos; | |
| int i; | |
| /* Prevent from tracer switch or rbtree modification */ | |
| mutex_lock(&session->stat_mutex); | |
| /* If we are in the beginning of the file, print the headers */ | |
| if (session->ts->stat_headers) { | |
| if (n == 0) | |
| return SEQ_START_TOKEN; | |
| n--; | |
| } | |
| node = rb_first(&session->stat_root); | |
| for (i = 0; node && i < n; i++) | |
| node = rb_next(node); | |
| return node; | |
| } | |
| static void *stat_seq_next(struct seq_file *s, void *p, loff_t *pos) | |
| { | |
| struct stat_session *session = s->private; | |
| struct rb_node *node = p; | |
| (*pos)++; | |
| if (p == SEQ_START_TOKEN) | |
| return rb_first(&session->stat_root); | |
| return rb_next(node); | |
| } | |
| static void stat_seq_stop(struct seq_file *s, void *p) | |
| { | |
| struct stat_session *session = s->private; | |
| mutex_unlock(&session->stat_mutex); | |
| } | |
| static int stat_seq_show(struct seq_file *s, void *v) | |
| { | |
| struct stat_session *session = s->private; | |
| struct stat_node *l = container_of(v, struct stat_node, node); | |
| if (v == SEQ_START_TOKEN) | |
| return session->ts->stat_headers(s); | |
| return session->ts->stat_show(s, l->stat); | |
| } | |
| static const struct seq_operations trace_stat_seq_ops = { | |
| .start = stat_seq_start, | |
| .next = stat_seq_next, | |
| .stop = stat_seq_stop, | |
| .show = stat_seq_show | |
| }; | |
| /* The session stat is refilled and resorted at each stat file opening */ | |
| static int tracing_stat_open(struct inode *inode, struct file *file) | |
| { | |
| int ret; | |
| struct seq_file *m; | |
| struct stat_session *session = inode->i_private; | |
| ret = security_locked_down(LOCKDOWN_TRACEFS); | |
| if (ret) | |
| return ret; | |
| ret = stat_seq_init(session); | |
| if (ret) | |
| return ret; | |
| ret = seq_open(file, &trace_stat_seq_ops); | |
| if (ret) { | |
| reset_stat_session(session); | |
| return ret; | |
| } | |
| m = file->private_data; | |
| m->private = session; | |
| return ret; | |
| } | |
| /* | |
| * Avoid consuming memory with our now useless rbtree. | |
| */ | |
| static int tracing_stat_release(struct inode *i, struct file *f) | |
| { | |
| struct stat_session *session = i->i_private; | |
| reset_stat_session(session); | |
| return seq_release(i, f); | |
| } | |
| static const struct file_operations tracing_stat_fops = { | |
| .open = tracing_stat_open, | |
| .read = seq_read, | |
| .llseek = seq_lseek, | |
| .release = tracing_stat_release | |
| }; | |
| static int tracing_stat_init(void) | |
| { | |
| int ret; | |
| ret = tracing_init_dentry(); | |
| if (ret) | |
| return -ENODEV; | |
| stat_dir = tracefs_create_dir("trace_stat", NULL); | |
| if (!stat_dir) { | |
| pr_warn("Could not create tracefs 'trace_stat' entry\n"); | |
| return -ENOMEM; | |
| } | |
| return 0; | |
| } | |
| static int init_stat_file(struct stat_session *session) | |
| { | |
| int ret; | |
| if (!stat_dir && (ret = tracing_stat_init())) | |
| return ret; | |
| session->file = tracefs_create_file(session->ts->name, TRACE_MODE_WRITE, | |
| stat_dir, session, | |
| &tracing_stat_fops); | |
| if (!session->file) | |
| return -ENOMEM; | |
| return 0; | |
| } | |
| int register_stat_tracer(struct tracer_stat *trace) | |
| { | |
| struct stat_session *session, *node; | |
| int ret; | |
| if (!trace) | |
| return -EINVAL; | |
| if (!trace->stat_start || !trace->stat_next || !trace->stat_show) | |
| return -EINVAL; | |
| guard(mutex)(&all_stat_sessions_mutex); | |
| /* Already registered? */ | |
| list_for_each_entry(node, &all_stat_sessions, session_list) { | |
| if (node->ts == trace) | |
| return -EINVAL; | |
| } | |
| /* Init the session */ | |
| session = kzalloc_obj(*session); | |
| if (!session) | |
| return -ENOMEM; | |
| session->ts = trace; | |
| INIT_LIST_HEAD(&session->session_list); | |
| mutex_init(&session->stat_mutex); | |
| ret = init_stat_file(session); | |
| if (ret) { | |
| destroy_session(session); | |
| return ret; | |
| } | |
| /* Register */ | |
| list_add_tail(&session->session_list, &all_stat_sessions); | |
| return 0; | |
| } | |
| void unregister_stat_tracer(struct tracer_stat *trace) | |
| { | |
| struct stat_session *node, *tmp; | |
| mutex_lock(&all_stat_sessions_mutex); | |
| list_for_each_entry_safe(node, tmp, &all_stat_sessions, session_list) { | |
| if (node->ts == trace) { | |
| list_del(&node->session_list); | |
| destroy_session(node); | |
| break; | |
| } | |
| } | |
| mutex_unlock(&all_stat_sessions_mutex); | |
| } |