|
@@ -33,10 +33,11 @@
|
|
* @children: child nodes
|
|
* @children: child nodes
|
|
* @all: list head for list of all nodes
|
|
* @all: list head for list of all nodes
|
|
* @parent: parent node
|
|
* @parent: parent node
|
|
- * @info: associated profiling data structure if not a directory
|
|
|
|
- * @ghost: when an object file containing profiling data is unloaded we keep a
|
|
|
|
- * copy of the profiling data here to allow collecting coverage data
|
|
|
|
- * for cleanup code. Such a node is called a "ghost".
|
|
|
|
|
|
+ * @loaded_info: array of pointers to profiling data sets for loaded object
|
|
|
|
+ * files.
|
|
|
|
+ * @num_loaded: number of profiling data sets for loaded object files.
|
|
|
|
+ * @unloaded_info: accumulated copy of profiling data sets for unloaded
|
|
|
|
+ * object files. Used only when gcov_persist=1.
|
|
* @dentry: main debugfs entry, either a directory or data file
|
|
* @dentry: main debugfs entry, either a directory or data file
|
|
* @links: associated symbolic links
|
|
* @links: associated symbolic links
|
|
* @name: data file basename
|
|
* @name: data file basename
|
|
@@ -51,10 +52,11 @@ struct gcov_node {
|
|
struct list_head children;
|
|
struct list_head children;
|
|
struct list_head all;
|
|
struct list_head all;
|
|
struct gcov_node *parent;
|
|
struct gcov_node *parent;
|
|
- struct gcov_info *info;
|
|
|
|
- struct gcov_info *ghost;
|
|
|
|
|
|
+ struct gcov_info **loaded_info;
|
|
|
|
+ struct gcov_info *unloaded_info;
|
|
struct dentry *dentry;
|
|
struct dentry *dentry;
|
|
struct dentry **links;
|
|
struct dentry **links;
|
|
|
|
+ int num_loaded;
|
|
char name[0];
|
|
char name[0];
|
|
};
|
|
};
|
|
|
|
|
|
@@ -136,16 +138,37 @@ static const struct seq_operations gcov_seq_ops = {
|
|
};
|
|
};
|
|
|
|
|
|
/*
|
|
/*
|
|
- * Return the profiling data set for a given node. This can either be the
|
|
|
|
- * original profiling data structure or a duplicate (also called "ghost")
|
|
|
|
- * in case the associated object file has been unloaded.
|
|
|
|
|
|
+ * Return a profiling data set associated with the given node. This is
|
|
|
|
+ * either a data set for a loaded object file or a data set copy in case
|
|
|
|
+ * all associated object files have been unloaded.
|
|
*/
|
|
*/
|
|
static struct gcov_info *get_node_info(struct gcov_node *node)
|
|
static struct gcov_info *get_node_info(struct gcov_node *node)
|
|
{
|
|
{
|
|
- if (node->info)
|
|
|
|
- return node->info;
|
|
|
|
|
|
+ if (node->num_loaded > 0)
|
|
|
|
+ return node->loaded_info[0];
|
|
|
|
|
|
- return node->ghost;
|
|
|
|
|
|
+ return node->unloaded_info;
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/*
|
|
|
|
+ * Return a newly allocated profiling data set which contains the sum of
|
|
|
|
+ * all profiling data associated with the given node.
|
|
|
|
+ */
|
|
|
|
+static struct gcov_info *get_accumulated_info(struct gcov_node *node)
|
|
|
|
+{
|
|
|
|
+ struct gcov_info *info;
|
|
|
|
+ int i = 0;
|
|
|
|
+
|
|
|
|
+ if (node->unloaded_info)
|
|
|
|
+ info = gcov_info_dup(node->unloaded_info);
|
|
|
|
+ else
|
|
|
|
+ info = gcov_info_dup(node->loaded_info[i++]);
|
|
|
|
+ if (!info)
|
|
|
|
+ return NULL;
|
|
|
|
+ for (; i < node->num_loaded; i++)
|
|
|
|
+ gcov_info_add(info, node->loaded_info[i]);
|
|
|
|
+
|
|
|
|
+ return info;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
/*
|
|
@@ -163,9 +186,10 @@ static int gcov_seq_open(struct inode *inode, struct file *file)
|
|
mutex_lock(&node_lock);
|
|
mutex_lock(&node_lock);
|
|
/*
|
|
/*
|
|
* Read from a profiling data copy to minimize reference tracking
|
|
* Read from a profiling data copy to minimize reference tracking
|
|
- * complexity and concurrent access.
|
|
|
|
|
|
+ * complexity and concurrent access and to keep accumulating multiple
|
|
|
|
+ * profiling data sets associated with one node simple.
|
|
*/
|
|
*/
|
|
- info = gcov_info_dup(get_node_info(node));
|
|
|
|
|
|
+ info = get_accumulated_info(node);
|
|
if (!info)
|
|
if (!info)
|
|
goto out_unlock;
|
|
goto out_unlock;
|
|
iter = gcov_iter_new(info);
|
|
iter = gcov_iter_new(info);
|
|
@@ -225,12 +249,25 @@ static struct gcov_node *get_node_by_name(const char *name)
|
|
return NULL;
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+/*
|
|
|
|
+ * Reset all profiling data associated with the specified node.
|
|
|
|
+ */
|
|
|
|
+static void reset_node(struct gcov_node *node)
|
|
|
|
+{
|
|
|
|
+ int i;
|
|
|
|
+
|
|
|
|
+ if (node->unloaded_info)
|
|
|
|
+ gcov_info_reset(node->unloaded_info);
|
|
|
|
+ for (i = 0; i < node->num_loaded; i++)
|
|
|
|
+ gcov_info_reset(node->loaded_info[i]);
|
|
|
|
+}
|
|
|
|
+
|
|
static void remove_node(struct gcov_node *node);
|
|
static void remove_node(struct gcov_node *node);
|
|
|
|
|
|
/*
|
|
/*
|
|
* write() implementation for gcov data files. Reset profiling data for the
|
|
* write() implementation for gcov data files. Reset profiling data for the
|
|
- * associated file. If the object file has been unloaded (i.e. this is
|
|
|
|
- * a "ghost" node), remove the debug fs node as well.
|
|
|
|
|
|
+ * corresponding file. If all associated object files have been unloaded,
|
|
|
|
+ * remove the debug fs node as well.
|
|
*/
|
|
*/
|
|
static ssize_t gcov_seq_write(struct file *file, const char __user *addr,
|
|
static ssize_t gcov_seq_write(struct file *file, const char __user *addr,
|
|
size_t len, loff_t *pos)
|
|
size_t len, loff_t *pos)
|
|
@@ -245,10 +282,10 @@ static ssize_t gcov_seq_write(struct file *file, const char __user *addr,
|
|
node = get_node_by_name(info->filename);
|
|
node = get_node_by_name(info->filename);
|
|
if (node) {
|
|
if (node) {
|
|
/* Reset counts or remove node for unloaded modules. */
|
|
/* Reset counts or remove node for unloaded modules. */
|
|
- if (node->ghost)
|
|
|
|
|
|
+ if (node->num_loaded == 0)
|
|
remove_node(node);
|
|
remove_node(node);
|
|
else
|
|
else
|
|
- gcov_info_reset(node->info);
|
|
|
|
|
|
+ reset_node(node);
|
|
}
|
|
}
|
|
/* Reset counts for open file. */
|
|
/* Reset counts for open file. */
|
|
gcov_info_reset(info);
|
|
gcov_info_reset(info);
|
|
@@ -378,7 +415,10 @@ static void init_node(struct gcov_node *node, struct gcov_info *info,
|
|
INIT_LIST_HEAD(&node->list);
|
|
INIT_LIST_HEAD(&node->list);
|
|
INIT_LIST_HEAD(&node->children);
|
|
INIT_LIST_HEAD(&node->children);
|
|
INIT_LIST_HEAD(&node->all);
|
|
INIT_LIST_HEAD(&node->all);
|
|
- node->info = info;
|
|
|
|
|
|
+ if (node->loaded_info) {
|
|
|
|
+ node->loaded_info[0] = info;
|
|
|
|
+ node->num_loaded = 1;
|
|
|
|
+ }
|
|
node->parent = parent;
|
|
node->parent = parent;
|
|
if (name)
|
|
if (name)
|
|
strcpy(node->name, name);
|
|
strcpy(node->name, name);
|
|
@@ -394,9 +434,13 @@ static struct gcov_node *new_node(struct gcov_node *parent,
|
|
struct gcov_node *node;
|
|
struct gcov_node *node;
|
|
|
|
|
|
node = kzalloc(sizeof(struct gcov_node) + strlen(name) + 1, GFP_KERNEL);
|
|
node = kzalloc(sizeof(struct gcov_node) + strlen(name) + 1, GFP_KERNEL);
|
|
- if (!node) {
|
|
|
|
- pr_warning("out of memory\n");
|
|
|
|
- return NULL;
|
|
|
|
|
|
+ if (!node)
|
|
|
|
+ goto err_nomem;
|
|
|
|
+ if (info) {
|
|
|
|
+ node->loaded_info = kcalloc(1, sizeof(struct gcov_info *),
|
|
|
|
+ GFP_KERNEL);
|
|
|
|
+ if (!node->loaded_info)
|
|
|
|
+ goto err_nomem;
|
|
}
|
|
}
|
|
init_node(node, info, name, parent);
|
|
init_node(node, info, name, parent);
|
|
/* Differentiate between gcov data file nodes and directory nodes. */
|
|
/* Differentiate between gcov data file nodes and directory nodes. */
|
|
@@ -416,6 +460,11 @@ static struct gcov_node *new_node(struct gcov_node *parent,
|
|
list_add(&node->all, &all_head);
|
|
list_add(&node->all, &all_head);
|
|
|
|
|
|
return node;
|
|
return node;
|
|
|
|
+
|
|
|
|
+err_nomem:
|
|
|
|
+ kfree(node);
|
|
|
|
+ pr_warning("out of memory\n");
|
|
|
|
+ return NULL;
|
|
}
|
|
}
|
|
|
|
|
|
/* Remove symbolic links associated with node. */
|
|
/* Remove symbolic links associated with node. */
|
|
@@ -441,8 +490,9 @@ static void release_node(struct gcov_node *node)
|
|
list_del(&node->all);
|
|
list_del(&node->all);
|
|
debugfs_remove(node->dentry);
|
|
debugfs_remove(node->dentry);
|
|
remove_links(node);
|
|
remove_links(node);
|
|
- if (node->ghost)
|
|
|
|
- gcov_info_free(node->ghost);
|
|
|
|
|
|
+ kfree(node->loaded_info);
|
|
|
|
+ if (node->unloaded_info)
|
|
|
|
+ gcov_info_free(node->unloaded_info);
|
|
kfree(node);
|
|
kfree(node);
|
|
}
|
|
}
|
|
|
|
|
|
@@ -477,7 +527,7 @@ static struct gcov_node *get_child_by_name(struct gcov_node *parent,
|
|
|
|
|
|
/*
|
|
/*
|
|
* write() implementation for reset file. Reset all profiling data to zero
|
|
* write() implementation for reset file. Reset all profiling data to zero
|
|
- * and remove ghost nodes.
|
|
|
|
|
|
+ * and remove nodes for which all associated object files are unloaded.
|
|
*/
|
|
*/
|
|
static ssize_t reset_write(struct file *file, const char __user *addr,
|
|
static ssize_t reset_write(struct file *file, const char __user *addr,
|
|
size_t len, loff_t *pos)
|
|
size_t len, loff_t *pos)
|
|
@@ -487,8 +537,8 @@ static ssize_t reset_write(struct file *file, const char __user *addr,
|
|
mutex_lock(&node_lock);
|
|
mutex_lock(&node_lock);
|
|
restart:
|
|
restart:
|
|
list_for_each_entry(node, &all_head, all) {
|
|
list_for_each_entry(node, &all_head, all) {
|
|
- if (node->info)
|
|
|
|
- gcov_info_reset(node->info);
|
|
|
|
|
|
+ if (node->num_loaded > 0)
|
|
|
|
+ reset_node(node);
|
|
else if (list_empty(&node->children)) {
|
|
else if (list_empty(&node->children)) {
|
|
remove_node(node);
|
|
remove_node(node);
|
|
/* Several nodes may have gone - restart loop. */
|
|
/* Several nodes may have gone - restart loop. */
|
|
@@ -564,37 +614,115 @@ err_remove:
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
/*
|
|
- * The profiling data set associated with this node is being unloaded. Store a
|
|
|
|
- * copy of the profiling data and turn this node into a "ghost".
|
|
|
|
|
|
+ * Associate a profiling data set with an existing node. Needs to be called
|
|
|
|
+ * with node_lock held.
|
|
*/
|
|
*/
|
|
-static int ghost_node(struct gcov_node *node)
|
|
|
|
|
|
+static void add_info(struct gcov_node *node, struct gcov_info *info)
|
|
{
|
|
{
|
|
- node->ghost = gcov_info_dup(node->info);
|
|
|
|
- if (!node->ghost) {
|
|
|
|
- pr_warning("could not save data for '%s' (out of memory)\n",
|
|
|
|
- node->info->filename);
|
|
|
|
- return -ENOMEM;
|
|
|
|
|
|
+ struct gcov_info **loaded_info;
|
|
|
|
+ int num = node->num_loaded;
|
|
|
|
+
|
|
|
|
+ /*
|
|
|
|
+ * Prepare new array. This is done first to simplify cleanup in
|
|
|
|
+ * case the new data set is incompatible, the node only contains
|
|
|
|
+ * unloaded data sets and there's not enough memory for the array.
|
|
|
|
+ */
|
|
|
|
+ loaded_info = kcalloc(num + 1, sizeof(struct gcov_info *), GFP_KERNEL);
|
|
|
|
+ if (!loaded_info) {
|
|
|
|
+ pr_warning("could not add '%s' (out of memory)\n",
|
|
|
|
+ info->filename);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
|
|
+ memcpy(loaded_info, node->loaded_info,
|
|
|
|
+ num * sizeof(struct gcov_info *));
|
|
|
|
+ loaded_info[num] = info;
|
|
|
|
+ /* Check if the new data set is compatible. */
|
|
|
|
+ if (num == 0) {
|
|
|
|
+ /*
|
|
|
|
+ * A module was unloaded, modified and reloaded. The new
|
|
|
|
+ * data set replaces the copy of the last one.
|
|
|
|
+ */
|
|
|
|
+ if (!gcov_info_is_compatible(node->unloaded_info, info)) {
|
|
|
|
+ pr_warning("discarding saved data for %s "
|
|
|
|
+ "(incompatible version)\n", info->filename);
|
|
|
|
+ gcov_info_free(node->unloaded_info);
|
|
|
|
+ node->unloaded_info = NULL;
|
|
|
|
+ }
|
|
|
|
+ } else {
|
|
|
|
+ /*
|
|
|
|
+ * Two different versions of the same object file are loaded.
|
|
|
|
+ * The initial one takes precedence.
|
|
|
|
+ */
|
|
|
|
+ if (!gcov_info_is_compatible(node->loaded_info[0], info)) {
|
|
|
|
+ pr_warning("could not add '%s' (incompatible "
|
|
|
|
+ "version)\n", info->filename);
|
|
|
|
+ kfree(loaded_info);
|
|
|
|
+ return;
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- node->info = NULL;
|
|
|
|
|
|
+ /* Overwrite previous array. */
|
|
|
|
+ kfree(node->loaded_info);
|
|
|
|
+ node->loaded_info = loaded_info;
|
|
|
|
+ node->num_loaded = num + 1;
|
|
|
|
+}
|
|
|
|
|
|
- return 0;
|
|
|
|
|
|
+/*
|
|
|
|
+ * Return the index of a profiling data set associated with a node.
|
|
|
|
+ */
|
|
|
|
+static int get_info_index(struct gcov_node *node, struct gcov_info *info)
|
|
|
|
+{
|
|
|
|
+ int i;
|
|
|
|
+
|
|
|
|
+ for (i = 0; i < node->num_loaded; i++) {
|
|
|
|
+ if (node->loaded_info[i] == info)
|
|
|
|
+ return i;
|
|
|
|
+ }
|
|
|
|
+ return -ENOENT;
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
/*
|
|
- * Profiling data for this node has been loaded again. Add profiling data
|
|
|
|
- * from previous instantiation and turn this node into a regular node.
|
|
|
|
|
|
+ * Save the data of a profiling data set which is being unloaded.
|
|
*/
|
|
*/
|
|
-static void revive_node(struct gcov_node *node, struct gcov_info *info)
|
|
|
|
|
|
+static void save_info(struct gcov_node *node, struct gcov_info *info)
|
|
{
|
|
{
|
|
- if (gcov_info_is_compatible(node->ghost, info))
|
|
|
|
- gcov_info_add(info, node->ghost);
|
|
|
|
|
|
+ if (node->unloaded_info)
|
|
|
|
+ gcov_info_add(node->unloaded_info, info);
|
|
else {
|
|
else {
|
|
- pr_warning("discarding saved data for '%s' (version changed)\n",
|
|
|
|
|
|
+ node->unloaded_info = gcov_info_dup(info);
|
|
|
|
+ if (!node->unloaded_info) {
|
|
|
|
+ pr_warning("could not save data for '%s' "
|
|
|
|
+ "(out of memory)\n", info->filename);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+/*
|
|
|
|
+ * Disassociate a profiling data set from a node. Needs to be called with
|
|
|
|
+ * node_lock held.
|
|
|
|
+ */
|
|
|
|
+static void remove_info(struct gcov_node *node, struct gcov_info *info)
|
|
|
|
+{
|
|
|
|
+ int i;
|
|
|
|
+
|
|
|
|
+ i = get_info_index(node, info);
|
|
|
|
+ if (i < 0) {
|
|
|
|
+ pr_warning("could not remove '%s' (not found)\n",
|
|
info->filename);
|
|
info->filename);
|
|
|
|
+ return;
|
|
}
|
|
}
|
|
- gcov_info_free(node->ghost);
|
|
|
|
- node->ghost = NULL;
|
|
|
|
- node->info = info;
|
|
|
|
|
|
+ if (gcov_persist)
|
|
|
|
+ save_info(node, info);
|
|
|
|
+ /* Shrink array. */
|
|
|
|
+ node->loaded_info[i] = node->loaded_info[node->num_loaded - 1];
|
|
|
|
+ node->num_loaded--;
|
|
|
|
+ if (node->num_loaded > 0)
|
|
|
|
+ return;
|
|
|
|
+ /* Last loaded data set was removed. */
|
|
|
|
+ kfree(node->loaded_info);
|
|
|
|
+ node->loaded_info = NULL;
|
|
|
|
+ node->num_loaded = 0;
|
|
|
|
+ if (!node->unloaded_info)
|
|
|
|
+ remove_node(node);
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
/*
|
|
@@ -609,30 +737,18 @@ void gcov_event(enum gcov_action action, struct gcov_info *info)
|
|
node = get_node_by_name(info->filename);
|
|
node = get_node_by_name(info->filename);
|
|
switch (action) {
|
|
switch (action) {
|
|
case GCOV_ADD:
|
|
case GCOV_ADD:
|
|
- /* Add new node or revive ghost. */
|
|
|
|
- if (!node) {
|
|
|
|
|
|
+ if (node)
|
|
|
|
+ add_info(node, info);
|
|
|
|
+ else
|
|
add_node(info);
|
|
add_node(info);
|
|
- break;
|
|
|
|
- }
|
|
|
|
- if (gcov_persist)
|
|
|
|
- revive_node(node, info);
|
|
|
|
- else {
|
|
|
|
- pr_warning("could not add '%s' (already exists)\n",
|
|
|
|
- info->filename);
|
|
|
|
- }
|
|
|
|
break;
|
|
break;
|
|
case GCOV_REMOVE:
|
|
case GCOV_REMOVE:
|
|
- /* Remove node or turn into ghost. */
|
|
|
|
- if (!node) {
|
|
|
|
|
|
+ if (node)
|
|
|
|
+ remove_info(node, info);
|
|
|
|
+ else {
|
|
pr_warning("could not remove '%s' (not found)\n",
|
|
pr_warning("could not remove '%s' (not found)\n",
|
|
info->filename);
|
|
info->filename);
|
|
- break;
|
|
|
|
}
|
|
}
|
|
- if (gcov_persist) {
|
|
|
|
- if (!ghost_node(node))
|
|
|
|
- break;
|
|
|
|
- }
|
|
|
|
- remove_node(node);
|
|
|
|
break;
|
|
break;
|
|
}
|
|
}
|
|
mutex_unlock(&node_lock);
|
|
mutex_unlock(&node_lock);
|