2011-01-18 17:15:24 +00:00
|
|
|
#include <dirent.h>
|
2012-01-19 16:08:15 +00:00
|
|
|
#include <limits.h>
|
|
|
|
#include <stdbool.h>
|
2011-01-18 17:15:24 +00:00
|
|
|
#include <stdlib.h>
|
|
|
|
#include <stdio.h>
|
2012-01-19 16:08:15 +00:00
|
|
|
#include <sys/types.h>
|
|
|
|
#include <sys/stat.h>
|
|
|
|
#include <unistd.h>
|
2012-02-08 16:32:52 +00:00
|
|
|
#include "strlist.h"
|
|
|
|
#include <string.h>
|
2015-06-26 09:29:07 +00:00
|
|
|
#include <api/fs/fs.h>
|
2015-06-22 22:36:05 +00:00
|
|
|
#include "asm/bug.h"
|
2011-01-18 17:15:24 +00:00
|
|
|
#include "thread_map.h"
|
2013-12-26 20:41:15 +00:00
|
|
|
#include "util.h"
|
2015-06-26 09:29:07 +00:00
|
|
|
#include "debug.h"
|
2015-10-25 14:51:21 +00:00
|
|
|
#include "event.h"
|
2011-01-18 17:15:24 +00:00
|
|
|
|
|
|
|
/* Skip "." and ".." directories */
|
|
|
|
static int filter(const struct dirent *dir)
|
|
|
|
{
|
|
|
|
if (dir->d_name[0] == '.')
|
|
|
|
return 0;
|
|
|
|
else
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
2015-06-26 09:29:06 +00:00
|
|
|
static void thread_map__reset(struct thread_map *map, int start, int nr)
|
|
|
|
{
|
|
|
|
size_t size = (nr - start) * sizeof(map->map[0]);
|
|
|
|
|
|
|
|
memset(&map->map[start], 0, size);
|
|
|
|
}
|
|
|
|
|
2015-06-14 08:19:17 +00:00
|
|
|
static struct thread_map *thread_map__realloc(struct thread_map *map, int nr)
|
|
|
|
{
|
2015-06-25 17:48:49 +00:00
|
|
|
size_t size = sizeof(*map) + sizeof(map->map[0]) * nr;
|
2015-06-26 09:29:06 +00:00
|
|
|
int start = map ? map->nr : 0;
|
|
|
|
|
|
|
|
map = realloc(map, size);
|
|
|
|
/*
|
|
|
|
* We only realloc to add more items, let's reset new items.
|
|
|
|
*/
|
|
|
|
if (map)
|
|
|
|
thread_map__reset(map, start, nr);
|
2015-06-14 08:19:17 +00:00
|
|
|
|
2015-06-26 09:29:06 +00:00
|
|
|
return map;
|
2015-06-14 08:19:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#define thread_map__alloc(__nr) thread_map__realloc(NULL, __nr)
|
|
|
|
|
2011-01-18 17:15:24 +00:00
|
|
|
struct thread_map *thread_map__new_by_pid(pid_t pid)
|
|
|
|
{
|
|
|
|
struct thread_map *threads;
|
|
|
|
char name[256];
|
|
|
|
int items;
|
|
|
|
struct dirent **namelist = NULL;
|
|
|
|
int i;
|
|
|
|
|
|
|
|
sprintf(name, "/proc/%d/task", pid);
|
|
|
|
items = scandir(name, &namelist, filter, NULL);
|
|
|
|
if (items <= 0)
|
2012-01-19 16:08:15 +00:00
|
|
|
return NULL;
|
2011-01-18 17:15:24 +00:00
|
|
|
|
2015-06-14 08:19:17 +00:00
|
|
|
threads = thread_map__alloc(items);
|
2011-01-18 17:15:24 +00:00
|
|
|
if (threads != NULL) {
|
|
|
|
for (i = 0; i < items; i++)
|
2015-06-22 22:36:02 +00:00
|
|
|
thread_map__set_pid(threads, i, atoi(namelist[i]->d_name));
|
2011-01-18 17:15:24 +00:00
|
|
|
threads->nr = items;
|
2015-06-22 22:36:05 +00:00
|
|
|
atomic_set(&threads->refcnt, 1);
|
2011-01-18 17:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (i=0; i<items; i++)
|
2013-12-27 19:55:14 +00:00
|
|
|
zfree(&namelist[i]);
|
2011-01-18 17:15:24 +00:00
|
|
|
free(namelist);
|
|
|
|
|
|
|
|
return threads;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct thread_map *thread_map__new_by_tid(pid_t tid)
|
|
|
|
{
|
2015-06-14 08:19:17 +00:00
|
|
|
struct thread_map *threads = thread_map__alloc(1);
|
2011-01-18 17:15:24 +00:00
|
|
|
|
|
|
|
if (threads != NULL) {
|
2015-06-22 22:36:02 +00:00
|
|
|
thread_map__set_pid(threads, 0, tid);
|
|
|
|
threads->nr = 1;
|
2015-06-22 22:36:05 +00:00
|
|
|
atomic_set(&threads->refcnt, 1);
|
2011-01-18 17:15:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return threads;
|
|
|
|
}
|
|
|
|
|
2012-01-19 16:08:15 +00:00
|
|
|
struct thread_map *thread_map__new_by_uid(uid_t uid)
|
|
|
|
{
|
|
|
|
DIR *proc;
|
|
|
|
int max_threads = 32, items, i;
|
|
|
|
char path[256];
|
|
|
|
struct dirent dirent, *next, **namelist = NULL;
|
2015-06-14 08:19:17 +00:00
|
|
|
struct thread_map *threads = thread_map__alloc(max_threads);
|
|
|
|
|
2012-01-19 16:08:15 +00:00
|
|
|
if (threads == NULL)
|
|
|
|
goto out;
|
|
|
|
|
|
|
|
proc = opendir("/proc");
|
|
|
|
if (proc == NULL)
|
|
|
|
goto out_free_threads;
|
|
|
|
|
|
|
|
threads->nr = 0;
|
2015-06-22 22:36:05 +00:00
|
|
|
atomic_set(&threads->refcnt, 1);
|
2012-01-19 16:08:15 +00:00
|
|
|
|
|
|
|
while (!readdir_r(proc, &dirent, &next) && next) {
|
|
|
|
char *end;
|
|
|
|
bool grow = false;
|
|
|
|
struct stat st;
|
|
|
|
pid_t pid = strtol(dirent.d_name, &end, 10);
|
|
|
|
|
|
|
|
if (*end) /* only interested in proper numerical dirents */
|
|
|
|
continue;
|
|
|
|
|
|
|
|
snprintf(path, sizeof(path), "/proc/%s", dirent.d_name);
|
|
|
|
|
|
|
|
if (stat(path, &st) != 0)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
if (st.st_uid != uid)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
snprintf(path, sizeof(path), "/proc/%d/task", pid);
|
|
|
|
items = scandir(path, &namelist, filter, NULL);
|
|
|
|
if (items <= 0)
|
|
|
|
goto out_free_closedir;
|
|
|
|
|
|
|
|
while (threads->nr + items >= max_threads) {
|
|
|
|
max_threads *= 2;
|
|
|
|
grow = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (grow) {
|
|
|
|
struct thread_map *tmp;
|
|
|
|
|
perf thread_map: Fix the sizeof() calculation for map entries
When we started adding extra stuff per array entry, growing the size of
those entries to more than sizeof(pid_t), we had to convert those sizeof
operations to the more robust sizeof(map->map[0]) idiom, that is future
proof, i.e. if/when we add more stuff to those entries, that expression
will produce the new per-entry size.
And besides that, we need to zero out those extra fields, that sometimes
may not get filled, like when we couldn't care less about the comms,
since we don't need those, but since we will try freeing it at
thread_map__delete(), we better fix it.
That is why a thread_map__realloc() was provided.
But that method wasn't used in thread_map__new_by_uid(), fix it.
Reported-by: Ingo Molnar <mingo@kernel.org>
Fixes: 792402fd5c0a ("perf thrad_map: Add comm string into array")
Fixes: 9d7e8c3a96e5 ("perf tools: Add thread_map__(alloc|realloc) helpers")
Cc: Adrian Hunter <adrian.hunter@intel.com>
Cc: Borislav Petkov <bp@suse.de>
Cc: David Ahern <dsahern@gmail.com>
Cc: Frederic Weisbecker <fweisbec@gmail.com>
Cc: Jiri Olsa <jolsa@redhat.com>
Cc: Namhyung Kim <namhyung@kernel.org>
Cc: Stephane Eranian <eranian@google.com>
Link: http://lkml.kernel.org/n/tip-6a0swlm6m8lnu3wpjv284hkb@git.kernel.org
Signed-off-by: Arnaldo Carvalho de Melo <acme@redhat.com>
2015-07-09 15:14:43 +00:00
|
|
|
tmp = thread_map__realloc(threads, max_threads);
|
2012-01-19 16:08:15 +00:00
|
|
|
if (tmp == NULL)
|
|
|
|
goto out_free_namelist;
|
|
|
|
|
|
|
|
threads = tmp;
|
|
|
|
}
|
|
|
|
|
2015-06-22 22:36:02 +00:00
|
|
|
for (i = 0; i < items; i++) {
|
|
|
|
thread_map__set_pid(threads, threads->nr + i,
|
|
|
|
atoi(namelist[i]->d_name));
|
|
|
|
}
|
2012-01-19 16:08:15 +00:00
|
|
|
|
|
|
|
for (i = 0; i < items; i++)
|
2013-12-27 19:55:14 +00:00
|
|
|
zfree(&namelist[i]);
|
2012-01-19 16:08:15 +00:00
|
|
|
free(namelist);
|
|
|
|
|
|
|
|
threads->nr += items;
|
|
|
|
}
|
|
|
|
|
|
|
|
out_closedir:
|
|
|
|
closedir(proc);
|
|
|
|
out:
|
|
|
|
return threads;
|
|
|
|
|
|
|
|
out_free_threads:
|
|
|
|
free(threads);
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
out_free_namelist:
|
|
|
|
for (i = 0; i < items; i++)
|
2013-12-27 19:55:14 +00:00
|
|
|
zfree(&namelist[i]);
|
2012-01-19 16:08:15 +00:00
|
|
|
free(namelist);
|
|
|
|
|
|
|
|
out_free_closedir:
|
2013-12-26 20:41:15 +00:00
|
|
|
zfree(&threads);
|
2012-01-19 16:08:15 +00:00
|
|
|
goto out_closedir;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct thread_map *thread_map__new(pid_t pid, pid_t tid, uid_t uid)
|
2011-01-18 17:15:24 +00:00
|
|
|
{
|
|
|
|
if (pid != -1)
|
|
|
|
return thread_map__new_by_pid(pid);
|
2012-01-19 16:08:15 +00:00
|
|
|
|
|
|
|
if (tid == -1 && uid != UINT_MAX)
|
|
|
|
return thread_map__new_by_uid(uid);
|
|
|
|
|
2011-01-18 17:15:24 +00:00
|
|
|
return thread_map__new_by_tid(tid);
|
|
|
|
}
|
|
|
|
|
2012-02-08 16:32:52 +00:00
|
|
|
static struct thread_map *thread_map__new_by_pid_str(const char *pid_str)
|
|
|
|
{
|
|
|
|
struct thread_map *threads = NULL, *nt;
|
|
|
|
char name[256];
|
|
|
|
int items, total_tasks = 0;
|
|
|
|
struct dirent **namelist = NULL;
|
|
|
|
int i, j = 0;
|
|
|
|
pid_t pid, prev_pid = INT_MAX;
|
|
|
|
char *end_ptr;
|
|
|
|
struct str_node *pos;
|
2015-07-20 15:13:34 +00:00
|
|
|
struct strlist_config slist_config = { .dont_dupstr = true, };
|
|
|
|
struct strlist *slist = strlist__new(pid_str, &slist_config);
|
2012-02-08 16:32:52 +00:00
|
|
|
|
|
|
|
if (!slist)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
strlist__for_each(pos, slist) {
|
|
|
|
pid = strtol(pos->s, &end_ptr, 10);
|
|
|
|
|
|
|
|
if (pid == INT_MIN || pid == INT_MAX ||
|
|
|
|
(*end_ptr != '\0' && *end_ptr != ','))
|
|
|
|
goto out_free_threads;
|
|
|
|
|
|
|
|
if (pid == prev_pid)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
sprintf(name, "/proc/%d/task", pid);
|
|
|
|
items = scandir(name, &namelist, filter, NULL);
|
|
|
|
if (items <= 0)
|
|
|
|
goto out_free_threads;
|
|
|
|
|
|
|
|
total_tasks += items;
|
2015-06-14 08:19:17 +00:00
|
|
|
nt = thread_map__realloc(threads, total_tasks);
|
2012-02-08 16:32:52 +00:00
|
|
|
if (nt == NULL)
|
2012-05-25 13:21:49 +00:00
|
|
|
goto out_free_namelist;
|
2012-02-08 16:32:52 +00:00
|
|
|
|
|
|
|
threads = nt;
|
|
|
|
|
2012-05-25 13:21:49 +00:00
|
|
|
for (i = 0; i < items; i++) {
|
2015-06-22 22:36:02 +00:00
|
|
|
thread_map__set_pid(threads, j++, atoi(namelist[i]->d_name));
|
2013-12-27 19:55:14 +00:00
|
|
|
zfree(&namelist[i]);
|
2012-05-25 13:21:49 +00:00
|
|
|
}
|
|
|
|
threads->nr = total_tasks;
|
2012-02-08 16:32:52 +00:00
|
|
|
free(namelist);
|
|
|
|
}
|
|
|
|
|
|
|
|
out:
|
|
|
|
strlist__delete(slist);
|
2015-06-22 22:36:05 +00:00
|
|
|
if (threads)
|
|
|
|
atomic_set(&threads->refcnt, 1);
|
2012-02-08 16:32:52 +00:00
|
|
|
return threads;
|
|
|
|
|
2012-05-25 13:21:49 +00:00
|
|
|
out_free_namelist:
|
|
|
|
for (i = 0; i < items; i++)
|
2013-12-27 19:55:14 +00:00
|
|
|
zfree(&namelist[i]);
|
2012-05-25 13:21:49 +00:00
|
|
|
free(namelist);
|
|
|
|
|
2012-02-08 16:32:52 +00:00
|
|
|
out_free_threads:
|
2013-12-26 20:41:15 +00:00
|
|
|
zfree(&threads);
|
2012-02-08 16:32:52 +00:00
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
2014-10-10 15:03:46 +00:00
|
|
|
struct thread_map *thread_map__new_dummy(void)
|
|
|
|
{
|
2015-06-14 08:19:17 +00:00
|
|
|
struct thread_map *threads = thread_map__alloc(1);
|
2014-10-10 15:03:46 +00:00
|
|
|
|
|
|
|
if (threads != NULL) {
|
2015-06-22 22:36:02 +00:00
|
|
|
thread_map__set_pid(threads, 0, -1);
|
|
|
|
threads->nr = 1;
|
2015-06-22 22:36:05 +00:00
|
|
|
atomic_set(&threads->refcnt, 1);
|
2014-10-10 15:03:46 +00:00
|
|
|
}
|
|
|
|
return threads;
|
|
|
|
}
|
|
|
|
|
2012-02-08 16:32:52 +00:00
|
|
|
static struct thread_map *thread_map__new_by_tid_str(const char *tid_str)
|
|
|
|
{
|
|
|
|
struct thread_map *threads = NULL, *nt;
|
|
|
|
int ntasks = 0;
|
|
|
|
pid_t tid, prev_tid = INT_MAX;
|
|
|
|
char *end_ptr;
|
|
|
|
struct str_node *pos;
|
2015-07-20 15:13:34 +00:00
|
|
|
struct strlist_config slist_config = { .dont_dupstr = true, };
|
2012-02-08 16:32:52 +00:00
|
|
|
struct strlist *slist;
|
|
|
|
|
|
|
|
/* perf-stat expects threads to be generated even if tid not given */
|
2014-10-10 15:03:46 +00:00
|
|
|
if (!tid_str)
|
|
|
|
return thread_map__new_dummy();
|
2012-02-08 16:32:52 +00:00
|
|
|
|
2015-07-20 15:13:34 +00:00
|
|
|
slist = strlist__new(tid_str, &slist_config);
|
2012-02-08 16:32:52 +00:00
|
|
|
if (!slist)
|
|
|
|
return NULL;
|
|
|
|
|
|
|
|
strlist__for_each(pos, slist) {
|
|
|
|
tid = strtol(pos->s, &end_ptr, 10);
|
|
|
|
|
|
|
|
if (tid == INT_MIN || tid == INT_MAX ||
|
|
|
|
(*end_ptr != '\0' && *end_ptr != ','))
|
|
|
|
goto out_free_threads;
|
|
|
|
|
|
|
|
if (tid == prev_tid)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
ntasks++;
|
2015-06-14 08:19:17 +00:00
|
|
|
nt = thread_map__realloc(threads, ntasks);
|
2012-02-08 16:32:52 +00:00
|
|
|
|
|
|
|
if (nt == NULL)
|
|
|
|
goto out_free_threads;
|
|
|
|
|
|
|
|
threads = nt;
|
2015-06-22 22:36:02 +00:00
|
|
|
thread_map__set_pid(threads, ntasks - 1, tid);
|
|
|
|
threads->nr = ntasks;
|
2012-02-08 16:32:52 +00:00
|
|
|
}
|
|
|
|
out:
|
2015-06-22 22:36:05 +00:00
|
|
|
if (threads)
|
|
|
|
atomic_set(&threads->refcnt, 1);
|
2012-02-08 16:32:52 +00:00
|
|
|
return threads;
|
|
|
|
|
|
|
|
out_free_threads:
|
2013-12-26 20:41:15 +00:00
|
|
|
zfree(&threads);
|
2015-12-10 03:00:58 +00:00
|
|
|
strlist__delete(slist);
|
2012-02-08 16:32:52 +00:00
|
|
|
goto out;
|
|
|
|
}
|
|
|
|
|
|
|
|
struct thread_map *thread_map__new_str(const char *pid, const char *tid,
|
|
|
|
uid_t uid)
|
|
|
|
{
|
|
|
|
if (pid)
|
|
|
|
return thread_map__new_by_pid_str(pid);
|
|
|
|
|
|
|
|
if (!tid && uid != UINT_MAX)
|
|
|
|
return thread_map__new_by_uid(uid);
|
|
|
|
|
|
|
|
return thread_map__new_by_tid_str(tid);
|
|
|
|
}
|
|
|
|
|
2015-06-22 22:36:05 +00:00
|
|
|
static void thread_map__delete(struct thread_map *threads)
|
2011-01-18 17:15:24 +00:00
|
|
|
{
|
2015-06-22 22:36:05 +00:00
|
|
|
if (threads) {
|
2015-06-26 09:29:07 +00:00
|
|
|
int i;
|
|
|
|
|
2015-06-22 22:36:05 +00:00
|
|
|
WARN_ONCE(atomic_read(&threads->refcnt) != 0,
|
|
|
|
"thread map refcnt unbalanced\n");
|
2015-06-26 09:29:07 +00:00
|
|
|
for (i = 0; i < threads->nr; i++)
|
|
|
|
free(thread_map__comm(threads, i));
|
2015-06-22 22:36:05 +00:00
|
|
|
free(threads);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct thread_map *thread_map__get(struct thread_map *map)
|
|
|
|
{
|
|
|
|
if (map)
|
|
|
|
atomic_inc(&map->refcnt);
|
|
|
|
return map;
|
|
|
|
}
|
|
|
|
|
|
|
|
void thread_map__put(struct thread_map *map)
|
|
|
|
{
|
|
|
|
if (map && atomic_dec_and_test(&map->refcnt))
|
|
|
|
thread_map__delete(map);
|
2011-01-18 17:15:24 +00:00
|
|
|
}
|
2012-01-19 16:07:23 +00:00
|
|
|
|
|
|
|
size_t thread_map__fprintf(struct thread_map *threads, FILE *fp)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
size_t printed = fprintf(fp, "%d thread%s: ",
|
|
|
|
threads->nr, threads->nr > 1 ? "s" : "");
|
|
|
|
for (i = 0; i < threads->nr; ++i)
|
2015-06-22 22:36:02 +00:00
|
|
|
printed += fprintf(fp, "%s%d", i ? ", " : "", thread_map__pid(threads, i));
|
2012-01-19 16:07:23 +00:00
|
|
|
|
|
|
|
return printed + fprintf(fp, "\n");
|
|
|
|
}
|
2015-06-26 09:29:07 +00:00
|
|
|
|
|
|
|
static int get_comm(char **comm, pid_t pid)
|
|
|
|
{
|
|
|
|
char *path;
|
|
|
|
size_t size;
|
|
|
|
int err;
|
|
|
|
|
|
|
|
if (asprintf(&path, "%s/%d/comm", procfs__mountpoint(), pid) == -1)
|
|
|
|
return -ENOMEM;
|
|
|
|
|
|
|
|
err = filename__read_str(path, comm, &size);
|
|
|
|
if (!err) {
|
|
|
|
/*
|
|
|
|
* We're reading 16 bytes, while filename__read_str
|
|
|
|
* allocates data per BUFSIZ bytes, so we can safely
|
|
|
|
* mark the end of the string.
|
|
|
|
*/
|
|
|
|
(*comm)[size] = 0;
|
|
|
|
rtrim(*comm);
|
|
|
|
}
|
|
|
|
|
|
|
|
free(path);
|
|
|
|
return err;
|
|
|
|
}
|
|
|
|
|
|
|
|
static void comm_init(struct thread_map *map, int i)
|
|
|
|
{
|
|
|
|
pid_t pid = thread_map__pid(map, i);
|
|
|
|
char *comm = NULL;
|
|
|
|
|
|
|
|
/* dummy pid comm initialization */
|
|
|
|
if (pid == -1) {
|
|
|
|
map->map[i].comm = strdup("dummy");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* The comm name is like extra bonus ;-),
|
|
|
|
* so just warn if we fail for any reason.
|
|
|
|
*/
|
|
|
|
if (get_comm(&comm, pid))
|
|
|
|
pr_warning("Couldn't resolve comm name for pid %d\n", pid);
|
|
|
|
|
|
|
|
map->map[i].comm = comm;
|
|
|
|
}
|
|
|
|
|
|
|
|
void thread_map__read_comms(struct thread_map *threads)
|
|
|
|
{
|
|
|
|
int i;
|
|
|
|
|
|
|
|
for (i = 0; i < threads->nr; ++i)
|
|
|
|
comm_init(threads, i);
|
|
|
|
}
|
2015-10-25 14:51:21 +00:00
|
|
|
|
|
|
|
static void thread_map__copy_event(struct thread_map *threads,
|
|
|
|
struct thread_map_event *event)
|
|
|
|
{
|
|
|
|
unsigned i;
|
|
|
|
|
|
|
|
threads->nr = (int) event->nr;
|
|
|
|
|
|
|
|
for (i = 0; i < event->nr; i++) {
|
|
|
|
thread_map__set_pid(threads, i, (pid_t) event->entries[i].pid);
|
|
|
|
threads->map[i].comm = strndup(event->entries[i].comm, 16);
|
|
|
|
}
|
|
|
|
|
|
|
|
atomic_set(&threads->refcnt, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
struct thread_map *thread_map__new_event(struct thread_map_event *event)
|
|
|
|
{
|
|
|
|
struct thread_map *threads;
|
|
|
|
|
|
|
|
threads = thread_map__alloc(event->nr);
|
|
|
|
if (threads)
|
|
|
|
thread_map__copy_event(threads, event);
|
|
|
|
|
|
|
|
return threads;
|
|
|
|
}
|