On 5/4/24 1:30 AM, Yuanchu Xie wrote:
A basic test that verifies the working set size of a simple memory accessor. It should work with or without the aging thread.
Question: I don't know how to best test file memory in selftests. Is there a place where I should put the temporary file? /tmp can be tmpfs mounted in many distros.
Signed-off-by: Yuanchu Xie yuanchu@google.com
tools/testing/selftests/mm/.gitignore | 1 + tools/testing/selftests/mm/Makefile | 3 + .../testing/selftests/mm/workingset_report.c | 317 +++++++++++++++++ .../testing/selftests/mm/workingset_report.h | 39 ++ .../selftests/mm/workingset_report_test.c | 332 ++++++++++++++++++
Add this test to run_vmtests.sh as all the tests are run from this script.
5 files changed, 692 insertions(+) create mode 100644 tools/testing/selftests/mm/workingset_report.c create mode 100644 tools/testing/selftests/mm/workingset_report.h create mode 100644 tools/testing/selftests/mm/workingset_report_test.c
diff --git a/tools/testing/selftests/mm/.gitignore b/tools/testing/selftests/mm/.gitignore index 4ff10ea61461..14a2412c8257 100644 --- a/tools/testing/selftests/mm/.gitignore +++ b/tools/testing/selftests/mm/.gitignore @@ -46,3 +46,4 @@ gup_longterm mkdirty va_high_addr_switch hugetlb_fault_after_madv +workingset_report_test diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/mm/Makefile index 2453add65d12..c0869bf07e99 100644 --- a/tools/testing/selftests/mm/Makefile +++ b/tools/testing/selftests/mm/Makefile @@ -70,6 +70,7 @@ TEST_GEN_FILES += ksm_tests TEST_GEN_FILES += ksm_functional_tests TEST_GEN_FILES += mdwe_test TEST_GEN_FILES += hugetlb_fault_after_madv +TEST_GEN_FILES += workingset_report_test ifneq ($(ARCH),arm64) TEST_GEN_FILES += soft-dirty @@ -123,6 +124,8 @@ $(TEST_GEN_FILES): vm_util.c thp_settings.c $(OUTPUT)/uffd-stress: uffd-common.c $(OUTPUT)/uffd-unit-tests: uffd-common.c +$(OUTPUT)/workingset_report_test: workingset_report.c
ifeq ($(ARCH),x86_64) BINARIES_32 := $(patsubst %,$(OUTPUT)/%,$(BINARIES_32)) BINARIES_64 := $(patsubst %,$(OUTPUT)/%,$(BINARIES_64)) diff --git a/tools/testing/selftests/mm/workingset_report.c b/tools/testing/selftests/mm/workingset_report.c new file mode 100644 index 000000000000..0d744bae5432 --- /dev/null +++ b/tools/testing/selftests/mm/workingset_report.c @@ -0,0 +1,317 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "workingset_report.h"
+#include <stddef.h> +#include <stdlib.h> +#include <stdio.h> +#include <stdbool.h> +#include <unistd.h> +#include <string.h> +#include <sys/mman.h> +#include <sys/wait.h>
+#include "../kselftest.h"
+#define SYSFS_NODE_ONLINE "/sys/devices/system/node/online" +#define PROC_DROP_CACHES "/proc/sys/vm/drop_caches"
+/* Returns read len on success, or -errno on failure. */ +static ssize_t read_text(const char *path, char *buf, size_t max_len) +{
- ssize_t len;
- int fd, err;
- size_t bytes_read = 0;
- if (!max_len)
return -EINVAL;
- fd = open(path, O_RDONLY);
- if (fd < 0)
return -errno;
- while (bytes_read < max_len - 1) {
len = read(fd, buf + bytes_read, max_len - 1 - bytes_read);
if (len <= 0)
break;
bytes_read += len;
- }
- buf[bytes_read] = '\0';
- err = -errno;
- close(fd);
- return len < 0 ? err : bytes_read;
+}
+/* Returns written len on success, or -errno on failure. */ +static ssize_t write_text(const char *path, const char *buf, ssize_t max_len) +{
- int fd, len, err;
- size_t bytes_written = 0;
- fd = open(path, O_WRONLY | O_APPEND);
- if (fd < 0)
return -errno;
- while (bytes_written < max_len) {
len = write(fd, buf + bytes_written, max_len - bytes_written);
if (len < 0)
break;
bytes_written += len;
- }
- err = -errno;
- close(fd);
- return len < 0 ? err : bytes_written;
+}
+static long read_num(const char *path) +{
- char buf[21];
- if (read_text(path, buf, sizeof(buf)) <= 0)
return -1;
- return (long)strtoul(buf, NULL, 10);
+}
+static int write_num(const char *path, unsigned long n) +{
- char buf[21];
- sprintf(buf, "%lu", n);
- if (write_text(path, buf, strlen(buf)) < 0)
return -1;
- return 0;
+}
+long sysfs_get_refresh_interval(int nid) +{
- char file[128];
- snprintf(
file,
sizeof(file),
"/sys/devices/system/node/node%d/workingset_report/refresh_interval",
nid);
- return read_num(file);
+}
+int sysfs_set_refresh_interval(int nid, long interval) +{
- char file[128];
- snprintf(
file,
sizeof(file),
"/sys/devices/system/node/node%d/workingset_report/refresh_interval",
nid);
- return write_num(file, interval);
+}
+int sysfs_get_page_age_intervals_str(int nid, char *buf, int len) +{
- char path[128];
- snprintf(
path,
sizeof(path),
"/sys/devices/system/node/node%d/workingset_report/page_age_intervals",
nid);
Not sure if checkpatch reports this or not, write arguments directly after the snprintf instead of using so many spaces.
- return read_text(path, buf, len);
+}
+int sysfs_set_page_age_intervals_str(int nid, const char *buf, int len) +{
- char path[128];
- snprintf(
path,
sizeof(path),
"/sys/devices/system/node/node%d/workingset_report/page_age_intervals",
nid);
- return write_text(path, buf, len);
+}
+int sysfs_set_page_age_intervals(int nid, const char *const intervals[],
int nr_intervals)
+{
- char file[128];
- char buf[1024];
- int i;
- int err, len = 0;
- for (i = 0; i < nr_intervals; ++i) {
err = snprintf(buf + len, sizeof(buf) - len, "%s", intervals[i]);
if (err < 0)
return err;
len += err;
if (i < nr_intervals - 1) {
err = snprintf(buf + len, sizeof(buf) - len, ",");
if (err < 0)
return err;
len += err;
}
- }
- snprintf(
file,
sizeof(file),
"/sys/devices/system/node/node%d/workingset_report/page_age_intervals",
nid);
- return write_text(file, buf, len);
+}
+int get_nr_nodes(void) +{
- char buf[22];
- char *found;
- if (read_text(SYSFS_NODE_ONLINE, buf, sizeof(buf)) <= 0)
return -1;
- found = strstr(buf, "-");
- if (found)
return (int)strtoul(found + 1, NULL, 10) + 1;
- return (long)strtoul(buf, NULL, 10) + 1;
+}
+int drop_pagecache(void) +{
- return write_num(PROC_DROP_CACHES, 1);
+}
+ssize_t sysfs_page_age_read(int nid, char *buf, size_t len)
+{
- char file[128];
- snprintf(file,
sizeof(file),
"/sys/devices/system/node/node%d/workingset_report/page_age",
nid);
- return read_text(file, buf, len);
+}
+/*
- Finds the first occurrence of "N<nid>\n"
- Modifies buf to terminate before the next occurrence of "N".
- Returns a substring of buf starting after "N<nid>\n"
- */
+char *page_age_split_node(char *buf, int nid, char **next) +{
- char node_str[5];
- char *found;
- int node_str_len;
- node_str_len = snprintf(node_str, sizeof(node_str), "N%u\n", nid);
- /* find the node prefix first */
- found = strstr(buf, node_str);
- if (!found) {
ksft_print_msg("cannot find '%s' in page_idle_age", node_str);
return NULL;
- }
- found += node_str_len;
- *next = strchr(found, 'N');
- if (*next)
*(*next - 1) = '\0';
- return found;
+}
+ssize_t page_age_read(const char *buf, const char *interval, int pagetype) +{
- static const char * const type[ANON_AND_FILE] = { "anon=", "file=" };
- char *found;
- found = strstr(buf, interval);
- if (!found) {
ksft_print_msg("cannot find %s in page_age", interval);
return -1;
- }
- found = strstr(found, type[pagetype]);
- if (!found) {
ksft_print_msg("cannot find %s in page_age", type[pagetype]);
return -1;
- }
- found += strlen(type[pagetype]);
- return (long)strtoul(found, NULL, 10);
+}
+static const char *TEMP_FILE = "/tmp/workingset_selftest"; +void cleanup_file_workingset(void) +{
- remove(TEMP_FILE);
+}
+int alloc_file_workingset(void *arg) +{
- int err = 0;
- char *ptr;
- int fd;
- int ppid;
- char *mapped;
- size_t size = (size_t)arg;
- size_t page_size = getpagesize();
- ppid = getppid();
- fd = open(TEMP_FILE, O_RDWR | O_CREAT);
- if (fd < 0) {
err = -errno;
ksft_perror("failed to open temp file\n");
goto cleanup;
- }
- if (fallocate(fd, 0, 0, size) < 0) {
err = -errno;
ksft_perror("fallocate");
goto cleanup;
- }
- mapped = (char *)mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED,
fd, 0);
- if (mapped == NULL) {
err = -errno;
ksft_perror("mmap");
goto cleanup;
- }
- while (getppid() == ppid) {
sync();
for (ptr = mapped; ptr < mapped + size; ptr += page_size)
*ptr = *ptr ^ 0xFF;
- }
+cleanup:
- cleanup_file_workingset();
- return err;
+}
+int alloc_anon_workingset(void *arg) +{
- char *buf, *ptr;
- int ppid = getppid();
- size_t size = (size_t)arg;
- size_t page_size = getpagesize();
- buf = malloc(size);
- if (!buf) {
ksft_print_msg("cannot allocate anon workingset");
exit(1);
- }
- while (getppid() == ppid) {
for (ptr = buf; ptr < buf + size; ptr += page_size)
*ptr = *ptr ^ 0xFF;
- }
- free(buf);
- return 0;
+} diff --git a/tools/testing/selftests/mm/workingset_report.h b/tools/testing/selftests/mm/workingset_report.h new file mode 100644 index 000000000000..c5c281e4069b --- /dev/null +++ b/tools/testing/selftests/mm/workingset_report.h @@ -0,0 +1,39 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +#ifndef WORKINGSET_REPORT_H_ +#define WORKINGSET_REPORT_H_
+#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif
+#include <fcntl.h> +#include <sys/stat.h> +#include <errno.h> +#include <stdint.h> +#include <sys/types.h>
+#define PAGETYPE_ANON 0 +#define PAGETYPE_FILE 1 +#define ANON_AND_FILE 2
+int get_nr_nodes(void); +int drop_pagecache(void);
+long sysfs_get_refresh_interval(int nid); +int sysfs_set_refresh_interval(int nid, long interval);
+int sysfs_get_page_age_intervals_str(int nid, char *buf, int len); +int sysfs_set_page_age_intervals_str(int nid, const char *buf, int len);
+int sysfs_set_page_age_intervals(int nid, const char *const intervals[],
int nr_intervals);
+char *page_age_split_node(char *buf, int nid, char **next); +ssize_t sysfs_page_age_read(int nid, char *buf, size_t len); +ssize_t page_age_read(const char *buf, const char *interval, int pagetype);
+int alloc_file_workingset(void *arg); +void cleanup_file_workingset(void); +int alloc_anon_workingset(void *arg);
+#endif /* WORKINGSET_REPORT_H_ */ diff --git a/tools/testing/selftests/mm/workingset_report_test.c b/tools/testing/selftests/mm/workingset_report_test.c new file mode 100644 index 000000000000..9a86c2215182 --- /dev/null +++ b/tools/testing/selftests/mm/workingset_report_test.c @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: GPL-2.0 +#include "workingset_report.h"
+#include <stdlib.h> +#include <stdio.h> +#include <signal.h> +#include <time.h>
+#include "../clone3/clone3_selftests.h"
+#define REFRESH_INTERVAL 5000 +#define MB(x) (x << 20)
+static void sleep_ms(int milliseconds) +{
- struct timespec ts;
- ts.tv_sec = milliseconds / 1000;
- ts.tv_nsec = (milliseconds % 1000) * 1000000;
- nanosleep(&ts, NULL);
+}
+/*
- Checks if two given values differ by less than err% of their sum.
- */
+static inline int values_close(long a, long b, int err) +{
- return labs(a - b) <= (a + b) / 100 * err;
+}
+static const char * const PAGE_AGE_INTERVALS[] = {
- "6000", "10000", "15000", "18446744073709551615",
+}; +#define NR_PAGE_AGE_INTERVALS (ARRAY_SIZE(PAGE_AGE_INTERVALS))
+static int set_page_age_intervals_all_nodes(const char *intervals, int nr_nodes) +{
- int i;
- for (i = 0; i < nr_nodes; ++i) {
int err = sysfs_set_page_age_intervals_str(
i, &intervals[i * 1024], strlen(&intervals[i * 1024]));
if (err < 0)
return err;
- }
- return 0;
+}
+static int get_page_age_intervals_all_nodes(char *intervals, int nr_nodes) +{
- int i;
- for (i = 0; i < nr_nodes; ++i) {
int err = sysfs_get_page_age_intervals_str(
i, &intervals[i * 1024], 1024);
if (err < 0)
return err;
- }
- return 0;
+}
+static int set_refresh_interval_all_nodes(const long *interval, int nr_nodes) +{
- int i;
- for (i = 0; i < nr_nodes; ++i) {
int err = sysfs_set_refresh_interval(i, interval[i]);
if (err < 0)
return err;
- }
- return 0;
+}
+static int get_refresh_interval_all_nodes(long *interval, int nr_nodes) +{
- int i;
- for (i = 0; i < nr_nodes; ++i) {
long val = sysfs_get_refresh_interval(i);
if (val < 0)
return val;
interval[i] = val;
- }
- return 0;
+}
+static pid_t clone_and_run(int fn(void *arg), void *arg) +{
- pid_t pid;
- struct __clone_args args = {
.exit_signal = SIGCHLD,
- };
- pid = sys_clone3(&args, sizeof(struct __clone_args));
- if (pid == 0)
exit(fn(arg));
- return pid;
+}
+static int read_workingset(int pagetype, int nid,
unsigned long page_age[NR_PAGE_AGE_INTERVALS])
+{
- int i, err;
- char buf[4096];
- err = sysfs_page_age_read(nid, buf, sizeof(buf));
- if (err < 0)
return err;
- for (i = 0; i < NR_PAGE_AGE_INTERVALS; ++i) {
err = page_age_read(buf, PAGE_AGE_INTERVALS[i], pagetype);
if (err < 0)
return err;
page_age[i] = err;
- }
- return 0;
+}
+static ssize_t read_interval_all_nodes(int pagetype, int interval) +{
- int i, err;
- unsigned long page_age[NR_PAGE_AGE_INTERVALS];
- ssize_t ret = 0;
- int nr_nodes = get_nr_nodes();
- for (i = 0; i < nr_nodes; ++i) {
err = read_workingset(pagetype, i, page_age);
if (err < 0)
return err;
ret += page_age[interval];
- }
- return ret;
+}
+#define TEST_SIZE MB(500l)
+static int run_test(int f(void)) +{
- int i, err, test_result;
- long *old_refresh_intervals;
- long *new_refresh_intervals;
- char *old_page_age_intervals;
- int nr_nodes = get_nr_nodes();
- if (nr_nodes <= 0) {
ksft_print_msg("failed to get nr_nodes\n");
return KSFT_FAIL;
- }
- old_refresh_intervals = calloc(nr_nodes, sizeof(long));
- new_refresh_intervals = calloc(nr_nodes, sizeof(long));
- old_page_age_intervals = calloc(nr_nodes, 1024);
- if (!(old_refresh_intervals && new_refresh_intervals &&
old_page_age_intervals)) {
ksft_print_msg("failed to allocate memory for intervals\n");
return KSFT_FAIL;
- }
- err = get_refresh_interval_all_nodes(old_refresh_intervals, nr_nodes);
- if (err < 0) {
ksft_print_msg("failed to read refresh interval\n");
return KSFT_FAIL;
- }
- err = get_page_age_intervals_all_nodes(old_page_age_intervals, nr_nodes);
- if (err < 0) {
ksft_print_msg("failed to read page age interval\n");
return KSFT_FAIL;
- }
- for (i = 0; i < nr_nodes; ++i)
new_refresh_intervals[i] = REFRESH_INTERVAL;
- for (i = 0; i < nr_nodes; ++i) {
err = sysfs_set_page_age_intervals(i, PAGE_AGE_INTERVALS,
NR_PAGE_AGE_INTERVALS - 1);
if (err < 0) {
ksft_print_msg("failed to set page age interval\n");
test_result = KSFT_FAIL;
goto fail;
}
- }
- err = set_refresh_interval_all_nodes(new_refresh_intervals, nr_nodes);
- if (err < 0) {
ksft_print_msg("failed to set refresh interval\n");
test_result = KSFT_FAIL;
goto fail;
- }
- sync();
- drop_pagecache();
- test_result = f();
+fail:
- err = set_refresh_interval_all_nodes(old_refresh_intervals, nr_nodes);
- if (err < 0) {
ksft_print_msg("failed to restore refresh interval\n");
test_result = KSFT_FAIL;
- }
- err = set_page_age_intervals_all_nodes(old_page_age_intervals, nr_nodes);
- if (err < 0) {
ksft_print_msg("failed to restore page age interval\n");
test_result = KSFT_FAIL;
- }
- return test_result;
+}
+static int test_file(void) +{
- ssize_t ws_size_ref, ws_size_test;
- int ret = KSFT_FAIL, i;
- pid_t pid = 0;
- ws_size_ref = read_interval_all_nodes(PAGETYPE_FILE, 0);
- if (ws_size_ref < 0)
goto cleanup;
- pid = clone_and_run(alloc_file_workingset, (void *)TEST_SIZE);
- if (pid < 0)
goto cleanup;
- read_interval_all_nodes(PAGETYPE_FILE, 0);
- sleep_ms(REFRESH_INTERVAL);
- for (i = 0; i < 3; ++i) {
sleep_ms(REFRESH_INTERVAL);
ws_size_test = read_interval_all_nodes(PAGETYPE_FILE, 0);
ws_size_test += read_interval_all_nodes(PAGETYPE_FILE, 1);
if (ws_size_test < 0)
goto cleanup;
if (!values_close(ws_size_test - ws_size_ref, TEST_SIZE, 10)) {
ksft_print_msg(
"file working set size difference too large: actual=%ld, expected=%ld\n",
ws_size_test - ws_size_ref, TEST_SIZE);
goto cleanup;
}
- }
- ret = KSFT_PASS;
+cleanup:
- if (pid > 0)
kill(pid, SIGKILL);
- cleanup_file_workingset();
- return ret;
+}
+static int test_anon(void) +{
- ssize_t ws_size_ref, ws_size_test;
- pid_t pid = 0;
- int ret = KSFT_FAIL, i;
- ws_size_ref = read_interval_all_nodes(PAGETYPE_ANON, 0);
- if (ws_size_ref < 0)
goto cleanup;
- pid = clone_and_run(alloc_anon_workingset, (void *)TEST_SIZE);
- if (pid < 0)
goto cleanup;
- sleep_ms(REFRESH_INTERVAL);
- read_interval_all_nodes(PAGETYPE_ANON, 0);
- for (i = 0; i < 5; ++i) {
sleep_ms(REFRESH_INTERVAL);
ws_size_test = read_interval_all_nodes(PAGETYPE_ANON, 0);
ws_size_test += read_interval_all_nodes(PAGETYPE_ANON, 1);
if (ws_size_test < 0)
goto cleanup;
if (!values_close(ws_size_test - ws_size_ref, TEST_SIZE, 10)) {
ksft_print_msg(
"anon working set size difference too large: actual=%ld, expected=%ld\n",
ws_size_test - ws_size_ref, TEST_SIZE);
goto cleanup;
}
- }
- ret = KSFT_PASS;
+cleanup:
- if (pid > 0)
kill(pid, SIGKILL);
- return ret;
+}
+#define T(x) { x, #x } +struct workingset_test {
- int (*fn)(void);
- const char *name;
+} tests[] = {
- T(test_anon),
- T(test_file),
+}; +#undef T
+int main(int argc, char **argv) +{
- int ret = EXIT_SUCCESS, i, err;
- for (i = 0; i < ARRAY_SIZE(tests); i++) {
err = run_test(tests[i].fn);
switch (err) {
case KSFT_PASS:
ksft_test_result_pass("%s\n", tests[i].name);
break;
case KSFT_SKIP:
ksft_test_result_skip("%s\n", tests[i].name);
break;
default:
ret = EXIT_FAILURE;
ksft_test_result_fail("%s with error %d\n",
tests[i].name, err);
break;
}
Replace this switch-case with ksft_test_result*().
- }
- return ret;
+}