From: Steven Rostedt The current print format that is exported to userspace is simply a copy of the printk format used to output the data. It would take a full C parser to parse it. But as more tools are made to read the binary data from ftrace, the larger the need for a nice parsing format to facilitate tools in reading the binary buffer. For example we currently have: irq_handler_entry: print fmt: "irq=%d handler=%s", REC->irq, (char *)((void *)REC + REC->__data_loc _name) softirq_entry: print fmt: "softirq=%d action=%s", REC->vec, ({ static const struct trace_print_ flags symbols[] = { { HI_SOFTIRQ, "HI" }, { TIMER_SOFTIRQ, "TIMER" }, { NET_TX_S OFTIRQ, "NET_TX" }, { NET_RX_SOFTIRQ, "NET_RX" }, { BLOCK_SOFTIRQ, "BLOCK" }, { TASKLET_SOFTIRQ, "TASKLET" }, { SCHED_SOFTIRQ, "SCHED" }, { HRTIMER_SOFTIRQ, "HR TIMER" }, { RCU_SOFTIRQ, "RCU" }, { -1, ((void *)0) }}; ftrace_print_symbols_seq (p, REC->vec, symbols); }) kmalloc: print fmt: "call_site=%lx ptr=%p bytes_req=%zu bytes_alloc=%zu gfp_flags=%s", RE C->call_site, REC->ptr, REC->bytes_req, REC->bytes_alloc, (REC->gfp_flags) ? ({ static const struct trace_print_flags flags[] = { {(unsigned long)(((gfp_t)0x10u ) | ((gfp_t)0x40u) | ((gfp_t)0x80u) | ((gfp_t)0x20000u) | ((gfp_t)0x02u) | ((gfp _t)0x100000u)), "GFP_HIGHUSER_MOVABLE"}, {(unsigned long)(((gfp_t)0x10u) | ((gfp _t)0x40u) | ((gfp_t)0x80u) | ((gfp_t)0x20000u) | ((gfp_t)0x02u)), "GFP_HIGHUSER" }, {(unsigned long)(((gfp_t)0x10u) | ((gfp_t)0x40u) | ((gfp_t)0x80u) | ((gfp_t)0 x20000u)), "GFP_USER"}, {(unsigned long)(((gfp_t)0x10u) | ((gfp_t)0x40u) | ((gfp _t)0x80u) | ((gfp_t)0x80000u)), "GFP_TEMPORARY"}, {(unsigned long)(((gfp_t)0x10u ) | ((gfp_t)0x40u) | ((gfp_t)0x80u)), "GFP_KERNEL"}, {(unsigned long)(((gfp_t)0x 10u) | ((gfp_t)0x40u)), "GFP_NOFS"}, {(unsigned long)(((gfp_t)0x20u)), "GFP_ATOM IC"}, {(unsigned long)(((gfp_t)0x10u)), "GFP_NOIO"}, {(unsigned long)((gfp_t)0x2 0u), "GFP_HIGH"}, {(unsigned long)((gfp_t)0x10u), "GFP_WAIT"}, {(unsigned long)( (gfp_t)0x40u), "GFP_IO"}, {(unsigned long)((gfp_t)0x100u), "GFP_COLD"}, {(unsign ed long)((gfp_t)0x200u), "GFP_NOWARN"}, {(unsigned long)((gfp_t)0x400u), "GFP_RE PEAT"}, {(unsigned long)((gfp_t)0x800u), "GFP_NOFAIL"}, {(unsigned long)((gfp_t) 0x1000u), "GFP_NORETRY"}, {(unsigned long)((gfp_t)0x4000u), "GFP_COMP"}, {(unsig ned long)((gfp_t)0x8000u), "GFP_ZERO"}, {(unsigned long)((gfp_t)0x10000u), "GFP_ NOMEMALLOC"}, {(unsigned long)((gfp_t)0x20000u), "GFP_HARDWALL"}, {(unsigned lon g)((gfp_t)0x40000u), "GFP_THISNODE"}, {(unsigned long)((gfp_t)0x80000u), "GFP_RE CLAIMABLE"}, {(unsigned long)((gfp_t)0x100000u), "GFP_MOVABLE"}, { -1, ((void *) 0) }}; ftrace_print_flags_seq(p, "|", REC->gfp_flags, flags); }) : "GFP_NOWAIT" The language that is added by this patch is of the following: * FMT := constant string FMT | COMMAND FMT | empty * COMMAND := | | | * * TYPE := int | hex | ptr | string | strarray * FIELD := defined by the event structure * MASKS := MASK=NAME,MASKS | MASK=NAME * MASK := the bit mask to match * DELIM := delimiter to separate the fields. None and ':' are both allowed * SYMBOLS := SYM=NAME,SYMBOLS | SYM=NAME * SYM := the symbol value to test against * TRUE := print when field is non zero * FALSE := print when field is zero or NULL * NAME := the name to write when a match is found * * A '\<' would print '<' The above examples would then look like: irq_handler_entry: format: irq= handler= softirq_entry: format: softirq= action= ptr= bytes_req= bytes_alloc= gfp_flags= --- include/linux/ftrace_event.h | 10 + include/trace/ftrace.h | 22 ++- kernel/trace/Makefile | 1 + kernel/trace/trace_read_binary.c | 674 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 705 insertions(+), 2 deletions(-) create mode 100644 kernel/trace/trace_read_binary.c diff --git a/include/linux/ftrace_event.h b/include/linux/ftrace_event.h index 5c093ff..f1b59d3 100644 --- a/include/linux/ftrace_event.h +++ b/include/linux/ftrace_event.h @@ -119,6 +119,9 @@ struct ftrace_event_call { void *filter; void *mod; + struct list_head print_info; + const char *print_text; + #ifdef CONFIG_EVENT_PROFILE atomic_t profile_count; int (*profile_enable)(struct ftrace_event_call *); @@ -136,6 +139,13 @@ extern int filter_current_check_discard(struct ftrace_event_call *call, void *rec, struct ring_buffer_event *event); +extern char *ftrace_read_binary(struct trace_seq *p, + struct ftrace_event_call *event, + struct trace_entry *entry); +extern int ftrace_initialize_print(struct ftrace_event_call *event, + const char *fmt, ...) + __attribute__ ((format (printf, 2, 3))); + extern int trace_define_field(struct ftrace_event_call *call, char *type, char *name, int offset, int size, int is_signed); diff --git a/include/trace/ftrace.h b/include/trace/ftrace.h index 40ede4d..e3370c5 100644 --- a/include/trace/ftrace.h +++ b/include/trace/ftrace.h @@ -124,6 +124,10 @@ #undef TP_printk #define TP_printk(fmt, args...) fmt "\n", args +#undef TP_FORMAT +#define TP_FORMAT(fmt, args...) \ + "%s\n", ftrace_read_binary(p, event_call, entry) + #undef __get_dynamic_array #define __get_dynamic_array(field) \ ((void *)__entry + __entry->__data_loc_##field) @@ -152,6 +156,7 @@ enum print_line_t \ ftrace_raw_output_##call(struct trace_iterator *iter, int flags) \ { \ + struct ftrace_event_call *event_call __maybe_unused = &event_##call; \ struct trace_seq *s = &iter->seq; \ struct ftrace_raw_##call *field; \ struct trace_entry *entry; \ @@ -234,7 +239,10 @@ ftrace_raw_output_##call(struct trace_iterator *iter, int flags) \ #define __entry REC #undef TP_printk -#define TP_printk(fmt, args...) "%s, %s\n", #fmt, __stringify(args) +#define TP_printk(fmt, args...) "print fmt: %s, %s\n", #fmt, __stringify(args) + +#undef TP_FORMAT +#define TP_FORMAT(fmt, args...) "format: " fmt "\n", ##args #undef TP_fast_assign #define TP_fast_assign(args...) args @@ -249,7 +257,7 @@ ftrace_format_##call(struct trace_seq *s) \ \ tstruct; \ \ - trace_seq_printf(s, "\nprint fmt: " print); \ + trace_seq_printf(s, "\n" print); \ \ return ret; \ } @@ -279,6 +287,13 @@ ftrace_format_##call(struct trace_seq *s) \ offsetof(typeof(field), __data_loc_##item), \ sizeof(field.__data_loc_##item), 0); +#undef TP_printk +#define TP_printk(fmt, args...) + +#undef TP_FORMAT +#define TP_FORMAT(fmt, args...) \ + ftrace_initialize_print(event_call, fmt, ##args) + #undef __string #define __string(item, src) __dynamic_array(char, item, -1) @@ -299,6 +314,8 @@ ftrace_define_fields_##call(void) \ \ tstruct; \ \ + print; \ + \ return ret; \ } @@ -563,6 +580,7 @@ static int ftrace_raw_init_event_##call(void) \ event_##call.id = id; \ INIT_LIST_HEAD(&event_##call.fields); \ init_preds(&event_##call); \ + INIT_LIST_HEAD(&event_##call.print_info); \ return 0; \ } \ \ diff --git a/kernel/trace/Makefile b/kernel/trace/Makefile index 06b8585..7c2ff68 100644 --- a/kernel/trace/Makefile +++ b/kernel/trace/Makefile @@ -51,5 +51,6 @@ obj-$(CONFIG_EVENT_TRACING) += trace_export.o obj-$(CONFIG_FTRACE_SYSCALLS) += trace_syscalls.o obj-$(CONFIG_EVENT_PROFILE) += trace_event_profile.o obj-$(CONFIG_EVENT_TRACING) += trace_events_filter.o +obj-$(CONFIG_EVENT_TRACING) += trace_read_binary.o libftrace-y := ftrace.o diff --git a/kernel/trace/trace_read_binary.c b/kernel/trace/trace_read_binary.c new file mode 100644 index 0000000..100d5c0 --- /dev/null +++ b/kernel/trace/trace_read_binary.c @@ -0,0 +1,674 @@ +/* + * trace_read_binary.c + * + * Copyright (C) 2009 Red Hat Inc, Steven Rostedt + * + */ + +#include +#include +#include +#include + +#include "trace.h" + +static DEFINE_MUTEX(buffer_lock); +static struct trace_seq buffer; + +/* + * Binary string parser. The print format uses a special language to explain + * the format to print the entry out. The language is as follows: + * + * FMT := constant string FMT | COMMAND FMT | empty + * COMMAND := | | | + * + * TYPE := int | hex | ptr | string | strarray + * FIELD := defined by the event structure + * MASKS := MASK=NAME,MASKS | MASK=NAME + * MASK := the bit mask to match + * DELIM := delimiter to separate the fields. None and ':' are both allowed + * SYMBOLS := SYM=NAME,SYMBOLS | SYM=NAME + * SYM := the symbol value to test against + * TRUE := print when field is non zero + * FALSE := print when field is zero or NULL + * NAME := the name to write when a match is found + * + * A '\<' would print '<' + */ + +#define TOK_SIZE 32 + +enum field_types { + FIELD_IS_TEXT, + FIELD_IS_INT, + FIELD_IS_PTR, + FIELD_IS_LT, + FIELD_IS_IF, + FIELD_IS_STRING, + FIELD_IS_STRARRAY, + FIELD_IS_HEX, + FIELD_IS_MASK, + FIELD_IS_SYMBOL, +}; + +struct sym_mask { + struct list_head list; + unsigned long long val; + unsigned short start; + unsigned short len; +}; + +struct print_info { + struct list_head list; + enum field_types type; + union { + struct { + unsigned short start; + unsigned short len; + } text; + struct { + struct ftrace_event_field *field; + } data; + struct { + struct ftrace_event_field *field; + unsigned short true_text; + unsigned short true_len; + unsigned short false_text; + unsigned short false_len; + } cond; + struct { + struct ftrace_event_field *field; + struct list_head masks; + unsigned short delim; + unsigned short len; + } sym_mask; + }; +}; + +static struct print_info * +alloc_print_info(struct ftrace_event_call *call, enum field_types type) +{ + struct print_info *info; + + info = kmalloc(sizeof(*info), GFP_KERNEL); + if (!info) + return NULL; + + info->type = type; + + list_add_tail(&info->list, &call->print_info); + + return info; +} + +static int +add_text(struct ftrace_event_call *call, const char *start, const char *end) +{ + struct print_info *info; + + info = alloc_print_info(call, FIELD_IS_TEXT); + if (!info) + return -ENOMEM; + + info->text.start = start - call->print_text; + if (!end) + end = call->print_text + strlen(call->print_text); + info->text.len = end - start; + + return 0; +} + +static int +add_less_than(struct ftrace_event_call *call, const char *start, const char *end) +{ + struct print_info *info; + + info = alloc_print_info(call, FIELD_IS_LT); + if (!info) + return -ENOMEM; + + info->text.start = start - call->print_text; + info->text.len = end - start; + + return 0; +} + +static int +add_data(struct ftrace_event_call *call, enum field_types type, + struct ftrace_event_field *field) +{ + struct print_info *info; + + info = alloc_print_info(call, type); + if (!info) + return -ENOMEM; + + info->data.field = field; + + return 0; +} + +static int +add_if(struct ftrace_event_call *call, struct ftrace_event_field *field, + const char *fmt, const char *end) +{ + struct print_info *info; + const char *tok; + + info = alloc_print_info(call, FIELD_IS_IF); + if (!info) + return -ENOMEM; + + info->cond.field = field; + + tok = strchr(fmt, ':'); + if (!tok || tok > end) + return -1; + + info->cond.true_text = fmt - call->print_text; + info->cond.true_len = tok - fmt; + + fmt = tok + 1; + + info->cond.false_text = fmt - call->print_text; + info->cond.false_len = end - fmt; + + return 0; +} + +static int add_sym_mask(struct ftrace_event_call *call, struct list_head *list, + unsigned long long val, + const char *start, const char *end) +{ + struct sym_mask *sm; + + sm = kmalloc(sizeof(*sm), GFP_KERNEL); + if (!sm) + return -ENOMEM; + + list_add_tail(&sm->list, list); + sm->val = val; + sm->start = start - call->print_text; + sm->len = end - start; + + return 0; +} + +static const char * +add_mask(struct ftrace_event_call *call, struct ftrace_event_field *field, + const char *delim, unsigned int delim_len, + const char *fmt, const char *end) +{ + struct print_info *info; + unsigned long long mask; + const char *tok; + + info = alloc_print_info(call, FIELD_IS_MASK); + if (!info) + return end; + + info->sym_mask.field = field; + + INIT_LIST_HEAD(&info->sym_mask.masks); + info->sym_mask.len = delim_len; + if (delim_len) + info->sym_mask.delim = delim - call->print_text; + + do { + while (isspace(*fmt)) + fmt++; + + tok = strchr(fmt, '='); + if (!tok || tok > end) + goto out_err; + + mask = simple_strtoull(fmt, NULL, 0); + fmt = tok + 1; + + tok = strchr(fmt, ','); + if (!tok || tok > end) + tok = end; + + add_sym_mask(call, &info->sym_mask.masks, mask, fmt, tok); + + fmt = tok + 1; + } while (fmt < end); + + return end; + out_err: + WARN_ON(1); + printk("error in format '%s'\n", fmt); + return end; +} + +static const char * +add_symbol(struct ftrace_event_call *call, struct ftrace_event_field *field, + const char *fmt, const char *end) +{ + struct print_info *info; + unsigned long long sym; + const char *tok; + + info = alloc_print_info(call, FIELD_IS_SYMBOL); + if (!info) + return end; + + info->sym_mask.field = field; + + INIT_LIST_HEAD(&info->sym_mask.masks); + + do { + while (isspace(*fmt)) + fmt++; + + tok = strchr(fmt, '='); + if (!tok || tok > end) + goto out_err; + + sym = simple_strtoull(fmt, NULL, 0); + fmt = tok + 1; + + tok = strchr(fmt, ','); + if (!tok || tok > end) + tok = end; + + add_sym_mask(call, &info->sym_mask.masks, sym, fmt, tok); + + fmt = tok + 1; + } while (fmt < end); + + return end; + out_err: + WARN_ON(1); + printk("error in format '%s'\n", fmt); + return end; +} + +static struct ftrace_event_field * +find_field(struct ftrace_event_call *call, const char *name, int len) +{ + struct ftrace_event_field *field; + + list_for_each_entry(field, &call->fields, link) { + if (!strncmp(field->name, name, len)) + return field; + } + + return NULL; +} + +const char *handle_field(struct ftrace_event_call *event, + const char *fmt, enum field_types field_type) +{ + struct ftrace_event_field *field; + const char *end, *tok, *delim; + unsigned int delim_len; + + end = strchr(fmt, '>'); + if (!end) + goto out_err; + + switch (field_type) { + case FIELD_IS_INT: + case FIELD_IS_PTR: + case FIELD_IS_HEX: + case FIELD_IS_STRING: + case FIELD_IS_STRARRAY: + field = find_field(event, fmt, end - fmt); + if (!field) + goto out_err; + + add_data(event, field_type, field); + break; + + case FIELD_IS_IF: + tok = strchr(fmt, ':'); + if (!tok || tok > end) + goto out_err; + + field = find_field(event, fmt, tok - fmt); + if (!field) + goto out_err; + + fmt = tok + 1; + + add_if(event, field, fmt, end); + break; + + case FIELD_IS_MASK: + case FIELD_IS_SYMBOL: + tok = strchr(fmt, ':'); + if (!tok || tok > end) + goto out_err; + + field = find_field(event, fmt, tok - fmt); + if (!field) + goto out_err; + + fmt = tok + 1; + + if (field_type == FIELD_IS_MASK) { + tok = strchr(fmt, ':'); + if (!tok || tok > end) + goto out_err; + + delim = fmt; + delim_len = tok - fmt; + + /* we allow ':' as a delimiter */ + if (!delim_len && tok[1] == ':') { + tok++; + delim_len++; + } + + fmt = tok+1; + + end = add_mask(event, field, delim, delim_len, fmt, end); + } else + end = add_symbol(event, field, fmt, end); + + break; + default: + WARN_ON(1); + printk("unknown field\n"); + } + + end++; + return end; + + out_err: + WARN_ON(1); + printk("error in format field: '%s'\n", fmt); + return NULL; +} + +int +ftrace_initialize_print(struct ftrace_event_call *event, const char *fmt, ...) +{ + const char *tok; + va_list ap; + int ret; + + mutex_lock(&buffer_lock); + trace_seq_init(&buffer); + + va_start(ap, fmt); + ret = trace_seq_vprintf(&buffer, fmt, ap); + va_end(ap); + if (!ret) + goto err_unlock; + + ret = trace_seq_putc(&buffer, 0); + if (!ret) + goto err_unlock; + + event->print_text = kstrdup(buffer.buffer, GFP_KERNEL); + if (!event->print_text) + goto err_unlock; + + mutex_unlock(&buffer_lock); + + fmt = event->print_text; + + do { + enum field_types field_type; + + tok = strchr(fmt, '<'); + if (!tok) { + add_text(event, fmt, tok); + break; + } + if (*(tok - 1) == '\\') { + add_less_than(event, fmt, tok); + fmt = tok + 1; + continue; + } + + add_text(event, fmt, tok); + + fmt = tok + 1; + + tok = strchr(fmt, ':'); + if (!tok) + goto err_format; + + if (strncmp(fmt, "int:", 4) == 0) + field_type = FIELD_IS_INT; + + else if (strncmp(fmt, "ptr:", 4) == 0) + field_type = FIELD_IS_PTR; + + else if (strncmp(fmt, "string:", 7) ==0) + field_type = FIELD_IS_STRING; + + else if (strncmp(fmt, "hex:", 4) == 0) + field_type = FIELD_IS_HEX; + + else if (strncmp(fmt, "if:", 3) == 0) + field_type = FIELD_IS_IF; + + else if (strncmp(fmt, "mask:", 5) == 0) + field_type = FIELD_IS_MASK; + + else if (strncmp(fmt, "sym:", 4) == 0) + field_type = FIELD_IS_SYMBOL; + + else if (strncmp(fmt, "strarray:", 9) == 0) + field_type = FIELD_IS_STRARRAY; + + else + goto err_format; + + tok++; + fmt = handle_field(event, tok, field_type); + + } while (fmt); + + return 0; + + err_unlock: + WARN_ON(1); + printk("Can not allocate event print format data\n"); + mutex_unlock(&buffer_lock); + return -1; + + err_format: + WARN_ON(1); + printk("error in format type: '%s'\n", fmt); + return -1; +} +EXPORT_SYMBOL_GPL(ftrace_initialize_print); + + +static void +trace_read_mask(struct trace_seq *s, unsigned long long val, + struct print_info *info, struct ftrace_event_call *event) +{ + unsigned long long mask; + struct sym_mask *sm; + int first = 1; + + list_for_each_entry(sm, &info->sym_mask.masks, list) { + mask = sm->val; + + if (first && !mask && !val) { + trace_seq_putmem(s, event->print_text + sm->start, + sm->len); + return; + } + + if (mask && (mask & val) == mask) { + if (first) + first = 0; + else if (info->sym_mask.len) + trace_seq_putmem(s, event->print_text + + info->sym_mask.delim, + info->sym_mask.len); + val &= ~mask; + + trace_seq_putmem(s, event->print_text + sm->start, + sm->len); + } + } + + if (val) + trace_seq_printf(s, "(%llx)", val); + + return; +} + +static void +trace_read_symbol(struct trace_seq *s, unsigned long long val, + struct print_info *info, struct ftrace_event_call *event) +{ + unsigned long long sym; + struct sym_mask *sm; + int found = 0; + + list_for_each_entry(sm, &info->sym_mask.masks, list) { + sym = sm->val; + + if (sym == val) { + found = 1; + trace_seq_putmem(s, event->print_text + sm->start, + sm->len); + break; + } + } + + if (!found) + trace_seq_printf(s, "(%llx)", val); + +} + +char * +ftrace_read_binary(struct trace_seq *s, struct ftrace_event_call *event, + struct trace_entry *entry) +{ + unsigned long long val, mask; + struct print_info *info; + char *start = s->buffer + s->len; + struct ftrace_event_field *field; + void *p; + + list_for_each_entry(info, &event->print_info, list) { + + p = entry; + + switch (info->type) { + case FIELD_IS_LT: + case FIELD_IS_TEXT: + trace_seq_putmem(s, event->print_text + info->text.start, + info->text.len); + if (info->type == FIELD_IS_LT) + trace_seq_putc(s, '<'); + break; + case FIELD_IS_INT: + case FIELD_IS_HEX: + case FIELD_IS_PTR: + field = info->data.field; + goto skip_if; + + case FIELD_IS_IF: + field = info->cond.field; + skip_if: + p += field->offset; + + switch (field->size) { + case 1: + val = *(char *)p; + mask = 0xffULL; + break; + case 2: + val = *(short *)p; + mask = 0xffffULL; + break; + case 4: + val = *(int *)p; + mask = 0xffffffffULL; + break; + case 8: + val = *(long long*)p; + mask = 0; + break; + + default: + trace_seq_printf(s, "\n", + field->size); + return start; + } + + if (info->type == FIELD_IS_IF) { + if (val) + trace_seq_putmem(s, event->print_text + + info->cond.true_text, + info->cond.true_len); + else + trace_seq_putmem(s, event->print_text + + info->cond.false_text, + info->cond.false_len); + } else if (info->type == FIELD_IS_INT) + trace_seq_printf(s, "%lld", val); + else { + /* hex should only print the size specified */ + if (mask) + val &= mask; + + trace_seq_printf(s, "%llx", val); + } + + break; + + case FIELD_IS_STRING: + p += info->data.field->offset; + /* indexes are expected to be unsigned short */ + WARN_ON(info->data.field->size != 2); + p = (void *)entry + *(unsigned short *)p; + trace_seq_puts(s, p); + break; + + case FIELD_IS_STRARRAY: + p += info->data.field->offset; + trace_seq_puts(s, p); + break; + + case FIELD_IS_MASK: + case FIELD_IS_SYMBOL: + + p += info->sym_mask.field->offset; + + switch (info->sym_mask.field->size) { + case 1: + val = *(unsigned char *)p; + break; + case 2: + val = *(unsigned short *)p; + break; + case 4: + val = *(unsigned int *)p; + break; + case 8: + val = *(unsigned long long*)p; + break; + + default: + trace_seq_printf(s, "\n", + info->sym_mask.field->size); + return start; + } + + if (info->type == FIELD_IS_MASK) + trace_read_mask(s, val, info, event); + else + trace_read_symbol(s, val, info, event); + break; + default: + trace_seq_printf(s, "UNKNOWN TYPE %d\n", info->type); + } + } + + trace_seq_putc(s, 0); + + return start; +} +EXPORT_SYMBOL_GPL(ftrace_read_binary); -- 1.6.3.1 -- -- To unsubscribe from this list: send the line "unsubscribe linux-kernel" in the body of a message to majordomo@vger.kernel.org More majordomo info at http://vger.kernel.org/majordomo-info.html Please read the FAQ at http://www.tux.org/lkml/