]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
sched_ext: Add a selftest for scx_bpf_dsq_peek
authorRyan Newton <newton@meta.com>
Wed, 15 Oct 2025 15:50:36 +0000 (11:50 -0400)
committerTejun Heo <tj@kernel.org>
Wed, 15 Oct 2025 16:46:36 +0000 (06:46 -1000)
This commit adds two tests. The first is the most basic unit test:
make sure an empty queue peeks as empty, and when we put one element
in the queue, make sure peek returns that element.

However, even this simple test is a little complicated by the different
behavior of scx_bpf_dsq_insert in different calling contexts:
 - insert is for direct dispatch in enqueue
 - insert is delayed when called from select_cpu

In this case we split the insert and the peek that verifies the
result between enqueue/dispatch.

Note: An alternative would be to call `scx_bpf_dsq_move_to_local` on an
empty queue, which in turn calls `flush_dispatch_buf`, in order to flush
the buffered insert. Unfortunately, this is not viable within the
enqueue path, as it attempts a voluntary context switch within an RCU
read-side critical section.

The second test is a stress test that performs many peeks on all DSQs
and records the observed tasks.

Signed-off-by: Ryan Newton <newton@meta.com>
Reviewed-by: Christian Loehle <christian.loehle@arm.com>
Signed-off-by: Tejun Heo <tj@kernel.org>
tools/testing/selftests/sched_ext/Makefile
tools/testing/selftests/sched_ext/peek_dsq.bpf.c [new file with mode: 0644]
tools/testing/selftests/sched_ext/peek_dsq.c [new file with mode: 0644]

index 9d9d6b4c38b015cc5c2b051150823bd347ab015a..5fe45f9c5f8fdb3cb62f91c7920fa3d591ecd63b 100644 (file)
@@ -174,6 +174,7 @@ auto-test-targets :=                        \
        minimal                         \
        numa                            \
        allowed_cpus                    \
+       peek_dsq                        \
        prog_run                        \
        reload_loop                     \
        select_cpu_dfl                  \
diff --git a/tools/testing/selftests/sched_ext/peek_dsq.bpf.c b/tools/testing/selftests/sched_ext/peek_dsq.bpf.c
new file mode 100644 (file)
index 0000000..a3faf5b
--- /dev/null
@@ -0,0 +1,251 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * A BPF program for testing DSQ operations and peek in particular.
+ *
+ * Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu>
+ */
+
+#include <scx/common.bpf.h>
+#include <scx/compat.bpf.h>
+
+char _license[] SEC("license") = "GPL";
+
+UEI_DEFINE(uei); /* Error handling */
+
+#define MAX_SAMPLES 100
+#define MAX_CPUS 512
+#define DSQ_POOL_SIZE 8
+int max_samples = MAX_SAMPLES;
+int max_cpus = MAX_CPUS;
+int dsq_pool_size = DSQ_POOL_SIZE;
+
+/* Global variables to store test results */
+int dsq_peek_result1 = -1;
+long dsq_inserted_pid = -1;
+int insert_test_cpu = -1; /* Set to the cpu that performs the test */
+long dsq_peek_result2 = -1;
+long dsq_peek_result2_pid = -1;
+long dsq_peek_result2_expected = -1;
+int test_dsq_id = 1234; /* Use a simple ID like create_dsq example */
+int real_dsq_id = 1235; /* DSQ for normal operation */
+int enqueue_count = -1;
+int dispatch_count = -1;
+bool debug_ksym_exists;
+
+/* DSQ pool for stress testing */
+int dsq_pool_base_id = 2000;
+int phase1_complete = -1;
+long total_peek_attempts = -1;
+long successful_peeks = -1;
+
+/* BPF map for sharing peek results with userspace */
+struct {
+       __uint(type, BPF_MAP_TYPE_ARRAY);
+       __uint(max_entries, MAX_SAMPLES);
+       __type(key, u32);
+       __type(value, long);
+} peek_results SEC(".maps");
+
+static int get_random_dsq_id(void)
+{
+       u64 time = bpf_ktime_get_ns();
+
+       return dsq_pool_base_id + (time % DSQ_POOL_SIZE);
+}
+
+static void record_peek_result(long pid)
+{
+       u32 slot_key;
+       long *slot_pid_ptr;
+       int ix;
+
+       if (pid <= 0)
+               return;
+
+       /* Find an empty slot or one with the same PID */
+       bpf_for(ix, 0, 10) {
+               slot_key = (pid + ix) % MAX_SAMPLES;
+               slot_pid_ptr = bpf_map_lookup_elem(&peek_results, &slot_key);
+               if (!slot_pid_ptr)
+                       continue;
+
+               if (*slot_pid_ptr == -1 || *slot_pid_ptr == pid) {
+                       *slot_pid_ptr = pid;
+                       break;
+               }
+       }
+}
+
+/* Scan all DSQs in the pool and try to move a task to local */
+static int scan_dsq_pool(void)
+{
+       struct task_struct *task;
+       int moved = 0;
+       int i;
+
+       bpf_for(i, 0, DSQ_POOL_SIZE) {
+               int dsq_id = dsq_pool_base_id + i;
+
+               total_peek_attempts++;
+
+               task = __COMPAT_scx_bpf_dsq_peek(dsq_id);
+               if (task) {
+                       successful_peeks++;
+                       record_peek_result(task->pid);
+
+                       /* Try to move this task to local */
+                       if (!moved && scx_bpf_dsq_move_to_local(dsq_id) == 0) {
+                               moved = 1;
+                               break;
+                       }
+               }
+       }
+       return moved;
+}
+
+/* Struct_ops scheduler for testing DSQ peek operations */
+void BPF_STRUCT_OPS(peek_dsq_enqueue, struct task_struct *p, u64 enq_flags)
+{
+       struct task_struct *peek_result;
+       int last_insert_test_cpu, cpu;
+
+       enqueue_count++;
+       cpu = bpf_get_smp_processor_id();
+       last_insert_test_cpu = __sync_val_compare_and_swap(&insert_test_cpu, -1, cpu);
+
+       /* Phase 1: Simple insert-then-peek test (only on first task) */
+       if (last_insert_test_cpu == -1) {
+               bpf_printk("peek_dsq_enqueue beginning phase 1 peek test on cpu %d", cpu);
+
+               /* Test 1: Peek empty DSQ - should return NULL */
+               peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id);
+               dsq_peek_result1 = (long)peek_result; /* Should be 0 (NULL) */
+
+               /* Test 2: Insert task into test DSQ for testing in dispatch callback */
+               dsq_inserted_pid = p->pid;
+               scx_bpf_dsq_insert(p, test_dsq_id, 0, enq_flags);
+               dsq_peek_result2_expected = (long)p; /* Expected the task we just inserted */
+       } else if (!phase1_complete) {
+               /* Still in phase 1, use real DSQ */
+               scx_bpf_dsq_insert(p, real_dsq_id, 0, enq_flags);
+       } else {
+               /* Phase 2: Random DSQ insertion for stress testing */
+               int random_dsq_id = get_random_dsq_id();
+
+               scx_bpf_dsq_insert(p, random_dsq_id, 0, enq_flags);
+       }
+}
+
+void BPF_STRUCT_OPS(peek_dsq_dispatch, s32 cpu, struct task_struct *prev)
+{
+       dispatch_count++;
+
+       /* Phase 1: Complete the simple peek test if we inserted a task but
+        * haven't tested peek yet
+        */
+       if (insert_test_cpu == cpu && dsq_peek_result2 == -1) {
+               struct task_struct *peek_result;
+
+               bpf_printk("peek_dsq_dispatch completing phase 1 peek test on cpu %d", cpu);
+
+               /* Test 3: Peek DSQ after insert - should return the task we inserted */
+               peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id);
+               /* Store the PID of the peeked task for comparison */
+               dsq_peek_result2 = (long)peek_result;
+               dsq_peek_result2_pid = peek_result ? peek_result->pid : -1;
+
+               /* Now consume the task since we've peeked at it */
+               scx_bpf_dsq_move_to_local(test_dsq_id);
+
+               /* Mark phase 1 as complete */
+               phase1_complete = 1;
+               bpf_printk("Phase 1 complete, starting phase 2 stress testing");
+       } else if (!phase1_complete) {
+               /* Still in phase 1, use real DSQ */
+               scx_bpf_dsq_move_to_local(real_dsq_id);
+       } else {
+               /* Phase 2: Scan all DSQs in the pool and try to move a task */
+               if (!scan_dsq_pool()) {
+                       /* No tasks found in DSQ pool, fall back to real DSQ */
+                       scx_bpf_dsq_move_to_local(real_dsq_id);
+               }
+       }
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(peek_dsq_init)
+{
+       s32 err;
+       int i;
+
+       /* Always set debug values so we can see which version we're using */
+       debug_ksym_exists = bpf_ksym_exists(scx_bpf_dsq_peek) ? 1 : 0;
+
+       /* Initialize state first */
+       insert_test_cpu = -1;
+       enqueue_count = 0;
+       dispatch_count = 0;
+       phase1_complete = 0;
+       total_peek_attempts = 0;
+       successful_peeks = 0;
+
+       /* Create the test and real DSQs */
+       err = scx_bpf_create_dsq(test_dsq_id, -1);
+       if (err) {
+               scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err);
+               return err;
+       }
+       err = scx_bpf_create_dsq(real_dsq_id, -1);
+       if (err) {
+               scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err);
+               return err;
+       }
+
+       /* Create the DSQ pool for stress testing */
+       bpf_for(i, 0, DSQ_POOL_SIZE) {
+               int dsq_id = dsq_pool_base_id + i;
+
+               err = scx_bpf_create_dsq(dsq_id, -1);
+               if (err) {
+                       scx_bpf_error("Failed to create DSQ pool entry %d: %d", dsq_id, err);
+                       return err;
+               }
+       }
+
+       /* Initialize the peek results map */
+       bpf_for(i, 0, MAX_SAMPLES) {
+               u32 key = i;
+               long pid = -1;
+
+               bpf_map_update_elem(&peek_results, &key, &pid, BPF_ANY);
+       }
+
+       return 0;
+}
+
+void BPF_STRUCT_OPS(peek_dsq_exit, struct scx_exit_info *ei)
+{
+       int i;
+
+       /* Destroy the primary DSQs */
+       scx_bpf_destroy_dsq(test_dsq_id);
+       scx_bpf_destroy_dsq(real_dsq_id);
+
+       /* Destroy the DSQ pool */
+       bpf_for(i, 0, DSQ_POOL_SIZE) {
+               int dsq_id = dsq_pool_base_id + i;
+
+               scx_bpf_destroy_dsq(dsq_id);
+       }
+
+       UEI_RECORD(uei, ei);
+}
+
+SEC(".struct_ops.link")
+struct sched_ext_ops peek_dsq_ops = {
+       .enqueue = (void *)peek_dsq_enqueue,
+       .dispatch = (void *)peek_dsq_dispatch,
+       .init = (void *)peek_dsq_init,
+       .exit = (void *)peek_dsq_exit,
+       .name = "peek_dsq",
+};
diff --git a/tools/testing/selftests/sched_ext/peek_dsq.c b/tools/testing/selftests/sched_ext/peek_dsq.c
new file mode 100644 (file)
index 0000000..a717384
--- /dev/null
@@ -0,0 +1,224 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Test for DSQ operations including create, destroy, and peek operations.
+ *
+ * Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu>
+ */
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <string.h>
+#include <sched.h>
+#include "peek_dsq.bpf.skel.h"
+#include "scx_test.h"
+
+#define NUM_WORKERS 4
+
+static bool workload_running = true;
+static pthread_t workload_threads[NUM_WORKERS];
+
+/**
+ * Background workload thread that sleeps and wakes rapidly to exercise
+ * the scheduler's enqueue operations and ensure DSQ operations get tested.
+ */
+static void *workload_thread_fn(void *arg)
+{
+       while (workload_running) {
+               /* Sleep for a very short time to trigger scheduler activity */
+               usleep(1000); /* 1ms sleep */
+               /* Yield to ensure we go through the scheduler */
+               sched_yield();
+       }
+       return NULL;
+}
+
+static enum scx_test_status setup(void **ctx)
+{
+       struct peek_dsq *skel;
+
+       skel = peek_dsq__open();
+       SCX_FAIL_IF(!skel, "Failed to open");
+       SCX_ENUM_INIT(skel);
+       SCX_FAIL_IF(peek_dsq__load(skel), "Failed to load skel");
+
+       *ctx = skel;
+
+       return SCX_TEST_PASS;
+}
+
+static int print_observed_pids(struct bpf_map *map, int max_samples, const char *dsq_name)
+{
+       long count = 0;
+
+       printf("Observed %s DSQ peek pids:\n", dsq_name);
+       for (int i = 0; i < max_samples; i++) {
+               long pid;
+               int err;
+
+               err = bpf_map_lookup_elem(bpf_map__fd(map), &i, &pid);
+               if (err == 0) {
+                       if (pid == 0) {
+                               printf("  Sample %d: NULL peek\n", i);
+                       } else if (pid > 0) {
+                               printf("  Sample %d: pid %ld\n", i, pid);
+                               count++;
+                       }
+               } else {
+                       printf("  Sample %d: error reading pid (err=%d)\n", i, err);
+               }
+       }
+       printf("Observed ~%ld pids in the %s DSQ(s)\n", count, dsq_name);
+       return count;
+}
+
+static enum scx_test_status run(void *ctx)
+{
+       struct peek_dsq *skel = ctx;
+       bool failed = false;
+       int seconds = 3;
+       int err;
+
+       /* Enable the scheduler to test DSQ operations */
+       printf("Enabling scheduler to test DSQ insert operations...\n");
+
+       struct bpf_link *link =
+               bpf_map__attach_struct_ops(skel->maps.peek_dsq_ops);
+
+       if (!link) {
+               SCX_ERR("Failed to attach struct_ops");
+               return SCX_TEST_FAIL;
+       }
+
+       printf("Starting %d background workload threads...\n", NUM_WORKERS);
+       workload_running = true;
+       for (int i = 0; i < NUM_WORKERS; i++) {
+               err = pthread_create(&workload_threads[i], NULL, workload_thread_fn, NULL);
+               if (err) {
+                       SCX_ERR("Failed to create workload thread %d: %s", i, strerror(err));
+                       /* Stop already created threads */
+                       workload_running = false;
+                       for (int j = 0; j < i; j++)
+                               pthread_join(workload_threads[j], NULL);
+                       bpf_link__destroy(link);
+                       return SCX_TEST_FAIL;
+               }
+       }
+
+       printf("Waiting for enqueue events.\n");
+       sleep(seconds);
+       while (skel->data->enqueue_count <= 0) {
+               printf(".");
+               fflush(stdout);
+               sleep(1);
+               seconds++;
+               if (seconds >= 30) {
+                       printf("\n\u2717 Timeout waiting for enqueue events\n");
+                       /* Stop workload threads and cleanup */
+                       workload_running = false;
+                       for (int i = 0; i < NUM_WORKERS; i++)
+                               pthread_join(workload_threads[i], NULL);
+                       bpf_link__destroy(link);
+                       return SCX_TEST_FAIL;
+               }
+       }
+
+       workload_running = false;
+       for (int i = 0; i < NUM_WORKERS; i++) {
+               err = pthread_join(workload_threads[i], NULL);
+               if (err) {
+                       SCX_ERR("Failed to join workload thread %d: %s", i, strerror(err));
+                       bpf_link__destroy(link);
+                       return SCX_TEST_FAIL;
+               }
+       }
+       printf("Background workload threads stopped.\n");
+
+       SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE));
+
+       /* Detach the scheduler */
+       bpf_link__destroy(link);
+
+       printf("Enqueue/dispatch count over %d seconds: %d / %d\n", seconds,
+               skel->data->enqueue_count, skel->data->dispatch_count);
+       printf("Debug: ksym_exists=%d\n",
+              skel->bss->debug_ksym_exists);
+
+       /* Check DSQ insert result */
+       printf("DSQ insert test done on cpu: %d\n", skel->data->insert_test_cpu);
+       if (skel->data->insert_test_cpu != -1)
+               printf("\u2713 DSQ insert succeeded !\n");
+       else {
+               printf("\u2717 DSQ insert failed or not attempted\n");
+               failed = true;
+       }
+
+       /* Check DSQ peek results */
+       printf("  DSQ peek result 1 (before insert): %d\n",
+              skel->data->dsq_peek_result1);
+       if (skel->data->dsq_peek_result1 == 0)
+               printf("\u2713 DSQ peek verification success: peek returned NULL!\n");
+       else {
+               printf("\u2717 DSQ peek verification failed\n");
+               failed = true;
+       }
+
+       printf("  DSQ peek result 2 (after insert): %ld\n",
+              skel->data->dsq_peek_result2);
+       printf("  DSQ peek result 2, expected: %ld\n",
+              skel->data->dsq_peek_result2_expected);
+       if (skel->data->dsq_peek_result2 ==
+           skel->data->dsq_peek_result2_expected)
+               printf("\u2713 DSQ peek verification success: peek returned the inserted task!\n");
+       else {
+               printf("\u2717 DSQ peek verification failed\n");
+               failed = true;
+       }
+
+       printf("  Inserted test task -> pid: %ld\n", skel->data->dsq_inserted_pid);
+       printf("  DSQ peek result 2 -> pid: %ld\n", skel->data->dsq_peek_result2_pid);
+
+       int pid_count;
+
+       pid_count = print_observed_pids(skel->maps.peek_results,
+                                       skel->data->max_samples, "DSQ pool");
+       printf("Total non-null peek observations: %ld out of %ld\n",
+              skel->data->successful_peeks, skel->data->total_peek_attempts);
+
+       if (skel->bss->debug_ksym_exists && pid_count == 0) {
+               printf("\u2717 DSQ pool test failed: no successful peeks in native mode\n");
+               failed = true;
+       }
+       if (skel->bss->debug_ksym_exists && pid_count > 0)
+               printf("\u2713 DSQ pool test success: observed successful peeks in native mode\n");
+
+       if (failed)
+               return SCX_TEST_FAIL;
+       else
+               return SCX_TEST_PASS;
+}
+
+static void cleanup(void *ctx)
+{
+       struct peek_dsq *skel = ctx;
+
+       if (workload_running) {
+               workload_running = false;
+               for (int i = 0; i < NUM_WORKERS; i++)
+                       pthread_join(workload_threads[i], NULL);
+       }
+
+       peek_dsq__destroy(skel);
+}
+
+struct scx_test peek_dsq = {
+       .name = "peek_dsq",
+       .description =
+               "Test DSQ create/destroy operations and future peek functionality",
+       .setup = setup,
+       .run = run,
+       .cleanup = cleanup,
+};
+REGISTER_SCX_TEST(&peek_dsq)