Commit 61bf86ad authored by Ingo Molnar's avatar Ingo Molnar

Merge tag 'perf-core-for-mingo' of...

Merge tag 'perf-core-for-mingo' of git://git.kernel.org/pub/scm/linux/kernel/git/acme/linux into perf/core

Pull perf/core improvements and fixes from Arnaldo Carvalho de Melo:

 * 'perf trace' arg formatting improvements to allow masking arguments
   in syscalls such as futex and open, where the some arguments are
   ignored and thus should not be printed depending on other args.

 * Beautify futex open, openat, open_by_handle_at, lseek and futex syscalls.

 * Add dummy software event to use when wanting just to keep receiving
   PERF_RECORD_{MMAP,COMM,etc}, add test for it, from Adrian Hunter.

 * Fix symbol offset computation for some dsos in 'perf script', from David Ahern.

 * Skip unsupported hardware events in 'perf list', from Namhyung Kim.
Signed-off-by: default avatarArnaldo Carvalho de Melo <acme@redhat.com>
Signed-off-by: default avatarIngo Molnar <mingo@kernel.org>
parents 7bfb7e6b 31cd3855
......@@ -109,6 +109,7 @@ enum perf_sw_ids {
PERF_COUNT_SW_PAGE_FAULTS_MAJ = 6,
PERF_COUNT_SW_ALIGNMENT_FAULTS = 7,
PERF_COUNT_SW_EMULATION_FAULTS = 8,
PERF_COUNT_SW_DUMMY = 9,
PERF_COUNT_SW_MAX, /* non-ABI */
};
......
......@@ -465,6 +465,7 @@ endif # NO_LIBELF
ifndef NO_LIBUNWIND
LIB_OBJS += $(OUTPUT)util/unwind.o
endif
LIB_OBJS += $(OUTPUT)tests/keep-tracking.o
ifndef NO_LIBAUDIT
BUILTIN_OBJS += $(OUTPUT)builtin-trace.o
......
......@@ -14,15 +14,49 @@
#include <libaudit.h>
#include <stdlib.h>
#include <sys/mman.h>
#include <linux/futex.h>
static size_t syscall_arg__scnprintf_hex(char *bf, size_t size, unsigned long arg)
static size_t syscall_arg__scnprintf_hex(char *bf, size_t size,
unsigned long arg,
u8 arg_idx __maybe_unused,
u8 *arg_mask __maybe_unused)
{
return scnprintf(bf, size, "%#lx", arg);
}
#define SCA_HEX syscall_arg__scnprintf_hex
static size_t syscall_arg__scnprintf_mmap_prot(char *bf, size_t size, unsigned long arg)
static size_t syscall_arg__scnprintf_whence(char *bf, size_t size,
unsigned long arg,
u8 arg_idx __maybe_unused,
u8 *arg_mask __maybe_unused)
{
int whence = arg;
switch (whence) {
#define P_WHENCE(n) case SEEK_##n: return scnprintf(bf, size, #n)
P_WHENCE(SET);
P_WHENCE(CUR);
P_WHENCE(END);
#ifdef SEEK_DATA
P_WHENCE(DATA);
#endif
#ifdef SEEK_HOLE
P_WHENCE(HOLE);
#endif
#undef P_WHENCE
default: break;
}
return scnprintf(bf, size, "%#x", whence);
}
#define SCA_WHENCE syscall_arg__scnprintf_whence
static size_t syscall_arg__scnprintf_mmap_prot(char *bf, size_t size,
unsigned long arg,
u8 arg_idx __maybe_unused,
u8 *arg_mask __maybe_unused)
{
int printed = 0, prot = arg;
......@@ -52,7 +86,9 @@ static size_t syscall_arg__scnprintf_mmap_prot(char *bf, size_t size, unsigned l
#define SCA_MMAP_PROT syscall_arg__scnprintf_mmap_prot
static size_t syscall_arg__scnprintf_mmap_flags(char *bf, size_t size, unsigned long arg)
static size_t syscall_arg__scnprintf_mmap_flags(char *bf, size_t size,
unsigned long arg, u8 arg_idx __maybe_unused,
u8 *arg_mask __maybe_unused)
{
int printed = 0, flags = arg;
......@@ -92,7 +128,9 @@ static size_t syscall_arg__scnprintf_mmap_flags(char *bf, size_t size, unsigned
#define SCA_MMAP_FLAGS syscall_arg__scnprintf_mmap_flags
static size_t syscall_arg__scnprintf_madvise_behavior(char *bf, size_t size, unsigned long arg)
static size_t syscall_arg__scnprintf_madvise_behavior(char *bf, size_t size,
unsigned long arg, u8 arg_idx __maybe_unused,
u8 *arg_mask __maybe_unused)
{
int behavior = arg;
......@@ -133,10 +171,111 @@ static size_t syscall_arg__scnprintf_madvise_behavior(char *bf, size_t size, uns
#define SCA_MADV_BHV syscall_arg__scnprintf_madvise_behavior
static size_t syscall_arg__scnprintf_futex_op(char *bf, size_t size, unsigned long arg,
u8 arg_idx __maybe_unused, u8 *arg_mask)
{
enum syscall_futex_args {
SCF_UADDR = (1 << 0),
SCF_OP = (1 << 1),
SCF_VAL = (1 << 2),
SCF_TIMEOUT = (1 << 3),
SCF_UADDR2 = (1 << 4),
SCF_VAL3 = (1 << 5),
};
int op = arg;
int cmd = op & FUTEX_CMD_MASK;
size_t printed = 0;
switch (cmd) {
#define P_FUTEX_OP(n) case FUTEX_##n: printed = scnprintf(bf, size, #n);
P_FUTEX_OP(WAIT); *arg_mask |= SCF_VAL3|SCF_UADDR2; break;
P_FUTEX_OP(WAKE); *arg_mask |= SCF_VAL3|SCF_UADDR2|SCF_TIMEOUT; break;
P_FUTEX_OP(FD); *arg_mask |= SCF_VAL3|SCF_UADDR2|SCF_TIMEOUT; break;
P_FUTEX_OP(REQUEUE); *arg_mask |= SCF_VAL3|SCF_TIMEOUT; break;
P_FUTEX_OP(CMP_REQUEUE); *arg_mask |= SCF_TIMEOUT; break;
P_FUTEX_OP(CMP_REQUEUE_PI); *arg_mask |= SCF_TIMEOUT; break;
P_FUTEX_OP(WAKE_OP); break;
P_FUTEX_OP(LOCK_PI); *arg_mask |= SCF_VAL3|SCF_UADDR2|SCF_TIMEOUT; break;
P_FUTEX_OP(UNLOCK_PI); *arg_mask |= SCF_VAL3|SCF_UADDR2|SCF_TIMEOUT; break;
P_FUTEX_OP(TRYLOCK_PI); *arg_mask |= SCF_VAL3|SCF_UADDR2; break;
P_FUTEX_OP(WAIT_BITSET); *arg_mask |= SCF_UADDR2; break;
P_FUTEX_OP(WAKE_BITSET); *arg_mask |= SCF_UADDR2; break;
P_FUTEX_OP(WAIT_REQUEUE_PI); break;
default: printed = scnprintf(bf, size, "%#x", cmd); break;
}
if (op & FUTEX_PRIVATE_FLAG)
printed += scnprintf(bf + printed, size - printed, "|PRIV");
if (op & FUTEX_CLOCK_REALTIME)
printed += scnprintf(bf + printed, size - printed, "|CLKRT");
return printed;
}
#define SCA_FUTEX_OP syscall_arg__scnprintf_futex_op
static size_t syscall_arg__scnprintf_open_flags(char *bf, size_t size,
unsigned long arg,
u8 arg_idx, u8 *arg_mask)
{
int printed = 0, flags = arg;
if (!(flags & O_CREAT))
*arg_mask |= 1 << (arg_idx + 1); /* Mask the mode parm */
if (flags == 0)
return scnprintf(bf, size, "RDONLY");
#define P_FLAG(n) \
if (flags & O_##n) { \
printed += scnprintf(bf + printed, size - printed, "%s%s", printed ? "|" : "", #n); \
flags &= ~O_##n; \
}
P_FLAG(APPEND);
P_FLAG(ASYNC);
P_FLAG(CLOEXEC);
P_FLAG(CREAT);
P_FLAG(DIRECT);
P_FLAG(DIRECTORY);
P_FLAG(EXCL);
P_FLAG(LARGEFILE);
P_FLAG(NOATIME);
P_FLAG(NOCTTY);
#ifdef O_NONBLOCK
P_FLAG(NONBLOCK);
#elif O_NDELAY
P_FLAG(NDELAY);
#endif
#ifdef O_PATH
P_FLAG(PATH);
#endif
P_FLAG(RDWR);
#ifdef O_DSYNC
if ((flags & O_SYNC) == O_SYNC)
printed += scnprintf(bf + printed, size - printed, "%s%s", printed ? "|" : "", "SYNC");
else {
P_FLAG(DSYNC);
}
#else
P_FLAG(SYNC);
#endif
P_FLAG(TRUNC);
P_FLAG(WRONLY);
#undef P_FLAG
if (flags)
printed += scnprintf(bf + printed, size - printed, "%s%#x", printed ? "|" : "", flags);
return printed;
}
#define SCA_OPEN_FLAGS syscall_arg__scnprintf_open_flags
static struct syscall_fmt {
const char *name;
const char *alias;
size_t (*arg_scnprintf[6])(char *bf, size_t size, unsigned long arg);
size_t (*arg_scnprintf[6])(char *bf, size_t size, unsigned long arg, u8 arg_idx, u8 *arg_mask);
bool errmsg;
bool timeout;
bool hexret;
......@@ -149,9 +288,12 @@ static struct syscall_fmt {
{ .name = "connect", .errmsg = true, },
{ .name = "fstat", .errmsg = true, .alias = "newfstat", },
{ .name = "fstatat", .errmsg = true, .alias = "newfstatat", },
{ .name = "futex", .errmsg = true, },
{ .name = "futex", .errmsg = true,
.arg_scnprintf = { [1] = SCA_FUTEX_OP, /* op */ }, },
{ .name = "ioctl", .errmsg = true,
.arg_scnprintf = { [2] = SCA_HEX, /* arg */ }, },
{ .name = "lseek", .errmsg = true,
.arg_scnprintf = { [2] = SCA_WHENCE, /* whence */ }, },
{ .name = "lstat", .errmsg = true, .alias = "newlstat", },
{ .name = "madvise", .errmsg = true,
.arg_scnprintf = { [0] = SCA_HEX, /* start */
......@@ -168,7 +310,12 @@ static struct syscall_fmt {
[4] = SCA_HEX, /* new_addr */ }, },
{ .name = "munmap", .errmsg = true,
.arg_scnprintf = { [0] = SCA_HEX, /* addr */ }, },
{ .name = "open", .errmsg = true, },
{ .name = "open", .errmsg = true,
.arg_scnprintf = { [1] = SCA_OPEN_FLAGS, /* flags */ }, },
{ .name = "open_by_handle_at", .errmsg = true,
.arg_scnprintf = { [2] = SCA_OPEN_FLAGS, /* flags */ }, },
{ .name = "openat", .errmsg = true,
.arg_scnprintf = { [2] = SCA_OPEN_FLAGS, /* flags */ }, },
{ .name = "poll", .errmsg = true, .timeout = true, },
{ .name = "ppoll", .errmsg = true, .timeout = true, },
{ .name = "pread", .errmsg = true, .alias = "pread64", },
......@@ -198,7 +345,8 @@ struct syscall {
const char *name;
bool filtered;
struct syscall_fmt *fmt;
size_t (**arg_scnprintf)(char *bf, size_t size, unsigned long arg);
size_t (**arg_scnprintf)(char *bf, size_t size,
unsigned long arg, u8 arg_idx, u8 *args_mask);
};
static size_t fprintf_duration(unsigned long t, FILE *fp)
......@@ -443,17 +591,23 @@ static size_t syscall__scnprintf_args(struct syscall *sc, char *bf, size_t size,
if (sc->tp_format != NULL) {
struct format_field *field;
u8 mask = 0, bit = 1;
for (field = sc->tp_format->format.fields->next; field;
field = field->next, ++i, bit <<= 1) {
if (mask & bit)
continue;
for (field = sc->tp_format->format.fields->next; field; field = field->next) {
printed += scnprintf(bf + printed, size - printed,
"%s%s: ", printed ? ", " : "", field->name);
if (sc->arg_scnprintf && sc->arg_scnprintf[i])
printed += sc->arg_scnprintf[i](bf + printed, size - printed, args[i]);
else
if (sc->arg_scnprintf && sc->arg_scnprintf[i]) {
printed += sc->arg_scnprintf[i](bf + printed, size - printed,
args[i], i, &mask);
} else {
printed += scnprintf(bf + printed, size - printed,
"%ld", args[i]);
++i;
}
}
} else {
while (i < 6) {
......
......@@ -107,6 +107,10 @@ static struct test {
.desc = "Test sample parsing",
.func = test__sample_parsing,
},
{
.desc = "Test using a dummy software event to keep tracking",
.func = test__keep_tracking,
},
{
.func = NULL,
},
......
#include <sys/types.h>
#include <unistd.h>
#include <sys/prctl.h>
#include "parse-events.h"
#include "evlist.h"
#include "evsel.h"
#include "thread_map.h"
#include "cpumap.h"
#include "tests.h"
#define CHECK__(x) { \
while ((x) < 0) { \
pr_debug(#x " failed!\n"); \
goto out_err; \
} \
}
#define CHECK_NOT_NULL__(x) { \
while ((x) == NULL) { \
pr_debug(#x " failed!\n"); \
goto out_err; \
} \
}
static int find_comm(struct perf_evlist *evlist, const char *comm)
{
union perf_event *event;
int i, found;
found = 0;
for (i = 0; i < evlist->nr_mmaps; i++) {
while ((event = perf_evlist__mmap_read(evlist, i)) != NULL) {
if (event->header.type == PERF_RECORD_COMM &&
(pid_t)event->comm.pid == getpid() &&
(pid_t)event->comm.tid == getpid() &&
strcmp(event->comm.comm, comm) == 0)
found += 1;
}
}
return found;
}
/**
* test__keep_tracking - test using a dummy software event to keep tracking.
*
* This function implements a test that checks that tracking events continue
* when an event is disabled but a dummy software event is not disabled. If the
* test passes %0 is returned, otherwise %-1 is returned.
*/
int test__keep_tracking(void)
{
struct perf_record_opts opts = {
.mmap_pages = UINT_MAX,
.user_freq = UINT_MAX,
.user_interval = ULLONG_MAX,
.freq = 4000,
.target = {
.uses_mmap = true,
},
};
struct thread_map *threads = NULL;
struct cpu_map *cpus = NULL;
struct perf_evlist *evlist = NULL;
struct perf_evsel *evsel = NULL;
int found, err = -1;
const char *comm;
threads = thread_map__new(-1, getpid(), UINT_MAX);
CHECK_NOT_NULL__(threads);
cpus = cpu_map__new(NULL);
CHECK_NOT_NULL__(cpus);
evlist = perf_evlist__new();
CHECK_NOT_NULL__(evlist);
perf_evlist__set_maps(evlist, cpus, threads);
CHECK__(parse_events(evlist, "dummy:u"));
CHECK__(parse_events(evlist, "cycles:u"));
perf_evlist__config(evlist, &opts);
evsel = perf_evlist__first(evlist);
evsel->attr.comm = 1;
evsel->attr.disabled = 1;
evsel->attr.enable_on_exec = 0;
if (perf_evlist__open(evlist) < 0) {
fprintf(stderr, " (not supported)");
err = 0;
goto out_err;
}
CHECK__(perf_evlist__mmap(evlist, UINT_MAX, false));
/*
* First, test that a 'comm' event can be found when the event is
* enabled.
*/
perf_evlist__enable(evlist);
comm = "Test COMM 1";
CHECK__(prctl(PR_SET_NAME, (unsigned long)comm, 0, 0, 0));
perf_evlist__disable(evlist);
found = find_comm(evlist, comm);
if (found != 1) {
pr_debug("First time, failed to find tracking event.\n");
goto out_err;
}
/*
* Secondly, test that a 'comm' event can be found when the event is
* disabled with the dummy event still enabled.
*/
perf_evlist__enable(evlist);
evsel = perf_evlist__last(evlist);
CHECK__(perf_evlist__disable_event(evlist, evsel));
comm = "Test COMM 2";
CHECK__(prctl(PR_SET_NAME, (unsigned long)comm, 0, 0, 0));
perf_evlist__disable(evlist);
found = find_comm(evlist, comm);
if (found != 1) {
pr_debug("Seconf time, failed to find tracking event.\n");
goto out_err;
}
err = 0;
out_err:
if (evlist) {
perf_evlist__disable(evlist);
perf_evlist__munmap(evlist);
perf_evlist__close(evlist);
perf_evlist__delete(evlist);
}
if (cpus)
cpu_map__delete(cpus);
if (threads)
thread_map__delete(threads);
return err;
}
......@@ -38,5 +38,6 @@ int test__sw_clock_freq(void);
int test__perf_time_to_tsc(void);
int test__code_reading(void);
int test__sample_parsing(void);
int test__keep_tracking(void);
#endif /* TESTS_H */
......@@ -246,7 +246,7 @@ void perf_evlist__disable(struct perf_evlist *evlist)
for (cpu = 0; cpu < nr_cpus; cpu++) {
list_for_each_entry(pos, &evlist->entries, node) {
if (!perf_evsel__is_group_leader(pos))
if (!perf_evsel__is_group_leader(pos) || !pos->fd)
continue;
for (thread = 0; thread < nr_threads; thread++)
ioctl(FD(pos, cpu, thread),
......@@ -264,7 +264,7 @@ void perf_evlist__enable(struct perf_evlist *evlist)
for (cpu = 0; cpu < nr_cpus; cpu++) {
list_for_each_entry(pos, &evlist->entries, node) {
if (!perf_evsel__is_group_leader(pos))
if (!perf_evsel__is_group_leader(pos) || !pos->fd)
continue;
for (thread = 0; thread < nr_threads; thread++)
ioctl(FD(pos, cpu, thread),
......@@ -273,6 +273,44 @@ void perf_evlist__enable(struct perf_evlist *evlist)
}
}
int perf_evlist__disable_event(struct perf_evlist *evlist,
struct perf_evsel *evsel)
{
int cpu, thread, err;
if (!evsel->fd)
return 0;
for (cpu = 0; cpu < evlist->cpus->nr; cpu++) {
for (thread = 0; thread < evlist->threads->nr; thread++) {
err = ioctl(FD(evsel, cpu, thread),
PERF_EVENT_IOC_DISABLE, 0);
if (err)
return err;
}
}
return 0;
}
int perf_evlist__enable_event(struct perf_evlist *evlist,
struct perf_evsel *evsel)
{
int cpu, thread, err;
if (!evsel->fd)
return -EINVAL;
for (cpu = 0; cpu < evlist->cpus->nr; cpu++) {
for (thread = 0; thread < evlist->threads->nr; thread++) {
err = ioctl(FD(evsel, cpu, thread),
PERF_EVENT_IOC_ENABLE, 0);
if (err)
return err;
}
}
return 0;
}
static int perf_evlist__alloc_pollfd(struct perf_evlist *evlist)
{
int nr_cpus = cpu_map__nr(evlist->cpus);
......
......@@ -110,6 +110,11 @@ void perf_evlist__munmap(struct perf_evlist *evlist);
void perf_evlist__disable(struct perf_evlist *evlist);
void perf_evlist__enable(struct perf_evlist *evlist);
int perf_evlist__disable_event(struct perf_evlist *evlist,
struct perf_evsel *evsel);
int perf_evlist__enable_event(struct perf_evlist *evlist,
struct perf_evsel *evsel);
void perf_evlist__set_selected(struct perf_evlist *evlist,
struct perf_evsel *evsel);
......
......@@ -323,6 +323,7 @@ const char *perf_evsel__sw_names[PERF_COUNT_SW_MAX] = {
"major-faults",
"alignment-faults",
"emulation-faults",
"dummy",
};
static const char *__perf_evsel__sw_name(u64 config)
......
......@@ -15,6 +15,7 @@
#define YY_EXTRA_TYPE int
#include "parse-events-flex.h"
#include "pmu.h"
#include "thread_map.h"
#define MAX_NAME_LEN 100
......@@ -108,6 +109,10 @@ static struct event_symbol event_symbols_sw[PERF_COUNT_SW_MAX] = {
.symbol = "emulation-faults",
.alias = "",
},
[PERF_COUNT_SW_DUMMY] = {
.symbol = "dummy",
.alias = "",
},
};
#define __PERF_EVENT_FIELD(config, name) \
......@@ -1072,6 +1077,33 @@ int is_valid_tracepoint(const char *event_string)
return 0;
}
static bool is_event_supported(u8 type, unsigned config)
{
bool ret = true;
struct perf_evsel *evsel;
struct perf_event_attr attr = {
.type = type,
.config = config,
.disabled = 1,
.exclude_kernel = 1,
};
struct {
struct thread_map map;
int threads[1];
} tmap = {
.map.nr = 1,
.threads = { 0 },
};
evsel = perf_evsel__new(&attr, 0);
if (evsel) {
ret = perf_evsel__open(evsel, NULL, &tmap.map) >= 0;
perf_evsel__delete(evsel);
}
return ret;
}
static void __print_events_type(u8 type, struct event_symbol *syms,
unsigned max)
{
......@@ -1079,14 +1111,16 @@ static void __print_events_type(u8 type, struct event_symbol *syms,
unsigned i;
for (i = 0; i < max ; i++, syms++) {
if (!is_event_supported(type, i))
continue;
if (strlen(syms->alias))
snprintf(name, sizeof(name), "%s OR %s",
syms->symbol, syms->alias);
else
snprintf(name, sizeof(name), "%s", syms->symbol);
printf(" %-50s [%s]\n", name,
event_type_descriptors[type]);
printf(" %-50s [%s]\n", name, event_type_descriptors[type]);
}
}
......@@ -1115,6 +1149,10 @@ int print_hwcache_events(const char *event_glob, bool name_only)
if (event_glob != NULL && !strglobmatch(name, event_glob))
continue;
if (!is_event_supported(PERF_TYPE_HW_CACHE,
type | (op << 8) | (i << 16)))
continue;
if (name_only)
printf("%s ", name);
else
......@@ -1144,6 +1182,9 @@ static void print_symbol_events(const char *event_glob, unsigned type,
(syms->alias && strglobmatch(syms->alias, event_glob))))
continue;
if (!is_event_supported(type, i))
continue;
if (name_only) {
printf("%s ", syms->symbol);
continue;
......
......@@ -145,6 +145,7 @@ context-switches|cs { return sym(yyscanner, PERF_TYPE_SOFTWARE, PERF_COUNT_SW
cpu-migrations|migrations { return sym(yyscanner, PERF_TYPE_SOFTWARE, PERF_COUNT_SW_CPU_MIGRATIONS); }
alignment-faults { return sym(yyscanner, PERF_TYPE_SOFTWARE, PERF_COUNT_SW_ALIGNMENT_FAULTS); }
emulation-faults { return sym(yyscanner, PERF_TYPE_SOFTWARE, PERF_COUNT_SW_EMULATION_FAULTS); }
dummy { return sym(yyscanner, PERF_TYPE_SOFTWARE, PERF_COUNT_SW_DUMMY); }
L1-dcache|l1-d|l1d|L1-data |
L1-icache|l1-i|l1i|L1-instruction |
......
......@@ -987,6 +987,7 @@ static struct {
{ "COUNT_SW_PAGE_FAULTS_MAJ", PERF_COUNT_SW_PAGE_FAULTS_MAJ },
{ "COUNT_SW_ALIGNMENT_FAULTS", PERF_COUNT_SW_ALIGNMENT_FAULTS },
{ "COUNT_SW_EMULATION_FAULTS", PERF_COUNT_SW_EMULATION_FAULTS },
{ "COUNT_SW_DUMMY", PERF_COUNT_SW_DUMMY },
{ "SAMPLE_IP", PERF_SAMPLE_IP },
{ "SAMPLE_TID", PERF_SAMPLE_TID },
......
......@@ -1513,6 +1513,7 @@ void perf_evsel__print_ip(struct perf_evsel *evsel, union perf_event *event,
printf(" ");
if (print_symoffset) {
al.addr = node->ip;
al.map = node->map;
symbol__fprintf_symname_offs(node->sym, &al, stdout);
} else
symbol__fprintf_symname(node->sym, stdout);
......
......@@ -259,7 +259,10 @@ size_t symbol__fprintf_symname_offs(const struct symbol *sym,
if (sym && sym->name) {
length = fprintf(fp, "%s", sym->name);
if (al) {
offset = al->addr - sym->start;
if (al->addr < sym->end)
offset = al->addr - sym->start;
else
offset = al->addr - al->map->start - sym->start;
length += fprintf(fp, "+0x%lx", offset);
}
return length;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment