From: Hui Zhu <[email protected]> Add an example of BPF-driven async proactive reclaim, keeping the complex monitoring scenario out of the selftests. A BPF program arms a bpf_wq/timer chain that watches the workingset refaults (file and anon) of a monitor cgroup; when they grow past a threshold, a round of bpf_proactive_reclaim() batches reclaims a target cgroup, re-arming the work queue until the round budget is spent.
It runs in two modes: bench: creates a high and a low priority cgroup, drives a page-cache workload in the high one and replicates the old selftest benchmark, with reclaim counters and optional baseline comparison as the result. watch: takes existing cgroups to monitor and reclaim and runs continuously, printing periodic statistics, so it can serve as a simple proactive-reclaim daemon. Threshold, interval, batch size, per-round batches and swappiness are all settable on the command line. Signed-off-by: Hui Zhu <[email protected]> --- samples/bpf/.gitignore | 1 + samples/bpf/Makefile | 10 +- samples/bpf/memcg_async_reclaim.bpf.c | 349 ++++++++ samples/bpf/memcg_async_reclaim.h | 39 + samples/bpf/memcg_async_reclaim_user.c | 1141 ++++++++++++++++++++++++ 5 files changed, 1539 insertions(+), 1 deletion(-) create mode 100644 samples/bpf/memcg_async_reclaim.bpf.c create mode 100644 samples/bpf/memcg_async_reclaim.h create mode 100644 samples/bpf/memcg_async_reclaim_user.c diff --git a/samples/bpf/.gitignore b/samples/bpf/.gitignore index 0002cd359fb1..ab1f0e14f88c 100644 --- a/samples/bpf/.gitignore +++ b/samples/bpf/.gitignore @@ -6,6 +6,7 @@ ibumad lathist lwt_len_hist map_perf_test +memcg_async_reclaim offwaketime per_socket_stats_example sampleip diff --git a/samples/bpf/Makefile b/samples/bpf/Makefile index 95a4fa1f1e44..eed26a823172 100644 --- a/samples/bpf/Makefile +++ b/samples/bpf/Makefile @@ -37,6 +37,7 @@ tprogs-y += xdp_fwd tprogs-y += task_fd_query tprogs-y += ibumad tprogs-y += hbm +tprogs-y += memcg_async_reclaim # Libbpf dependencies LIBBPF_SRC = $(TOOLS_PATH)/lib/bpf @@ -78,6 +79,7 @@ xdp_fwd-objs := xdp_fwd_user.o task_fd_query-objs := task_fd_query_user.o $(TRACE_HELPERS) ibumad-objs := ibumad_user.o hbm-objs := hbm.o $(CGROUP_HELPERS) +memcg_async_reclaim-objs := memcg_async_reclaim_user.o $(CGROUP_HELPERS) xdp_router_ipv4-objs := xdp_router_ipv4_user.o $(XDP_SAMPLE) @@ -122,6 +124,7 @@ always-y += task_fd_query_kern.o always-y += ibumad_kern.o always-y += hbm_out_kern.o always-y += hbm_edt_kern.o +always-y += memcg_async_reclaim.bpf.o COMMON_CFLAGS = $(TPROGS_USER_CFLAGS) TPROGS_LDFLAGS = $(TPROGS_USER_LDFLAGS) @@ -284,6 +287,10 @@ $(obj)/$(TRACE_HELPERS) $(obj)/$(CGROUP_HELPERS) $(obj)/$(XDP_SAMPLE): | libbpf_ $(obj)/xdp_router_ipv4_user.o: $(obj)/xdp_router_ipv4.skel.h +$(obj)/memcg_async_reclaim_user.o: $(obj)/memcg_async_reclaim.skel.h \ + $(src)/memcg_async_reclaim.h +$(obj)/memcg_async_reclaim.bpf.o: $(src)/memcg_async_reclaim.h + $(obj)/tracex5.bpf.o: $(obj)/syscall_nrs.h $(obj)/hbm_out_kern.o: $(src)/hbm.h $(src)/hbm_kern.h $(obj)/hbm.o: $(src)/hbm.h @@ -347,10 +354,11 @@ $(obj)/%.bpf.o: $(src)/%.bpf.c $(obj)/vmlinux.h $(src)/xdp_sample.bpf.h $(src)/x -I$(LIBBPF_INCLUDE) $(CLANG_SYS_INCLUDES) \ -c $(filter %.bpf.c,$^) -o $@ -LINKED_SKELS := xdp_router_ipv4.skel.h +LINKED_SKELS := xdp_router_ipv4.skel.h memcg_async_reclaim.skel.h clean-files += $(LINKED_SKELS) xdp_router_ipv4.skel.h-deps := xdp_router_ipv4.bpf.o xdp_sample.bpf.o +memcg_async_reclaim.skel.h-deps := memcg_async_reclaim.bpf.o LINKED_BPF_SRCS := $(patsubst %.bpf.o,%.bpf.c,$(foreach skel,$(LINKED_SKELS),$($(skel)-deps))) diff --git a/samples/bpf/memcg_async_reclaim.bpf.c b/samples/bpf/memcg_async_reclaim.bpf.c new file mode 100644 index 000000000000..7bde946aaa9b --- /dev/null +++ b/samples/bpf/memcg_async_reclaim.bpf.c @@ -0,0 +1,349 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Refault-driven asynchronous proactive reclaim. + * + * A bpf_timer ticks every cfg->interval_ns. On each tick a bpf_wq callback + * checks whether the monitored cgroup refaulted enough to warrant reclaim; if + * so it opens a round of up to cfg->max_batches batches and reclaims one batch + * of cfg->batch_bytes per callback, requeueing itself until the round's budget + * is spent. Nothing here blocks the monitored workload: the reclaim happens on + * a workqueue, against a different cgroup. + */ + +#include "vmlinux.h" +#include <bpf/bpf_helpers.h> +#include <bpf/bpf_core_read.h> + +#include "memcg_async_reclaim.h" + +#define CLOCK_MONOTONIC 1 + +struct cgroup_memcg { + struct cgroup *cgrp; + struct mem_cgroup *memcg; +}; + +static u64 wq_monitor_cgroup_id; +static u64 wq_target_cgroup_id; + +/* + * Statistics exposed to userspace through .bss, so that the loader can report + * what actually happened instead of inferring it from timings. + * + * These are __u64 rather than u64 because bpftool emits the type name verbatim + * into the generated skeleton, and the loader has no kernel typedefs. + */ +__u64 reclaim_calls; +__u64 reclaimed_bytes; + +/* + * Number of rounds started, i.e. ticks on which the monitored cgroup crossed + * the refault threshold. A round spans up to max_batches callbacks, so this + * tells the two apart: many calls per round means the target had a lot to + * give, many rounds means the monitor keeps refaulting. + */ +__u64 reclaim_rounds; + +/* + * A timer that fails to rearm stops the whole chain, which from userspace looks + * exactly like "nothing needed reclaiming". Count it so the two can be told + * apart instead of blaming the workload. + */ +__u64 timer_failures; + +/* + * bpf_proactive_reclaim() reports failure as a negative return, which is + * otherwise indistinguishable from "this cgroup has nothing left to reclaim". + * Keep the last one so a failing kfunc is not mistaken for an idle cgroup. + * Stored as a positive errno. + */ +__u64 last_reclaim_err; + +struct { + __uint(type, BPF_MAP_TYPE_RINGBUF); + __uint(max_entries, 1 << 16); +} reclaim_events SEC(".maps"); + +/* + * Reports are best effort: when the ringbuf is full, for example when + * userspace does not consume the events, the report is dropped. + */ +static void emit_reclaim_event(enum reclaim_outcome outcome) +{ + struct reclaim_event *ev; + + ev = bpf_ringbuf_reserve(&reclaim_events, sizeof(*ev), 0); + if (!ev) + return; + + ev->outcome = outcome; + bpf_ringbuf_submit(ev, 0); +} + +static int get_cgroup_memcg_from_id(u64 cgroup_id, struct cgroup_memcg *cm) +{ + cm->cgrp = bpf_cgroup_from_id(cgroup_id); + if (!cm->cgrp) + return -1; + + cm->memcg = bpf_get_mem_cgroup(&cm->cgrp->self); + if (!cm->memcg) { + bpf_cgroup_release(cm->cgrp); + return -1; + } + + return 0; +} + +static void put_cgroup_memcg(struct cgroup_memcg *cm) +{ + bpf_put_mem_cgroup(cm->memcg); + bpf_cgroup_release(cm->cgrp); +} + +/* + * Total workingset refaults of a cgroup, across memory types. + * + * Pressure shows up in the counter matching the memory the workload uses: a + * page-cache workload refaults file pages, an anonymous one refaults anon. + * Watching only either counter makes the trigger blind to the other kind of + * workload, so sum them; for a workload that stresses one type only, the + * other term stays 0. + */ +static int get_cgroup_refaults(u64 cgroup_id, u64 *val) +{ + struct cgroup_memcg cm; + + if (get_cgroup_memcg_from_id(cgroup_id, &cm)) + return -1; + bpf_mem_cgroup_flush_stats(cm.memcg); + *val = bpf_mem_cgroup_page_state(cm.memcg, + bpf_core_enum_value(enum node_stat_item, + WORKINGSET_REFAULT_FILE)) + + bpf_mem_cgroup_page_state(cm.memcg, + bpf_core_enum_value(enum node_stat_item, + WORKINGSET_REFAULT_ANON)); + put_cgroup_memcg(&cm); + + return 0; +} + +static bool +should_reclaim_cgroup(u64 cgroup_id, u64 *prev_refaults, u64 threshold) +{ + u64 cur, delta; + + if (get_cgroup_refaults(cgroup_id, &cur)) + return false; + + delta = cur - *prev_refaults; + *prev_refaults = cur; + + return delta >= threshold; +} + +/* + * rmdir clears CSS_ONLINE on cgrp->self in cgroup_destroy_locked() and only + * then deactivates the cgroup's kernfs node with kernfs_remove(). + * bpf_cgroup_from_id() resolves an id through + * kernfs_find_and_get_node_by_id(), which refuses deactivated nodes, so the + * lookup starts failing inside the rmdir syscall -- not, as one might expect, + * once the cgroup's last reference has been dropped. + * + * The window in which the lookup still succeeds while the cgroup is already + * dying is therefore just the few statements between those two. An attempt that + * lands in it reports RECLAIM_OUTCOME_SKIPPED_DYING; after rmdir has returned, + * every attempt reports TARGET_GONE instead. The check is kept because it is + * the only thing standing between reclaim and a cgroup that is mid-teardown. + * + * Only CSS_ONLINE is worth testing. CSS_DYING is raised by kill_css_sync(), + * which dereferences css->ss and so is never called with cgrp->self; the + * cgroup's own css only ever loses CSS_ONLINE. This is therefore + * cgroup_is_dead() from include/linux/cgroup.h, not css_is_dying(). + * + * CSS_ONLINE comes from vmlinux.h: the kernel defines it in an anonymous enum, + * so bpf_core_enum_value() has no enum type to bind to, and redeclaring it + * locally would clash with the vmlinux.h enumerators. vmlinux.h is generated + * from the running kernel's BTF, so the value already matches the target + * kernel. + */ +static bool cgroup_is_dying(struct cgroup *cgrp) +{ + return !(cgrp->self.flags & CSS_ONLINE); +} + +/* + * Reclaim one batch from the target cgroup. Returns the number of bytes + * reclaimed, or 0 if the cgroup is dying or gone, the kfunc failed, or nothing + * was reclaimed. Each attempt reports its outcome through the reclaim_events + * ringbuf, and a failing kfunc additionally records its errno in + * last_reclaim_err. + */ +static u64 reclaim_cgroup(u64 cgroup_id, u64 size, int swappiness) +{ + struct cgroup_memcg cm; + long nr; + + if (get_cgroup_memcg_from_id(cgroup_id, &cm)) { + emit_reclaim_event(RECLAIM_OUTCOME_TARGET_GONE); + return 0; + } + + if (cgroup_is_dying(cm.cgrp)) { + emit_reclaim_event(RECLAIM_OUTCOME_SKIPPED_DYING); + put_cgroup_memcg(&cm); + return 0; + } + + reclaim_calls++; + nr = bpf_proactive_reclaim(cm.memcg, size, swappiness); + if (nr < 0) + last_reclaim_err = -nr; + else if (nr > 0) + reclaimed_bytes += nr; + emit_reclaim_event(RECLAIM_OUTCOME_CALLED); + + put_cgroup_memcg(&cm); + + return nr > 0 ? nr : 0; +} + +struct wq_elem { + struct bpf_timer timer; + struct bpf_wq work; + u64 prev_refaults; + u64 refault_threshold; + u64 check_ns; + u64 batch_bytes; + u64 max_batches; + int swappiness; + /* + * Bytes still to reclaim in the current round, carried across requeues. + * 0 means no round is in progress; the timer path starts a new round by + * resetting it, requeued work only looks at it. + */ + u64 remaining; +}; + +struct { + __uint(type, BPF_MAP_TYPE_ARRAY); + __uint(max_entries, 1); + __type(key, __u32); + __type(value, struct wq_elem); +} wq_map SEC(".maps"); + +/* + * batch_bytes is a byte count, not a page count: bpf_proactive_reclaim() + * converts it against the running kernel's PAGE_SIZE and returns bytes, so the + * configuration holds on any page size. The kfunc also clamps a batch to the + * kernel's MEMCG_CHARGE_BATCH, which is not visible to BPF; a clamped batch + * only means a round needs more callbacks, because remaining is decremented by + * the bytes actually reclaimed. + */ +static int reclaim_work_fn(void *map, int *key, void *value) +{ + struct wq_elem *elem = value; + u64 nr, size; + + if (!elem->remaining) { + /* + * Timer-triggered entry: start a new round only when the + * monitored cgroup refaults enough. Requeued entries skip this + * check and only look at remaining, so a round consumes the + * refault delta only on the tick that starts it; ticks that find + * no round in progress consume and discard it. + */ + if (!should_reclaim_cgroup(wq_monitor_cgroup_id, + &elem->prev_refaults, + elem->refault_threshold)) + return 0; + elem->remaining = elem->max_batches * elem->batch_bytes; + reclaim_rounds++; + } + + /* One bounded reclaim pass per callback */ + size = elem->remaining < elem->batch_bytes ? elem->remaining + : elem->batch_bytes; + nr = reclaim_cgroup(wq_target_cgroup_id, size, elem->swappiness); + if (!nr) { + elem->remaining = 0; + return 0; + } + + /* try_to_free_mem_cgroup_pages() may reclaim more than requested */ + if (nr >= elem->remaining) + elem->remaining = 0; + else + elem->remaining -= nr; + + /* Requeue the same work item for the next batch */ + if (elem->remaining) + bpf_wq_start(&elem->work, 0); + + return 0; +} + +static int wq_timer_cb(void *map, int *key, struct wq_elem *elem) +{ + /* + * A failed bpf_wq_start() self-heals on the next tick; a failed rearm + * does not, so only the latter is worth reporting. + */ + bpf_wq_start(&elem->work, 0); + if (bpf_timer_start(&elem->timer, elem->check_ns, 0)) + timer_failures++; + + return 0; +} + +SEC("syscall") +int reclaim_prog_init(struct reclaim_cfg *ctx) +{ + struct wq_elem *elem; + __u32 key = 0; + int ret; + + elem = bpf_map_lookup_elem(&wq_map, &key); + if (!elem) + return -1; + + ret = bpf_wq_init(&elem->work, &wq_map, 0); + if (ret) + return ret; + + ret = bpf_wq_set_callback(&elem->work, reclaim_work_fn, 0); + if (ret) + return ret; + + ret = bpf_timer_init(&elem->timer, &wq_map, CLOCK_MONOTONIC); + if (ret) + return ret; + + ret = bpf_timer_set_callback(&elem->timer, wq_timer_cb); + if (ret) + return ret; + + elem->prev_refaults = 0; + elem->remaining = 0; + elem->refault_threshold = ctx->refault_threshold; + elem->check_ns = ctx->interval_ns; + elem->batch_bytes = ctx->batch_bytes; + elem->max_batches = ctx->max_batches; + elem->swappiness = ctx->swappiness; + + wq_monitor_cgroup_id = ctx->monitor_cgroup_id; + wq_target_cgroup_id = ctx->target_cgroup_id; + + /* + * Seed the refault baseline instead of leaving it at 0: a cgroup that + * has been running for a while has a large counter already, and the + * first tick would otherwise read the whole history as new refaults and + * open a round nobody asked for. Failing to read it is not fatal, it + * only costs that first spurious round. + */ + get_cgroup_refaults(wq_monitor_cgroup_id, &elem->prev_refaults); + + return bpf_timer_start(&elem->timer, elem->check_ns, 0); +} + +char _license[] SEC("license") = "GPL"; diff --git a/samples/bpf/memcg_async_reclaim.h b/samples/bpf/memcg_async_reclaim.h new file mode 100644 index 000000000000..96fe03d12912 --- /dev/null +++ b/samples/bpf/memcg_async_reclaim.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* + * Shared between memcg_async_reclaim.bpf.c and memcg_async_reclaim_user.c. + * + * Both sides must have pulled in their type headers first: vmlinux.h for the + * BPF program, <linux/types.h> (via test_progs.h or libbpf) for the loader. + */ +#ifndef __MEMCG_ASYNC_RECLAIM_H +#define __MEMCG_ASYNC_RECLAIM_H + +struct reclaim_cfg { + /* Cgroup whose refaults are watched, i.e. the one under pressure. */ + __u64 monitor_cgroup_id; + /* Cgroup that gets reclaimed from when the monitor refaults. */ + __u64 target_cgroup_id; + /* Refault delta per tick that starts a reclaim round. */ + __u64 refault_threshold; + /* Monitor tick period, in nanoseconds. */ + __u64 interval_ns; + /* Bytes requested per bpf_wq callback. */ + __u64 batch_bytes; + /* Callbacks per reclaim round. */ + __u64 max_batches; + /* Passed through to bpf_proactive_reclaim(): -1, 0..200, or 201. */ + __s64 swappiness; +}; + +/* Outcome of a single reclaim attempt, reported through the ringbuf. */ +enum reclaim_outcome { + RECLAIM_OUTCOME_CALLED, + RECLAIM_OUTCOME_SKIPPED_DYING, + RECLAIM_OUTCOME_TARGET_GONE, +}; + +struct reclaim_event { + __u64 outcome; +}; + +#endif /* __MEMCG_ASYNC_RECLAIM_H */ diff --git a/samples/bpf/memcg_async_reclaim_user.c b/samples/bpf/memcg_async_reclaim_user.c new file mode 100644 index 000000000000..d5296fd0fab1 --- /dev/null +++ b/samples/bpf/memcg_async_reclaim_user.c @@ -0,0 +1,1141 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * memcg_async_reclaim - refault-driven asynchronous proactive reclaim + * + * A BPF program watches the workingset refaults of one cgroup and, whenever + * they grow, reclaims from a second cgroup on a workqueue through + * bpf_proactive_reclaim(). The monitored workload is never blocked: reclaim + * runs asynchronously, in bounded batches, against somebody else's memory. + * + * Two modes: + * + * bench Create a high/low priority cgroup pair, run a memory-pressured + * workload in both, and report the effect on the pressured one. By + * default the same workload is first run without the BPF program to + * produce a baseline. Self-contained: creates and removes everything. + * + * watch Watch two existing cgroups given by path and keep reclaiming for as + * long as the program runs. Removing the target while it runs is + * handled: reclaim stops and the events say so. + * + * Both modes need root. + */ + +#define _GNU_SOURCE + +#include <errno.h> +#include <fcntl.h> +#include <getopt.h> +#include <limits.h> +#include <linux/compiler.h> +#include <linux/magic.h> +#include <signal.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/stat.h> +#include <sys/statfs.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <time.h> +#include <unistd.h> + +#include <bpf/bpf.h> +#include <bpf/libbpf.h> + +#include "cgroup_helpers.h" +#include "memcg_async_reclaim.h" +#include "memcg_async_reclaim.skel.h" + +/* + * The bench workload lasts tens of milliseconds, so its tick has to be far + * shorter than that to catch any refaults at all. A daemon has no such + * constraint, and a short tick means a stat flush per tick against the + * monitored cgroup, so watch defaults to something production-shaped. + */ +#define BENCH_INTERVAL_MS 2 +#define WATCH_INTERVAL_MS 10 + +#define DEFAULT_THRESHOLD 1 +#define DEFAULT_BATCH_BYTES (128 * 1024UL) +#define DEFAULT_MAX_BATCHES 32 +#define DEFAULT_SWAPPINESS (-1L) +#define DEFAULT_STATS_SEC 10 + +#define DEFAULT_CG_LIMIT (32 * 1024 * 1024UL) +#define DEFAULT_FILE_SIZE (32 * 1024 * 1024UL) +#define DEFAULT_READ_TIMES 50UL +#define WRITE_BUFFER_SIZE 4096 + +#define BENCH_CG_DIR "/memcg_async_reclaim" +#define BENCH_CG_HIGH_DIR BENCH_CG_DIR "/high" +#define BENCH_CG_LOW_DIR BENCH_CG_DIR "/low" + +/* How long a single ring_buffer__poll() blocks while waiting for a signal. */ +#define POLL_SLICE_MS 200 + +struct options { + /* shared */ + unsigned long interval_ms; /* 0 means "use the mode's default" */ + unsigned long threshold; + unsigned long batch_bytes; + unsigned long max_batches; + long swappiness; + bool verbose; + /* bench */ + unsigned long cg_limit; + unsigned long file_size; + unsigned long read_times; + bool no_baseline; + /* watch */ + const char *monitor_path; + const char *target_path; + unsigned long duration_sec; + unsigned long stats_sec; +}; + +struct counters { + unsigned long long rounds; + unsigned long long calls; + unsigned long long bytes; +}; + +struct session { + struct memcg_async_reclaim *skel; + struct ring_buffer *rb; + bool verbose; + double start; + unsigned long long called; + unsigned long long skipped_dying; + unsigned long long target_gone; + struct counters last; +}; + +/* Set by the signal handler, polled with READ_ONCE() in the main loops. */ +static sig_atomic_t exiting; + +static void handle_signal(__maybe_unused int sig) +{ + WRITE_ONCE(exiting, 1); +} + +static double now_sec(void) +{ + struct timespec ts; + + clock_gettime(CLOCK_MONOTONIC, &ts); + + return ts.tv_sec + ts.tv_nsec / 1e9; +} + +static int parse_ulong(const char *s, unsigned long *out) +{ + unsigned long v; + char *end; + + errno = 0; + v = strtoul(s, &end, 0); + if (errno || end == s || *end) + return -1; + + *out = v; + + return 0; +} + +static int parse_long(const char *s, long *out) +{ + long v; + char *end; + + errno = 0; + v = strtol(s, &end, 0); + if (errno || end == s || *end) + return -1; + + *out = v; + + return 0; +} + +/* Accepts a plain byte count or one suffixed with K, M or G. */ +static int parse_size(const char *s, unsigned long *out) +{ + unsigned long v; + char *end; + + errno = 0; + v = strtoul(s, &end, 0); + if (errno || end == s) + return -1; + + switch (*end) { + case 'k': + case 'K': + v <<= 10; + end++; + break; + case 'm': + case 'M': + v <<= 20; + end++; + break; + case 'g': + case 'G': + v <<= 30; + end++; + break; + case '\0': + break; + default: + return -1; + } + + if (*end) + return -1; + + *out = v; + + return 0; +} + +static void fmt_bytes(char *buf, size_t len, unsigned long long bytes) +{ + if (bytes >= (1ULL << 30)) + snprintf(buf, len, "%.1f GiB", bytes / (double)(1ULL << 30)); + else if (bytes >= (1ULL << 20)) + snprintf(buf, len, "%.1f MiB", bytes / (double)(1ULL << 20)); + else if (bytes >= (1ULL << 10)) + snprintf(buf, len, "%.1f KiB", bytes / (double)(1ULL << 10)); + else + snprintf(buf, len, "%llu B", bytes); +} + +static const char *outcome_str(__u64 outcome) +{ + switch (outcome) { + case RECLAIM_OUTCOME_CALLED: + return "called"; + case RECLAIM_OUTCOME_SKIPPED_DYING: + return "skipped-dying"; + case RECLAIM_OUTCOME_TARGET_GONE: + return "target-gone"; + default: + return "unknown"; + } +} + +/* session code */ + +static int on_reclaim_event(void *ctx, void *data, size_t sz) +{ + struct session *s = ctx; + const struct reclaim_event *ev = data; + + if (sz < sizeof(*ev)) + return 0; + + switch (ev->outcome) { + case RECLAIM_OUTCOME_CALLED: + s->called++; + break; + case RECLAIM_OUTCOME_SKIPPED_DYING: + s->skipped_dying++; + break; + case RECLAIM_OUTCOME_TARGET_GONE: + s->target_gone++; + break; + } + + if (s->verbose) + printf("[%7.3fs] event %-13s called=%llu dying=%llu gone=%llu\n", + now_sec() - s->start, outcome_str(ev->outcome), + s->called, s->skipped_dying, s->target_gone); + + return 0; +} + +static void read_counters(struct session *s, struct counters *c) +{ + c->rounds = s->skel->bss->reclaim_rounds; + c->calls = s->skel->bss->reclaim_calls; + c->bytes = s->skel->bss->reclaimed_bytes; +} + +static int session_start(struct session *s, const struct options *o, + __u64 monitor_id, __u64 target_id) +{ + struct reclaim_cfg cfg = { + .monitor_cgroup_id = monitor_id, + .target_cgroup_id = target_id, + .refault_threshold = o->threshold, + .interval_ns = (unsigned long long)o->interval_ms * 1000000ULL, + .batch_bytes = o->batch_bytes, + .max_batches = o->max_batches, + .swappiness = o->swappiness, + }; + LIBBPF_OPTS(bpf_test_run_opts, run_opts, + .ctx_in = &cfg, + .ctx_size_in = sizeof(cfg)); + int init_fd, err; + + s->verbose = o->verbose; + s->start = now_sec(); + + s->skel = memcg_async_reclaim__open_and_load(); + if (!s->skel) { + fprintf(stderr, "ERROR: failed to open or load the BPF object\n"); + return -1; + } + + /* + * The syscall program installs the timer and the work item into the + * map and starts the timer; from then on the chain runs on its own. + */ + init_fd = bpf_program__fd(s->skel->progs.reclaim_prog_init); + err = bpf_prog_test_run_opts(init_fd, &run_opts); + if (err || run_opts.retval) { + fprintf(stderr, "ERROR: reclaim_prog_init failed: %s (retval %d)\n", + err ? strerror(errno) : "rejected by the program", + run_opts.retval); + goto err_out; + } + + s->rb = ring_buffer__new(bpf_map__fd(s->skel->maps.reclaim_events), + on_reclaim_event, s, NULL); + if (!s->rb) { + fprintf(stderr, "ERROR: failed to create the ring buffer\n"); + goto err_out; + } + + read_counters(s, &s->last); + + return 0; + +err_out: + memcg_async_reclaim__destroy(s->skel); + s->skel = NULL; + + return -1; +} + +/* Poll until the timeout expires; returns early on a signal or an error. */ +static int session_poll(struct session *s, int timeout_ms) +{ + int deadline_ms = timeout_ms, n; + + while (!READ_ONCE(exiting) && deadline_ms > 0) { + int slice = deadline_ms > POLL_SLICE_MS ? POLL_SLICE_MS + : deadline_ms; + + n = ring_buffer__poll(s->rb, slice); + /* A signal interrupts epoll_wait(), which is how we stop. */ + if (n < 0 && n != -EINTR) + return -1; + + deadline_ms -= slice; + } + + return 0; +} + +static void print_counters(struct session *s, const char *prefix) +{ + struct counters now; + char bytes[32], delta[32]; + + read_counters(s, &now); + fmt_bytes(bytes, sizeof(bytes), now.bytes); + fmt_bytes(delta, sizeof(delta), now.bytes - s->last.bytes); + + printf("%s rounds=%llu(+%llu) calls=%llu(+%llu) reclaimed=%s(+%s)\n", + prefix, now.rounds, now.rounds - s->last.rounds, + now.calls, now.calls - s->last.calls, bytes, delta); + + s->last = now; +} + +static void session_stop(struct session *s) +{ + unsigned long long failures, last_err; + + if (!s->skel) + return; + + ring_buffer__free(s->rb); + s->rb = NULL; + + failures = s->skel->bss->timer_failures; + last_err = s->skel->bss->last_reclaim_err; + + printf("\nran for %.1fs\n", now_sec() - s->start); + print_counters(s, "total:"); + printf("events: called=%llu skipped_dying=%llu target_gone=%llu\n", + s->called, s->skipped_dying, s->target_gone); + if (failures) + printf("WARNING: monitor timer rearm failed %llu time(s), reclaim stopped early\n", + failures); + if (last_err) + printf("WARNING: bpf_proactive_reclaim() last failed with -%llu\n", + last_err); + + /* Destroying the skeleton drops the map, which cancels timer and wq. */ + memcg_async_reclaim__destroy(s->skel); + s->skel = NULL; +} + +/* watch mode setup code */ + +/* + * cgroup_helpers' get_cgroup_id() resolves paths relative to the private + * hierarchy it mounts for the bench mode, so watch mode needs its own lookup + * against the paths the user gave. cgroupfs file handles are always 8 bytes + * and hold the cgroup id, so there is no need for the two-call size probe. + */ +static __u64 cgroup_id_from_path(const char *path) +{ + struct { + struct file_handle fh; + __u64 cgid; + } h = {}; + int mnt_id; + + h.fh.handle_bytes = sizeof(h.cgid); + + if (name_to_handle_at(AT_FDCWD, path, &h.fh, &mnt_id, 0)) + return 0; + /* + * Only the size is checked, not handle_type: cgroupfs encodes through + * kernfs_encode_fh(), whose FILEID_KERNFS is not visible to userspace. + */ + if (h.fh.handle_bytes != sizeof(h.cgid)) + return 0; + + return h.cgid; +} + +static bool memcg_on_path(const char *path) +{ + char file[PATH_MAX]; + int fd; + + snprintf(file, sizeof(file), "%s/memory.current", path); + fd = open(file, O_RDONLY); + if (fd < 0) + return false; + close(fd); + + return true; +} + +/* bench mode code */ + +/* + * The workload files must sit on a regular filesystem: with swap disabled for + * the cgroup, tmpfs pages are unevictable and would OOM the cgroup instead of + * exercising reclaim; they are also charged as shmem rather than as the page + * cache this workload is meant to build, so they never raise the workingset + * refaults the BPF program watches. + * + * /tmp is tmpfs on many systems, so fall back to the current directory. That + * fallback needs the same check: both directories are tmpfs more often than + * not. + */ +static const char *workload_files_dir(void) +{ + static const char * const dirs[] = { "/tmp", "." }; + struct statfs st; + int i; + + for (i = 0; i < 2; i++) + if (!statfs(dirs[i], &st) && st.f_type != TMPFS_MAGIC && + st.f_type != RAMFS_MAGIC) + return dirs[i]; + + return NULL; +} + +/* + * Keep the workload from swapping out, so that reclaim has to evict page cache. + * memory.swap.max only exists when the kernel is built with CONFIG_SWAP, and + * /proc/swaps is registered by the same CONFIG_SWAP-gated code (mm/swapfile.c), + * so it stands in for the config here. + */ +static int disable_swap(const char *cgroup_dir) +{ + if (access("/proc/swaps", F_OK)) + return 0; + + return write_cgroup_file(cgroup_dir, "memory.swap.max", "0"); +} + +static int setup_bench_cgroups(const struct options *o, __u64 *high_id, + __u64 *low_id) +{ + char limit_buf[32]; + int fd; + + if (setup_cgroup_environment()) { + fprintf(stderr, "ERROR: failed to set up the cgroup environment\n"); + return -1; + } + + fd = create_and_get_cgroup(BENCH_CG_DIR); + if (fd < 0) + goto err; + close(fd); + + if (enable_controllers(BENCH_CG_DIR, "memory")) + goto err; + + snprintf(limit_buf, sizeof(limit_buf), "%lu", o->cg_limit); + if (write_cgroup_file(BENCH_CG_DIR, "memory.max", limit_buf)) + goto err; + if (disable_swap(BENCH_CG_DIR)) + goto err; + + fd = create_and_get_cgroup(BENCH_CG_HIGH_DIR); + if (fd < 0) + goto err; + close(fd); + + fd = create_and_get_cgroup(BENCH_CG_LOW_DIR); + if (fd < 0) + goto err; + close(fd); + + *high_id = get_cgroup_id(BENCH_CG_HIGH_DIR); + *low_id = get_cgroup_id(BENCH_CG_LOW_DIR); + if (!*high_id || !*low_id) { + fprintf(stderr, "ERROR: failed to read the cgroup ids\n"); + goto err; + } + + return 0; + +err: + cleanup_cgroup_environment(); + + return -1; +} + +/* + * The forked children cannot report through stdio, so the exit status is the + * one diagnostics channel that reliably reaches the parent: each failing step + * gets its own code. + */ +enum child_exit_code { + CHILD_EXIT_OK = 0, + CHILD_EXIT_JOIN_CGROUP, + CHILD_EXIT_WRITE_FILE, + CHILD_EXIT_READ_FILE, + CHILD_EXIT_TIME_FILE, +}; + +static const char *child_exit_str(int code) +{ + switch (code) { + case CHILD_EXIT_OK: + return "success"; + case CHILD_EXIT_JOIN_CGROUP: + return "join cgroup"; + case CHILD_EXIT_WRITE_FILE: + return "write data file"; + case CHILD_EXIT_READ_FILE: + return "read data file"; + case CHILD_EXIT_TIME_FILE: + return "write time file"; + default: + return "unknown"; + } +} + +static int write_file(int fd, unsigned long file_size) +{ + char *buffer; + size_t written = 0; + + buffer = malloc(WRITE_BUFFER_SIZE); + if (!buffer) + return -1; + + memset(buffer, 'A', WRITE_BUFFER_SIZE); + + while (written < file_size) { + size_t to_write = file_size - written < WRITE_BUFFER_SIZE ? + file_size - written : WRITE_BUFFER_SIZE; + ssize_t n = write(fd, buffer, to_write); + + if (n <= 0) { + free(buffer); + return -1; + } + written += n; + } + + free(buffer); + + return 0; +} + +static int read_file(int fd, unsigned long file_size, unsigned long iterations) +{ + long page_size = sysconf(_SC_PAGESIZE); + unsigned long i, iter; + struct stat sb; + char *map; + + if (fstat(fd, &sb) || (unsigned long)sb.st_size != file_size) + return -1; + + map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, fd, 0); + if (map == MAP_FAILED) + return -1; + + for (iter = 0; iter < iterations; iter++) + for (i = 0; i < file_size; i += page_size) + /* touch a byte per page to trigger the fault */ + (void)READ_ONCE(map[i]); + + return munmap(map, file_size) ? -1 : 0; +} + +static int child_work(const char *cgroup_path, int data_fd, int time_fd, + const struct options *o) +{ + struct timespec start, end; + char buf[32]; + double elapsed; + int len; + + if (join_parent_cgroup(cgroup_path)) + return CHILD_EXIT_JOIN_CGROUP; + + clock_gettime(CLOCK_MONOTONIC, &start); + + if (write_file(data_fd, o->file_size)) + return CHILD_EXIT_WRITE_FILE; + if (read_file(data_fd, o->file_size, o->read_times)) + return CHILD_EXIT_READ_FILE; + + clock_gettime(CLOCK_MONOTONIC, &end); + + elapsed = (end.tv_sec - start.tv_sec) + + (end.tv_nsec - start.tv_nsec) / 1000000000.0; + + len = snprintf(buf, sizeof(buf), "%.6f", elapsed); + /* snprintf() returns the untruncated length, so bound it before write() */ + if (len < 0 || len >= (int)sizeof(buf) || write(time_fd, buf, len) != len) + return CHILD_EXIT_TIME_FILE; + + return CHILD_EXIT_OK; +} + +static int get_time(int fd, double *elapsed) +{ + char buf[64] = {}; + ssize_t n; + + /* The child wrote through the fork-shared description, so rewind. */ + if (lseek(fd, 0, SEEK_SET) == (off_t)-1) { + fprintf(stderr, "ERROR: lseek time file: %s\n", strerror(errno)); + return -1; + } + + n = read(fd, buf, sizeof(buf) - 1); + if (n <= 0) { + fprintf(stderr, "ERROR: read time file: %s\n", strerror(errno)); + return -1; + } + + if (sscanf(buf, "%lf", elapsed) != 1) { + fprintf(stderr, "ERROR: cannot parse time '%s'\n", buf); + return -1; + } + + return 0; +} + +/* + * Reap one workload child and turn its exit code into a diagnostic. @pid is + * cleared on every path, including failure: once the child has been reaped its + * PID can be reused, and the error path signals whatever is left in it. + */ +static int reap_child(pid_t *pid, const char *who) +{ + int status; + + if (waitpid(*pid, &status, 0) <= 0) { + fprintf(stderr, "ERROR: waitpid %s: %s\n", who, strerror(errno)); + *pid = -1; + return -1; + } + *pid = -1; + + if (!WIFEXITED(status)) { + fprintf(stderr, "ERROR: %s child died, status 0x%x\n", who, status); + return -1; + } + if (WEXITSTATUS(status) != CHILD_EXIT_OK) { + fprintf(stderr, "ERROR: %s child failed at: %s (exit status %d)\n", + who, child_exit_str(WEXITSTATUS(status)), + WEXITSTATUS(status)); + return -1; + } + + return 0; +} + +/* + * Run the workload in the high and low priority cgroups at the same time and + * report how long each took. The high priority one is the cgroup the BPF + * program protects, the low priority one is what it reclaims from. + */ +static int run_workload(const char *dir, const struct options *o, + double *high_elapsed, double *low_elapsed) +{ + char high_data[PATH_MAX], low_data[PATH_MAX]; + char high_time[PATH_MAX], low_time[PATH_MAX]; + int high_data_fd = -1, low_data_fd = -1; + int high_time_fd = -1, low_time_fd = -1; + pid_t high_pid = -1, low_pid = -1; + int ret = -1; + + snprintf(high_data, sizeof(high_data), "%s/memcg_high_data_XXXXXX", dir); + snprintf(low_data, sizeof(low_data), "%s/memcg_low_data_XXXXXX", dir); + snprintf(high_time, sizeof(high_time), "%s/memcg_high_time_XXXXXX", dir); + snprintf(low_time, sizeof(low_time), "%s/memcg_low_time_XXXXXX", dir); + + /* + * The mkstemp() fds are kept and inherited by the children rather than + * reopened by name: reopening would resolve the path again and follow + * whatever sits there by then. + */ + high_data_fd = mkstemp(high_data); + low_data_fd = mkstemp(low_data); + high_time_fd = mkstemp(high_time); + low_time_fd = mkstemp(low_time); + if (high_data_fd < 0 || low_data_fd < 0 || high_time_fd < 0 || + low_time_fd < 0) { + fprintf(stderr, "ERROR: mkstemp: %s\n", strerror(errno)); + goto cleanup; + } + + low_pid = fork(); + if (low_pid < 0) { + fprintf(stderr, "ERROR: fork low: %s\n", strerror(errno)); + goto cleanup; + } + if (low_pid == 0) + _exit(child_work(BENCH_CG_LOW_DIR, low_data_fd, low_time_fd, o)); + + high_pid = fork(); + if (high_pid < 0) { + fprintf(stderr, "ERROR: fork high: %s\n", strerror(errno)); + goto cleanup; + } + if (high_pid == 0) + _exit(child_work(BENCH_CG_HIGH_DIR, high_data_fd, high_time_fd, o)); + + if (reap_child(&low_pid, "low")) + goto cleanup; + if (reap_child(&high_pid, "high")) + goto cleanup; + + if (get_time(high_time_fd, high_elapsed)) + goto cleanup; + if (get_time(low_time_fd, low_elapsed)) + goto cleanup; + + ret = 0; + +cleanup: + /* On failure, make sure no child process is left behind. */ + if (ret) { + if (high_pid > 0) { + kill(high_pid, SIGKILL); + waitpid(high_pid, NULL, 0); + } + if (low_pid > 0) { + kill(low_pid, SIGKILL); + waitpid(low_pid, NULL, 0); + } + } + if (high_data_fd >= 0) + close(high_data_fd); + if (low_data_fd >= 0) + close(low_data_fd); + if (high_time_fd >= 0) + close(high_time_fd); + if (low_time_fd >= 0) + close(low_time_fd); + unlink(high_data); + unlink(low_data); + unlink(high_time); + unlink(low_time); + + return ret; +} + +static void print_speedup(const char *who, double baseline, double measured) +{ + if (baseline <= 0.0) + return; + + printf("%s: baseline=%.3fs reclaim=%.3fs speedup=%+.1f%%\n", who, + baseline, measured, + 100.0 * (baseline - measured) / baseline); +} + +static int do_bench(struct options *o) +{ + double base_high = 0.0, base_low = 0.0; + double high = 0.0, low = 0.0; + struct session s = {}; + __u64 high_id, low_id; + const char *dir; + int ret = 1; + + dir = workload_files_dir(); + if (!dir) { + fprintf(stderr, "ERROR: neither /tmp nor the working directory is on a\n" + " regular filesystem; the workload needs one because\n" + " tmpfs pages are not reclaimable without swap.\n"); + return 1; + } + + if (setup_bench_cgroups(o, &high_id, &low_id)) + return 1; + + if (!o->no_baseline) { + printf("baseline run (no BPF program)...\n"); + if (run_workload(dir, o, &base_high, &base_low)) + goto out; + + /* + * The baseline left up to cg_limit of page cache charged to the + * two cgroups, and memcg charges outlive the workload processes. + * Recreate the cgroups so that the instrumented run starts from + * the same cold state; otherwise the reported speedup compares a + * cold run against a warm one. + */ + cleanup_cgroup_environment(); + if (setup_bench_cgroups(o, &high_id, &low_id)) + return 1; + } + + if (session_start(&s, o, high_id, low_id)) + goto out; + + printf("reclaim run (BPF program active)...\n"); + if (run_workload(dir, o, &high, &low)) + goto out; + + printf("\nworkload: file_size=%lu read_times=%lu memory.max=%lu\n", + o->file_size, o->read_times, o->cg_limit); + printf("high priority: %.3fs low priority: %.3fs\n", high, low); + if (!o->no_baseline) { + print_speedup("high", base_high, high); + print_speedup("low", base_low, low); + } + + ret = 0; + +out: + session_stop(&s); + cleanup_cgroup_environment(); + + return ret; +} + +/* watch mode code */ + +static int do_watch(struct options *o) +{ + struct session s = {}; + __u64 monitor_id, target_id; + double next_stats, deadline = 0; + int ret = 1; + + if (!o->monitor_path || !o->target_path) { + fprintf(stderr, "ERROR: watch needs both --monitor and --target\n"); + return 1; + } + + monitor_id = cgroup_id_from_path(o->monitor_path); + if (!monitor_id) { + fprintf(stderr, "ERROR: cannot resolve %s: %s\n", o->monitor_path, + strerror(errno)); + return 1; + } + if (!memcg_on_path(o->monitor_path)) { + fprintf(stderr, "ERROR: %s has no memory.current; the memory\n" + " controller is not enabled on that cgroup.\n", + o->monitor_path); + return 1; + } + + target_id = cgroup_id_from_path(o->target_path); + if (!target_id) { + fprintf(stderr, "ERROR: cannot resolve %s: %s\n", o->target_path, + strerror(errno)); + return 1; + } + if (!memcg_on_path(o->target_path)) { + fprintf(stderr, "ERROR: %s has no memory.current; the memory\n" + " controller is not enabled on that cgroup.\n", + o->target_path); + return 1; + } + + if (session_start(&s, o, monitor_id, target_id)) + return 1; + + signal(SIGINT, handle_signal); + signal(SIGTERM, handle_signal); + + printf("monitor: %s (id %llu)\n", o->monitor_path, + (unsigned long long)monitor_id); + printf("target: %s (id %llu)\n", o->target_path, + (unsigned long long)target_id); + printf("interval=%lums threshold=%lu batch=%lu max_batches=%lu swappiness=%ld\n", + o->interval_ms, o->threshold, o->batch_bytes, o->max_batches, + o->swappiness); + if (o->duration_sec) + printf("running for %lus, ", o->duration_sec); + printf("interrupt with SIGINT to stop\n\n"); + + next_stats = now_sec() + o->stats_sec; + if (o->duration_sec) + deadline = s.start + o->duration_sec; + + while (!READ_ONCE(exiting)) { + double now = now_sec(); + int wait_ms; + + if (deadline && now >= deadline) + break; + + wait_ms = (int)((next_stats - now) * 1000); + if (wait_ms < 0) + wait_ms = 0; + + if (session_poll(&s, wait_ms)) { + fprintf(stderr, "ERROR: ring buffer poll failed\n"); + goto out; + } + + if (now_sec() >= next_stats) { + char prefix[32]; + + snprintf(prefix, sizeof(prefix), "[%5.0fs]", + now_sec() - s.start); + print_counters(&s, prefix); + next_stats += o->stats_sec; + } + } + + ret = 0; + +out: + session_stop(&s); + + return ret; +} + +/* cli code */ + +static void usage(const char *prog) +{ + printf("Usage: %s <mode> [options]\n\n" + "Modes:\n" + " bench create a high/low priority cgroup pair, run a memory\n" + " pressured workload in both, and report the effect of\n" + " asynchronous reclaim on the pressured one\n" + " watch watch an existing cgroup and reclaim from another one\n" + " whenever it comes under pressure, until interrupted\n\n" + "Common options:\n" + " -i, --interval MS monitor tick (bench: %d, watch: %d)\n" + " -t, --threshold N refault delta per tick that starts a round (%d)\n" + " -b, --batch BYTES bytes requested per callback (%luK)\n" + " -n, --max-batches N callbacks per reclaim round (%d)\n" + " -S, --swappiness N -1 = the memcg's own, 0..200, 201 = anon\n" + " pages only (needs swap; the default) (%ld)\n" + " -v, --verbose print every reclaim event\n" + " -h, --help\n\n" + "bench options:\n" + " -l, --limit BYTES memory.max for the test cgroups (%luM)\n" + " -f, --file-size BYTES workload file size (%luM)\n" + " -R, --read-times N workload re-read iterations (%lu)\n" + " --no-baseline skip the run without the BPF program\n\n" + "watch options:\n" + " -m, --monitor PATH cgroup to watch (required)\n" + " -T, --target PATH cgroup to reclaim from (required)\n" + " -D, --duration SEC stop after SEC seconds instead of on signal\n" + " -s, --stats SEC print a statistics line every SEC seconds (%d)\n\n" + "BYTES accepts a K, M or G suffix. Both modes need root.\n\n" + "One call to bpf_proactive_reclaim() reclaims at most the kernel's\n" + "MEMCG_CHARGE_BATCH pages, so --batch above that limit only means a\n" + "round needs more callbacks, not that a single callback does more.\n", + prog, BENCH_INTERVAL_MS, WATCH_INTERVAL_MS, DEFAULT_THRESHOLD, + DEFAULT_BATCH_BYTES / 1024, DEFAULT_MAX_BATCHES, + DEFAULT_SWAPPINESS, DEFAULT_CG_LIMIT / (1024 * 1024), + DEFAULT_FILE_SIZE / (1024 * 1024), DEFAULT_READ_TIMES, + DEFAULT_STATS_SEC); +} + +enum { + OPT_NO_BASELINE = 256, +}; + +static int parse_options(int argc, char **argv, struct options *o) +{ + static const struct option long_opts[] = { + { "interval", required_argument, NULL, 'i' }, + { "threshold", required_argument, NULL, 't' }, + { "batch", required_argument, NULL, 'b' }, + { "max-batches", required_argument, NULL, 'n' }, + { "swappiness", required_argument, NULL, 'S' }, + { "verbose", no_argument, NULL, 'v' }, + { "help", no_argument, NULL, 'h' }, + { "limit", required_argument, NULL, 'l' }, + { "file-size", required_argument, NULL, 'f' }, + { "read-times", required_argument, NULL, 'R' }, + { "no-baseline", no_argument, NULL, OPT_NO_BASELINE }, + { "monitor", required_argument, NULL, 'm' }, + { "target", required_argument, NULL, 'T' }, + { "duration", required_argument, NULL, 'D' }, + { "stats", required_argument, NULL, 's' }, + { } + }; + int c; + + while ((c = getopt_long(argc, argv, "i:t:b:n:S:vhl:f:R:m:T:D:s:", + long_opts, NULL)) != -1) { + switch (c) { + case 'i': + if (parse_ulong(optarg, &o->interval_ms)) + return -1; + break; + case 't': + if (parse_ulong(optarg, &o->threshold)) + return -1; + break; + case 'b': + if (parse_size(optarg, &o->batch_bytes)) + return -1; + break; + case 'n': + if (parse_ulong(optarg, &o->max_batches)) + return -1; + break; + case 'S': + if (parse_long(optarg, &o->swappiness)) + return -1; + break; + case 'v': + o->verbose = true; + break; + case 'l': + if (parse_size(optarg, &o->cg_limit)) + return -1; + break; + case 'f': + if (parse_size(optarg, &o->file_size)) + return -1; + break; + case 'R': + if (parse_ulong(optarg, &o->read_times)) + return -1; + break; + case OPT_NO_BASELINE: + o->no_baseline = true; + break; + case 'm': + o->monitor_path = optarg; + break; + case 'T': + o->target_path = optarg; + break; + case 'D': + if (parse_ulong(optarg, &o->duration_sec)) + return -1; + break; + case 's': + if (parse_ulong(optarg, &o->stats_sec)) + return -1; + break; + default: + return -1; + } + } + + return 0; +} + +int main(int argc, char **argv) +{ + struct options o = { + .threshold = DEFAULT_THRESHOLD, + .batch_bytes = DEFAULT_BATCH_BYTES, + .max_batches = DEFAULT_MAX_BATCHES, + .swappiness = DEFAULT_SWAPPINESS, + .cg_limit = DEFAULT_CG_LIMIT, + .file_size = DEFAULT_FILE_SIZE, + .read_times = DEFAULT_READ_TIMES, + .stats_sec = DEFAULT_STATS_SEC, + }; + const char *mode; + bool bench; + + if (argc < 2) { + usage(argv[0]); + return 1; + } + + mode = argv[1]; + if (!strcmp(mode, "-h") || !strcmp(mode, "--help")) { + usage(argv[0]); + return 0; + } + + bench = !strcmp(mode, "bench"); + if (!bench && strcmp(mode, "watch")) { + fprintf(stderr, "ERROR: unknown mode '%s', expected bench or watch\n\n", + mode); + usage(argv[0]); + return 1; + } + + /* Let getopt_long see the options rather than the mode. */ + optind = 2; + if (parse_options(argc, argv, &o)) { + usage(argv[0]); + return 1; + } + + if (!o.interval_ms) + o.interval_ms = bench ? BENCH_INTERVAL_MS : WATCH_INTERVAL_MS; + if (!o.stats_sec) + o.stats_sec = DEFAULT_STATS_SEC; + if (!o.threshold || !o.batch_bytes || !o.max_batches) { + fprintf(stderr, "ERROR: --threshold, --batch and --max-batches must be non-zero\n"); + return 1; + } + if (o.swappiness != -1 && (o.swappiness < 0 || o.swappiness > 201)) { + fprintf(stderr, "ERROR: --swappiness must be -1, 0..200, or 201\n"); + return 1; + } + /* + * One round budgets max_batches * batch_bytes, and the BPF side keeps + * requeueing until it is spent, so bound the product: it also keeps the + * multiplication from wrapping. + */ + if (o.max_batches > (1UL << 30) / o.batch_bytes) { + fprintf(stderr, "ERROR: --max-batches * --batch must not exceed 1G\n"); + return 1; + } + + return bench ? do_bench(&o) : do_watch(&o); +} -- 2.43.0

