From 71e711430cd793daa0599c5f911018889778798b Mon Sep 17 00:00:00 2001 From: thiagoftsm Date: Wed, 3 Mar 2021 16:08:08 +0000 Subject: Add Linux page cache metrics to eBPF (#10693) Add new eBPF thread to display page cache utilization. --- collectors/ebpf.plugin/ebpf.c | 73 ++-- collectors/ebpf.plugin/ebpf.conf | 2 + collectors/ebpf.plugin/ebpf.h | 9 +- collectors/ebpf.plugin/ebpf_apps.c | 32 +- collectors/ebpf.plugin/ebpf_apps.h | 6 + collectors/ebpf.plugin/ebpf_cachestat.c | 658 ++++++++++++++++++++++++++++++++ collectors/ebpf.plugin/ebpf_cachestat.h | 63 +++ 7 files changed, 809 insertions(+), 34 deletions(-) create mode 100644 collectors/ebpf.plugin/ebpf_cachestat.c create mode 100644 collectors/ebpf.plugin/ebpf_cachestat.h (limited to 'collectors') diff --git a/collectors/ebpf.plugin/ebpf.c b/collectors/ebpf.plugin/ebpf.c index 3f84f817db..9959279cef 100644 --- a/collectors/ebpf.plugin/ebpf.c +++ b/collectors/ebpf.plugin/ebpf.c @@ -83,6 +83,9 @@ ebpf_module_t ebpf_modules[] = { { .thread_name = "socket", .config_name = "socket", .enabled = 0, .start_routine = ebpf_socket_thread, .update_time = 1, .global_charts = 1, .apps_charts = 1, .mode = MODE_ENTRY, .optional = 0, .apps_routine = ebpf_socket_create_apps_charts }, + { .thread_name = "cachestat", .config_name = "cachestat", .enabled = 0, .start_routine = ebpf_cachestat_thread, + .update_time = 1, .global_charts = 1, .apps_charts = 1, .mode = MODE_ENTRY, + .optional = 0, .apps_routine = ebpf_cachestat_create_apps_charts }, { .thread_name = NULL, .enabled = 0, .start_routine = NULL, .update_time = 1, .global_charts = 0, .apps_charts = 1, .mode = MODE_ENTRY, .optional = 0, .apps_routine = NULL }, @@ -585,21 +588,23 @@ void ebpf_print_help() "\n" " Available command line options:\n" "\n" - " SECONDS set the data collection frequency.\n" + " SECONDS Set the data collection frequency.\n" "\n" - " --help or -h show this help.\n" + " --help or -h Show this help.\n" "\n" - " --version or -v show software version.\n" + " --version or -v Show software version.\n" "\n" - " --global or -g disable charts per application.\n" + " --global or -g Disable charts per application.\n" "\n" - " --all or -a Enable all chart groups (global and apps), unless -g is also given.\n" + " --all or -a Enable all chart groups (global and apps), unless -g is also given.\n" "\n" - " --net or -n Enable network viewer charts.\n" + " --cachestat or -c Enable charts related to process run time.\n" "\n" - " --process or -p Enable charts related to process run time.\n" + " --net or -n Enable network viewer charts.\n" "\n" - " --return or -r Run the collector in return mode.\n" + " --process or -p Enable charts related to process run time.\n" + "\n" + " --return or -r Run the collector in return mode.\n" "\n", VERSION, (year >= 116) ? year + 1900 : 2020); @@ -1657,7 +1662,7 @@ static void read_collector_values(int *disable_apps) // Read ebpf programs section enabled = appconfig_get_boolean(&collector_config, EBPF_PROGRAMS_SECTION, - ebpf_modules[0].config_name, CONFIG_BOOLEAN_YES); + ebpf_modules[EBPF_MODULE_PROCESS_IDX].config_name, CONFIG_BOOLEAN_YES); int started = 0; if (enabled) { ebpf_enable_chart(EBPF_MODULE_PROCESS_IDX, *disable_apps); @@ -1668,7 +1673,8 @@ static void read_collector_values(int *disable_apps) enabled = appconfig_get_boolean(&collector_config, EBPF_PROGRAMS_SECTION, "network viewer", CONFIG_BOOLEAN_NO); if (!enabled) - enabled = appconfig_get_boolean(&collector_config, EBPF_PROGRAMS_SECTION, ebpf_modules[1].config_name, + enabled = appconfig_get_boolean(&collector_config, EBPF_PROGRAMS_SECTION, + ebpf_modules[EBPF_MODULE_SOCKET_IDX].config_name, CONFIG_BOOLEAN_NO); if (enabled) { @@ -1685,7 +1691,15 @@ static void read_collector_values(int *disable_apps) if (!enabled) enabled = appconfig_get_boolean(&collector_config, EBPF_PROGRAMS_SECTION, "network connections", CONFIG_BOOLEAN_NO); - ebpf_modules[1].optional = enabled; + ebpf_modules[EBPF_MODULE_SOCKET_IDX].optional = enabled; + + enabled = appconfig_get_boolean(&collector_config, EBPF_PROGRAMS_SECTION, "cachestat", + CONFIG_BOOLEAN_NO); + + if (enabled) { + ebpf_enable_chart(EBPF_MODULE_CACHESTAT_IDX, *disable_apps); + started++; + } if (!started){ ebpf_enable_all_charts(*disable_apps); @@ -1761,13 +1775,14 @@ static void parse_args(int argc, char **argv) int freq = 0; int option_index = 0; static struct option long_options[] = { - {"help", no_argument, 0, 'h' }, - {"version", no_argument, 0, 'v' }, - {"global", no_argument, 0, 'g' }, - {"all", no_argument, 0, 'a' }, - {"net", no_argument, 0, 'n' }, - {"process", no_argument, 0, 'p' }, - {"return", no_argument, 0, 'r' }, + {"help", no_argument, 0, 'h' }, + {"version", no_argument, 0, 'v' }, + {"global", no_argument, 0, 'g' }, + {"all", no_argument, 0, 'a' }, + {"cachestat", no_argument, 0, 'c' }, + {"net", no_argument, 0, 'n' }, + {"process", no_argument, 0, 'p' }, + {"return", no_argument, 0, 'r' }, {0, 0, 0, 0} }; @@ -1782,7 +1797,7 @@ static void parse_args(int argc, char **argv) } while (1) { - int c = getopt_long(argc, argv, "hvganpr", long_options, &option_index); + int c = getopt_long(argc, argv, "hvgcanpr", long_options, &option_index); if (c == -1) break; @@ -1808,6 +1823,15 @@ static void parse_args(int argc, char **argv) ebpf_enable_all_charts(disable_apps); #ifdef NETDATA_INTERNAL_CHECKS info("EBPF running with all chart groups, because it was started with the option \"--all\" or \"-a\"."); +#endif + break; + } + case 'c': { + enabled = 1; + ebpf_enable_chart(EBPF_MODULE_CACHESTAT_IDX, disable_apps); +#ifdef NETDATA_INTERNAL_CHECKS + info( + "EBPF enabling \"CACHESTAT\" charts, because it was started with the option \"--cachestat\" or \"-c\"."); #endif break; } @@ -1953,9 +1977,14 @@ int main(int argc, char **argv) read_local_ports("/proc/net/udp6", IPPROTO_UDP); struct netdata_static_thread ebpf_threads[] = { - {"EBPF PROCESS", NULL, NULL, 1, NULL, NULL, ebpf_modules[0].start_routine}, - {"EBPF SOCKET" , NULL, NULL, 1, NULL, NULL, ebpf_modules[1].start_routine}, - {NULL , NULL, NULL, 0, NULL, NULL, NULL} + {"EBPF PROCESS", NULL, NULL, 1, + NULL, NULL, ebpf_modules[EBPF_MODULE_PROCESS_IDX].start_routine}, + {"EBPF SOCKET" , NULL, NULL, 1, + NULL, NULL, ebpf_modules[EBPF_MODULE_SOCKET_IDX].start_routine}, + {"EBPF CACHESTAT" , NULL, NULL, 1, + NULL, NULL, ebpf_modules[EBPF_MODULE_CACHESTAT_IDX].start_routine}, + {NULL , NULL, NULL, 0, + NULL, NULL, NULL} }; //clean_loaded_events(); diff --git a/collectors/ebpf.plugin/ebpf.conf b/collectors/ebpf.plugin/ebpf.conf index 3a5b77395d..f31b5fca4e 100644 --- a/collectors/ebpf.plugin/ebpf.conf +++ b/collectors/ebpf.plugin/ebpf.conf @@ -19,11 +19,13 @@ # # The eBPF collector enables and runs the following eBPF programs by default: # +# `cachestat`: Make charts for kernel functions related to page cache. # `process` : This eBPF program creates charts that show information about process creation, VFS IO, and # files removed. # `socket` : This eBPF program creates charts with information about `TCP` and `UDP` functions, including the # bandwidth consumed by each. [ebpf programs] + cachestat = no process = yes socket = yes network connections = no diff --git a/collectors/ebpf.plugin/ebpf.h b/collectors/ebpf.plugin/ebpf.h index 6a7a214f0c..21665738cd 100644 --- a/collectors/ebpf.plugin/ebpf.h +++ b/collectors/ebpf.plugin/ebpf.h @@ -70,8 +70,11 @@ typedef struct netdata_error_report { } netdata_error_report_t; extern ebpf_module_t ebpf_modules[]; -#define EBPF_MODULE_PROCESS_IDX 0 -#define EBPF_MODULE_SOCKET_IDX 1 +enum ebpf_module_indexes { + EBPF_MODULE_PROCESS_IDX, + EBPF_MODULE_SOCKET_IDX, + EBPF_MODULE_CACHESTAT_IDX +}; // Copied from musl header #ifndef offsetof @@ -181,6 +184,7 @@ extern void ebpf_cleanup_publish_syscall(netdata_publish_syscall_t *nps); #define EBPF_NETWORK_VIEWER_SECTION "network connections" #define EBPF_SERVICE_NAME_SECTION "service name" +#define EBPF_COMMON_DIMENSION_PERCENTAGE "%" #define EBPF_COMMON_DIMENSION_CALL "calls/s" #define EBPF_COMMON_DIMENSION_BITS "kilobits/s" #define EBPF_COMMON_DIMENSION_BYTES "bytes/s" @@ -198,6 +202,7 @@ extern char *ebpf_algorithms[]; // Common functions extern void ebpf_process_create_apps_charts(struct ebpf_module *em, void *ptr); extern void ebpf_socket_create_apps_charts(struct ebpf_module *em, void *ptr); +extern void ebpf_cachestat_create_apps_charts(struct ebpf_module *em, void *root); extern collected_number get_value_from_structure(char *basis, size_t offset); extern struct pid_stat *root_of_pids; extern ebpf_process_stat_t *global_process_stat; diff --git a/collectors/ebpf.plugin/ebpf_apps.c b/collectors/ebpf.plugin/ebpf_apps.c index 844ce23b83..1be7b92604 100644 --- a/collectors/ebpf.plugin/ebpf_apps.c +++ b/collectors/ebpf.plugin/ebpf_apps.c @@ -909,6 +909,26 @@ static inline void del_pid_entry(pid_t pid) all_pids_count--; } +/** + * Cleanup variable from other threads + * + * @param pid current pid. + */ +void cleanup_variables_from_other_threads(uint32_t pid) +{ + // Clean socket structures + if (socket_bandwidth_curr) { + freez(socket_bandwidth_curr[pid]); + socket_bandwidth_curr[pid] = NULL; + } + + // Clean cachestat strcture + if (cachestat_pid) { + freez(cachestat_pid[pid]); + cachestat_pid[pid] = NULL; + } +} + /** * Remove PIDs when they are not running more. */ @@ -932,11 +952,7 @@ void cleanup_exited_pids() freez(current_apps_data[r]); current_apps_data[r] = NULL; - // Clean socket structures - if (socket_bandwidth_curr) { - freez(socket_bandwidth_curr[r]); - socket_bandwidth_curr[r] = NULL; - } + cleanup_variables_from_other_threads(r); } else { if (unlikely(p->keep)) p->keeploops++; @@ -1054,11 +1070,7 @@ void collect_data_for_all_processes(int tbl_pid_stats_fd) freez(current_apps_data[key]); current_apps_data[key] = NULL; - // Clean socket structures - if (socket_bandwidth_curr) { - freez(socket_bandwidth_curr[key]); - socket_bandwidth_curr[key] = NULL; - } + cleanup_variables_from_other_threads(key); pids = pids->next; continue; diff --git a/collectors/ebpf.plugin/ebpf_apps.h b/collectors/ebpf.plugin/ebpf_apps.h index f8cb7ac725..cace52d7bc 100644 --- a/collectors/ebpf.plugin/ebpf_apps.h +++ b/collectors/ebpf.plugin/ebpf_apps.h @@ -15,8 +15,10 @@ #define NETDATA_APPS_VFS_GROUP "ebpf vfs" #define NETDATA_APPS_PROCESS_GROUP "ebpf process" #define NETDATA_APPS_NET_GROUP "ebpf net" +#define NETDATA_APPS_CACHESTAT_GROUP "ebpf cachestat" #include "ebpf_process.h" +#include "ebpf_cachestat.h" #define MAX_COMPARE_NAME 100 #define MAX_NAME 100 @@ -105,6 +107,9 @@ struct target { uid_t uid; gid_t gid; + // Page cache statistic per process + netdata_publish_cachestat_t cachestat; + /* These variables are not necessary for eBPF collector kernel_uint_t minflt; kernel_uint_t cminflt; @@ -426,5 +431,6 @@ extern void collect_data_for_all_processes(int tbl_pid_stats_fd); extern ebpf_process_stat_t **global_process_stats; extern ebpf_process_publish_apps_t **current_apps_data; +extern netdata_publish_cachestat_t **cachestat_pid; #endif /* NETDATA_EBPF_APPS_H */ diff --git a/collectors/ebpf.plugin/ebpf_cachestat.c b/collectors/ebpf.plugin/ebpf_cachestat.c new file mode 100644 index 0000000000..93be8e6e93 --- /dev/null +++ b/collectors/ebpf.plugin/ebpf_cachestat.c @@ -0,0 +1,658 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "ebpf.h" +#include "ebpf_cachestat.h" + +static ebpf_data_t cachestat_data; +netdata_publish_cachestat_t **cachestat_pid; + +static struct bpf_link **probe_links = NULL; +static struct bpf_object *objects = NULL; + +static char *cachestat_counter_dimension_name[NETDATA_CACHESTAT_END] = { "ratio", "dirty", "hit", + "miss" }; +static netdata_syscall_stat_t *cachestat_counter_aggregated_data = NULL; +static netdata_publish_syscall_t *cachestat_counter_publish_aggregated = NULL; + +netdata_cachestat_pid_t *cachestat_vector = NULL; + +static netdata_idx_t *cachestat_hash_values = NULL; + +static int read_thread_closed = 1; + +struct netdata_static_thread cachestat_threads = {"CACHESTAT KERNEL", + NULL, NULL, 1, NULL, + NULL, NULL}; + +static int *map_fd = NULL; + +/***************************************************************** + * + * FUNCTIONS TO CLOSE THE THREAD + * + *****************************************************************/ + +/** + * Clean PID structures + * + * Clean the allocated structures. + */ +static void clean_pid_structures() { + struct pid_stat *pids = root_of_pids; + while (pids) { + freez(cachestat_pid[pids->pid]); + + pids = pids->next; + } +} + +/** + * Clean up the main thread. + * + * @param ptr thread data. + */ +static void ebpf_cachestat_cleanup(void *ptr) +{ + ebpf_module_t *em = (ebpf_module_t *)ptr; + if (!em->enabled) + return; + + heartbeat_t hb; + heartbeat_init(&hb); + uint32_t tick = 2*USEC_PER_MS; + while (!read_thread_closed) { + usec_t dt = heartbeat_next(&hb, tick); + UNUSED(dt); + } + + clean_pid_structures(); + freez(cachestat_pid); + + freez(cachestat_counter_aggregated_data); + ebpf_cleanup_publish_syscall(cachestat_counter_publish_aggregated); + freez(cachestat_counter_publish_aggregated); + + freez(cachestat_vector); + freez(cachestat_hash_values); + + struct bpf_program *prog; + size_t i = 0 ; + bpf_object__for_each_program(prog, objects) { + bpf_link__destroy(probe_links[i]); + i++; + } + bpf_object__close(objects); +} + +/***************************************************************** + * + * COMMON FUNCTIONS + * + *****************************************************************/ + +/** + * Write charts + * + * Write the current information to publish the charts. + * + * @param family chart family + * @param chart chart id + * @param dim dimension name + * @param v1 value. + */ +static inline void cachestat_write_charts(char *family, char *chart, char *dim, long long v1) +{ + write_begin_chart(family, chart); + + write_chart_dimension(dim, v1); + + write_end_chart(); +} + +/** + * Update publish + * + * Update publish values before to write dimension. + * + * @param out strcuture that will receive data. + * @param mpa calls for mark_page_accessed during the last second. + * @param mbd calls for mark_buffer_dirty during the last second. + * @param apcl calls for add_to_page_cache_lru during the last second. + * @param apd calls for account_page_dirtied during the last second. + */ +void cachestat_update_publish(netdata_publish_cachestat_t *out, uint64_t mpa, uint64_t mbd, + uint64_t apcl, uint64_t apd) +{ + // Adapted algorithm from https://github.com/iovisor/bcc/blob/master/tools/cachestat.py#L126-L138 + calculated_number total = (calculated_number) (((long long)mpa) - ((long long)mbd)); + if (total < 0) + total = 0; + + calculated_number misses = (calculated_number) ( ((long long) apcl) - ((long long) apd) ); + if (misses < 0) + misses = 0; + + // If hits are < 0, then its possible misses are overestimate due to possibly page cache read ahead adding + // more pages than needed. In this case just assume misses as total and reset hits. + calculated_number hits = total - misses; + if (hits < 0 ) { + misses = total; + hits = 0; + } + + calculated_number ratio = (total > 0) ? hits/total : 0; + + out->ratio = (long long )(ratio*100); + out->hit = (long long)hits; + out->miss = (long long)misses; +} + +/** + * Save previous values + * + * Save values used this time. + * + * @param publish + */ +static void save_previous_values(netdata_publish_cachestat_t *publish) { + publish->prev.mark_page_accessed = cachestat_hash_values[NETDATA_KEY_CALLS_MARK_PAGE_ACCESSED]; + publish->prev.account_page_dirtied = cachestat_hash_values[NETDATA_KEY_CALLS_ACCOUNT_PAGE_DIRTIED]; + publish->prev.add_to_page_cache_lru = cachestat_hash_values[NETDATA_KEY_CALLS_ADD_TO_PAGE_CACHE_LRU]; + publish->prev.mark_buffer_dirty = cachestat_hash_values[NETDATA_KEY_CALLS_MARK_BUFFER_DIRTY]; +} + +/** + * Calculate statistics + * + * @param publish the structure where we will store the data. + */ +static void calculate_stats(netdata_publish_cachestat_t *publish) { + if (!publish->prev.mark_page_accessed) { + save_previous_values(publish); + return; + } + + uint64_t mpa = cachestat_hash_values[NETDATA_KEY_CALLS_MARK_PAGE_ACCESSED] - publish->prev.mark_page_accessed; + uint64_t mbd = cachestat_hash_values[NETDATA_KEY_CALLS_MARK_BUFFER_DIRTY] - publish->prev.mark_buffer_dirty; + uint64_t apcl = cachestat_hash_values[NETDATA_KEY_CALLS_ADD_TO_PAGE_CACHE_LRU] - publish->prev.add_to_page_cache_lru; + uint64_t apd = cachestat_hash_values[NETDATA_KEY_CALLS_ACCOUNT_PAGE_DIRTIED] - publish->prev.account_page_dirtied; + + save_previous_values(publish); + + // We are changing the original algorithm to have a smooth ratio. + cachestat_update_publish(publish, mpa, mbd, apcl, apd); +} + + +/***************************************************************** + * + * APPS + * + *****************************************************************/ + +/** + * Apps Accumulator + * + * Sum all values read from kernel and store in the first address. + * + * @param out the vector with read values. + */ +static void cachestat_apps_accumulator(netdata_cachestat_pid_t *out) +{ + int i, end = (running_on_kernel >= NETDATA_KERNEL_V4_15) ? ebpf_nprocs : 1; + netdata_cachestat_pid_t *total = &out[0]; + for (i = 1; i < end; i++) { + netdata_cachestat_pid_t *w = &out[i]; + total->account_page_dirtied += w->account_page_dirtied; + total->add_to_page_cache_lru += w->add_to_page_cache_lru; + total->mark_buffer_dirty += w->mark_buffer_dirty; + total->mark_page_accessed += w->mark_page_accessed; + } +} + +/** + * Save Pid values + * + * Save the current values inside the structure + * + * @param out vector used to plot charts + * @param publish vector with values read from hash tables. + */ +static inline void cachestat_save_pid_values(netdata_publish_cachestat_t *out, netdata_cachestat_pid_t *publish) +{ + if (!out->current.mark_page_accessed) { + memcpy(&out->current, &publish[0], sizeof(netdata_cachestat_pid_t)); + return; + } + + memcpy(&out->prev, &out->current, sizeof(netdata_cachestat_pid_t)); + memcpy(&out->current, &publish[0], sizeof(netdata_cachestat_pid_t)); +} + +/** + * Fill PID + * + * Fill PID structures + * + * @param current_pid pid that we are collecting data + * @param out values read from hash tables; + */ +static void cachestat_fill_pid(uint32_t current_pid, netdata_cachestat_pid_t *publish) +{ + netdata_publish_cachestat_t *curr = cachestat_pid[current_pid]; + if (!curr) { + curr = callocz(1, sizeof(netdata_publish_cachestat_t)); + cachestat_pid[current_pid] = curr; + + cachestat_save_pid_values(curr, publish); + return; + } + + cachestat_save_pid_values(curr, publish); +} + +/** + * Read APPS table + * + * Read the apps table and store data inside the structure. + */ +static void read_apps_table() +{ + netdata_cachestat_pid_t *cv = cachestat_vector; + uint32_t key; + struct pid_stat *pids = root_of_pids; + int fd = map_fd[NETDATA_CACHESTAT_PID_STATS]; + size_t length = sizeof(netdata_cachestat_pid_t)*ebpf_nprocs; + while (pids) { + key = pids->pid; + + if (bpf_map_lookup_elem(fd, &key, cv)) { + pids = pids->next; + continue; + } + + cachestat_apps_accumulator(cv); + + cachestat_fill_pid(key, cv); + + // We are cleaning to avoid passing data read from one process to other. + memset(cv, 0, length); + + pids = pids->next; + } +} + +/** + * Create apps charts + * + * Call ebpf_create_chart to create the charts on apps submenu. + * + * @param em a pointer to the structure with the default values. + */ +void ebpf_cachestat_create_apps_charts(struct ebpf_module *em, void *ptr) +{ + UNUSED(em); + struct target *root = ptr; + ebpf_create_charts_on_apps(NETDATA_CACHESTAT_HIT_RATIO_CHART, + "The ratio is calculated dividing the Hit pages per total cache accesses without counting dirties.", + EBPF_COMMON_DIMENSION_PERCENTAGE, + NETDATA_APPS_CACHESTAT_GROUP, + 20090, + ebpf_algorithms[NETDATA_EBPF_ABSOLUTE_IDX], + root); + + ebpf_create_charts_on_apps(NETDATA_CACHESTAT_DIRTY_CHART, + "Number of pages marked as dirty. When a page is called dirty, this means that the data stored inside the page needs to be written to devices.", + EBPF_CACHESTAT_DIMENSION_PAGE, + NETDATA_APPS_CACHESTAT_GROUP, + 20091, + ebpf_algorithms[NETDATA_EBPF_INCREMENTAL_IDX], + root); + + ebpf_create_charts_on_apps(NETDATA_CACHESTAT_HIT_CHART, + "Number of cache access without counting dirty pages and page additions.", + EBPF_CACHESTAT_DIMENSION_HITS, + NETDATA_APPS_CACHESTAT_GROUP, + 20092, + ebpf_algorithms[NETDATA_EBPF_ABSOLUTE_IDX], + root); + + ebpf_create_charts_on_apps(NETDATA_CACHESTAT_MISSES_CHART, + "Page caches added without counting dirty pages", + EBPF_CACHESTAT_DIMENSION_MISSES, + NETDATA_APPS_CACHESTAT_GROUP, + 20093, + ebpf_algorithms[NETDATA_EBPF_ABSOLUTE_IDX], + root); +} + +/***************************************************************** + * + * MAIN LOOP + * + *****************************************************************/ + +/** + * Read global counter + * + * Read the table with number of calls for all functions + */ +static void read_global_table() +{ + uint32_t idx; + netdata_idx_t *val = cachestat_hash_values; + netdata_idx_t stored; + int fd = map_fd[NETDATA_CACHESTAT_GLOBAL_STATS]; + + for (idx = NETDATA_KEY_CALLS_ADD_TO_PAGE_CACHE_LRU; idx < NETDATA_CACHESTAT_END; idx++) { + if (!bpf_map_lookup_elem(fd, &idx, &stored)) { + val[idx] = stored; + } + } +} + +/** + * Socket read hash + * + * This is the thread callback. + * This thread is necessary, because we cannot freeze the whole plugin to read the data on very busy socket. + * + * @param ptr It is a NULL value for this thread. + * + * @return It always returns NULL. + */ +void *ebpf_cachestat_read_hash(void *ptr) +{ + read_thread_closed = 0; + + heartbeat_t hb; + heartbeat_init(&hb); + usec_t step = NETDATA_LATENCY_CACHESTAT_SLEEP_MS; + + ebpf_module_t *em = (ebpf_module_t *)ptr; + + int apps = em->apps_charts; + while (!close_ebpf_plugin) { + usec_t dt = heartbeat_next(&hb, step); + (void)dt; + + read_global_table(); + + if (apps) + read_apps_table(); + } + read_thread_closed = 1; + + return NULL; +} + +/** + * Send global + * + * Send global charts to Netdata + */ +static void cachestat_send_global(netdata_publish_cachestat_t *publish) +{ + calculate_stats(publish); + + netdata_publish_syscall_t *ptr = cachestat_counter_publish_aggregated; + // The algorithm sets this value to zero sometimes, we are not written them to have a smooth chart + if (publish->ratio) { + cachestat_write_charts(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_HIT_RATIO_CHART, + ptr[NETDATA_CACHESTAT_IDX_RATIO].dimension, publish->ratio); + } + + cachestat_write_charts(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_DIRTY_CHART, + ptr[NETDATA_CACHESTAT_IDX_DIRTY].dimension, + cachestat_hash_values[NETDATA_KEY_CALLS_MARK_BUFFER_DIRTY]); + + cachestat_write_charts(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_HIT_CHART, + ptr[NETDATA_CACHESTAT_IDX_HIT].dimension, publish->hit); + + cachestat_write_charts(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_MISSES_CHART, + ptr[NETDATA_CACHESTAT_IDX_MISS].dimension, publish->miss); +} + +/** + * Cachestat sum PIDs + * + * Sum values for all PIDs associated to a group + * + * @param publish output structure. + * @param root structure with listed IPs + */ +void ebpf_cachestat_sum_pids(netdata_publish_cachestat_t *publish, struct pid_on_target *root) +{ + memcpy(&publish->prev, &publish->current,sizeof(publish->current)); + memset(&publish->current, 0, sizeof(publish->current)); + + netdata_cachestat_pid_t *dst = &publish->current; + while (root) { + int32_t pid = root->pid; + netdata_publish_cachestat_t *w = cachestat_pid[pid]; + if (w) { + netdata_cachestat_pid_t *src = &w->current; + dst->account_page_dirtied += src->account_page_dirtied; + dst->add_to_page_cache_lru += src->add_to_page_cache_lru; + dst->mark_buffer_dirty += src->mark_buffer_dirty; + dst->mark_page_accessed += src->mark_page_accessed; + } + + root = root->next; + } +} + +/** + * Send data to Netdata calling auxiliar functions. + * + * @param root the target list. +*/ +void ebpf_cache_send_apps_data(struct target *root) +{ + struct target *w; + collected_number value; + + write_begin_chart(NETDATA_APPS_FAMILY, NETDATA_CACHESTAT_HIT_RATIO_CHART); + for (w = root; w; w = w->next) { + if (unlikely(w->exposed && w->processes)) { + ebpf_cachestat_sum_pids(&w->cachestat, w->root_pid); + netdata_cachestat_pid_t *current = &w->cachestat.current; + netdata_cachestat_pid_t *prev = &w->cachestat.prev; + + uint64_t mpa = current->mark_page_accessed - prev->mark_page_accessed; + uint64_t mbd = current->mark_buffer_dirty - prev->mark_buffer_dirty; + w->cachestat.dirty = current->mark_buffer_dirty; + uint64_t apcl = current->add_to_page_cache_lru - prev->add_to_page_cache_lru; + uint64_t apd = current->account_page_dirtied - prev->account_page_dirtied; + + cachestat_update_publish(&w->cachestat, mpa, mbd, apcl, apd); + value = (collected_number) w->cachestat.ratio; + // Here we are using different approach to have a chart more smooth + write_chart_dimension(w->name, value); + } + } + write_end_chart(); + + write_begin_chart(NETDATA_APPS_FAMILY, NETDATA_CACHESTAT_DIRTY_CHART); + for (w = root; w; w = w->next) { + if (unlikely(w->exposed && w->processes)) { + value = (collected_number) w->cachestat.dirty; + write_chart_dimension(w->name, value); + } + } + write_end_chart(); + + write_begin_chart(NETDATA_APPS_FAMILY, NETDATA_CACHESTAT_HIT_CHART); + for (w = root; w; w = w->next) { + if (unlikely(w->exposed && w->processes)) { + value = (collected_number) w->cachestat.hit; + write_chart_dimension(w->name, value); + } + } + write_end_chart(); + + write_begin_chart(NETDATA_APPS_FAMILY, NETDATA_CACHESTAT_MISSES_CHART); + for (w = root; w; w = w->next) { + if (unlikely(w->exposed && w->processes)) { + value = (collected_number) w->cachestat.miss; + write_chart_dimension(w->name, value); + } + } + write_end_chart(); +} + +/** +* Main loop for this collector. +*/ +static void cachestat_collector(ebpf_module_t *em) +{ + cachestat_threads.thread = mallocz(sizeof(netdata_thread_t)); + cachestat_threads.start_routine = ebpf_cachestat_read_hash; + + map_fd = cachestat_data.map_fd; + + netdata_thread_create(cachestat_threads.thread, cachestat_threads.name, NETDATA_THREAD_OPTION_JOINABLE, + ebpf_cachestat_read_hash, em); + + netdata_publish_cachestat_t publish; + memset(&publish, 0, sizeof(publish)); + int apps = em->apps_charts; + while (!close_ebpf_plugin) { + pthread_mutex_lock(&collect_data_mutex); + pthread_cond_wait(&collect_data_cond_var, &collect_data_mutex); + + pthread_mutex_lock(&lock); + + cachestat_send_global(&publish); + + if (apps) + ebpf_cache_send_apps_data(apps_groups_root_target); + + pthread_mutex_unlock(&lock); + pthread_mutex_unlock(&collect_data_mutex); + } +} + +/***************************************************************** + * + * INITIALIZE THREAD + * + *****************************************************************/ + +/** + * Create global charts + * + * Call ebpf_create_chart to create the charts for the collector. + */ +static void ebpf_create_memory_charts() +{ + ebpf_create_chart(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_HIT_RATIO_CHART, + "Hit is calculating using total cache added without dirties per total added because of red misses.", + EBPF_CACHESTAT_DIMENSION_HITS, NETDATA_CACHESTAT_SUBMENU, + NULL, + 21100, + ebpf_create_global_dimension, + cachestat_counter_publish_aggregated, 1); + + ebpf_create_chart(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_DIRTY_CHART, + "Number of dirty pages added to the page cache.", + EBPF_CACHESTAT_DIMENSION_PAGE, NETDATA_CACHESTAT_SUBMENU, + NULL, + 21101, + ebpf_create_global_dimension, + &cachestat_counter_publish_aggregated[NETDATA_CACHESTAT_IDX_DIRTY], 1); + + ebpf_create_chart(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_HIT_CHART, + "Hits are function calls that Netdata counts.", + EBPF_CACHESTAT_DIMENSION_HITS, NETDATA_CACHESTAT_SUBMENU, + NULL, + 21102, + ebpf_create_global_dimension, + &cachestat_counter_publish_aggregated[NETDATA_CACHESTAT_IDX_HIT], 1); + + ebpf_create_chart(NETDATA_EBPF_MEMORY_GROUP, NETDATA_CACHESTAT_MISSES_CHART, + "Misses are function calls that Netdata counts.", + EBPF_CACHESTAT_DIMENSION_MISSES, NETDATA_CACHESTAT_SUBMENU, + NULL, + 21103, + ebpf_create_global_dimension, + &cachestat_counter_publish_aggregated[NETDATA_CACHESTAT_IDX_MISS], 1); + + fflush(stdout); +} + +/** + * Allocate vectors used with this thread. + * + * We are not testing the return, because callocz does this and shutdown the software + * case it was not possible to allocate. + * + * @param length is the length for the vectors used inside the collector. + */ +static void ebpf_cachestat_allocate_global_vectors(size_t length) +{ + cachestat_pid = callocz((size_t)pid_max, sizeof(netdata_publish_cachestat_t *)); + cachestat_vector = callocz((size_t)ebpf_nprocs, sizeof(netdata_cachestat_pid_t)); + + cachestat_hash_values = callocz(length, sizeof(netdata_idx_t)); + + cachestat_counter_aggregated_data = callocz(length, sizeof(netdata_syscall_stat_t)); + cachestat_counter_publish_aggregated = callocz(length, sizeof(netdata_publish_syscall_t)); +} + +/***************************************************************** + * + * MAIN THREAD + * + *****************************************************************/ + +/** + * Cachestat thread + * + * Thread used to make cachestat thread + * + * @param ptr a pointer to `struct ebpf_module` + * + * @return It always return NULL + */ +void *ebpf_cachestat_thread(void *ptr) +{ + netdata_thread_cleanup_push(ebpf_cachestat_cleanup, ptr); + + ebpf_module_t *em = (ebpf_module_t *)ptr; + fill_ebpf_data(&cachestat_data); + + if (!em->enabled) + goto endcachestat; + + pthread_mutex_lock(&lock); + ebpf_cachestat_allocate_global_vectors(NETDATA_CACHESTAT_END); + if (ebpf_update_kernel(&cachestat_data)) { + pthread_mutex_unlock(&lock); + goto endcachestat; + } + + probe_links = ebpf_load_program(ebpf_plugin_dir, em, kernel_string, &objects, cachestat_data.map_fd); + if (!probe_links) { + pthread_mutex_unlock(&lock); + goto endcachestat; + } + + int algorithms[NETDATA_CACHESTAT_END] = { + NETDATA_EBPF_ABSOLUTE_IDX, NETDATA_EBPF_INCREMENTAL_IDX, NETDATA_EBPF_ABSOLUTE_IDX, NETDATA_EBPF_ABSOLUTE_IDX + }; + + ebpf_global_labels(cachestat_counter_aggregated_data, cachestat_counter_publish_aggregated, + cachestat_counter_dimension_name, cachestat_counter_dimension_name, + algorithms, NETDATA_CACHESTAT_END); + + ebpf_create_memory_charts(); + + pthread_mutex_unlock(&lock); + + cachestat_collector(em); + +endcachestat: + netdata_thread_cleanup_pop(1); + return NULL; +} diff --git a/collectors/ebpf.plugin/ebpf_cachestat.h b/collectors/ebpf.plugin/ebpf_cachestat.h new file mode 100644 index 0000000000..b1100b9432 --- /dev/null +++ b/collectors/ebpf.plugin/ebpf_cachestat.h @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_EBPF_CACHESTAT_H +#define NETDATA_EBPF_CACHESTAT_H 1 + +#define NETDATA_EBPF_MEMORY_GROUP "mem" + +// charts +#define NETDATA_CACHESTAT_HIT_RATIO_CHART "cachestat_ratio" +#define NETDATA_CACHESTAT_DIRTY_CHART "cachestat_dirties" +#define NETDATA_CACHESTAT_HIT_CHART "cachestat_hits" +#define NETDATA_CACHESTAT_MISSES_CHART "cachestat_misses" + +#define NETDATA_CACHESTAT_SUBMENU "page cache" + +#define EBPF_CACHESTAT_DIMENSION_PAGE "pages/s" +#define EBPF_CACHESTAT_DIMENSION_HITS "hits/s" +#define EBPF_CACHESTAT_DIMENSION_MISSES "misses/s" + +#define NETDATA_LATENCY_CACHESTAT_SLEEP_MS 600000ULL + +// variables +enum cachestat_counters { + NETDATA_KEY_CALLS_ADD_TO_PAGE_CACHE_LRU, + NETDATA_KEY_CALLS_MARK_PAGE_ACCESSED, + NETDATA_KEY_CALLS_ACCOUNT_PAGE_DIRTIED, + NETDATA_KEY_CALLS_MARK_BUFFER_DIRTY, + + NETDATA_CACHESTAT_END +}; + +enum cachestat_indexes { + NETDATA_CACHESTAT_IDX_RATIO, + NETDATA_CACHESTAT_IDX_DIRTY, + NETDATA_CACHESTAT_IDX_HIT, + NETDATA_CACHESTAT_IDX_MISS +}; + +enum cachesta_tables { + NETDATA_CACHESTAT_GLOBAL_STATS, + NETDATA_CACHESTAT_PID_STATS +}; + +typedef struct netdata_publish_cachestat_pid { + uint64_t add_to_page_cache_lru; + uint64_t mark_page_accessed; + uint64_t account_page_dirtied; + uint64_t mark_buffer_dirty; +} netdata_cachestat_pid_t; + +typedef struct netdata_publish_cachestat { + long long ratio; + long long dirty; + long long hit; + long long miss; + + netdata_cachestat_pid_t current; + netdata_cachestat_pid_t prev; +} netdata_publish_cachestat_t; + +extern void *ebpf_cachestat_thread(void *ptr); + +#endif // NETDATA_EBPF_CACHESTAT_H -- cgit v1.2.3