root/drivers/gpu/drm/scheduler/tests/tests_scheduler.c
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */

#include <linux/delay.h>
#include <linux/kthread.h>
#include <linux/ktime.h>
#include <linux/math64.h>

#include "sched_tests.h"

/*
 * DRM scheduler tests exercise load balancing decisions ie. entity selection
 * logic.
 */

static int drm_sched_scheduler_init(struct kunit *test)
{
        struct drm_mock_scheduler *sched;

        sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
        sched->base.credit_limit = 1;

        test->priv = sched;

        return 0;
}

static int drm_sched_scheduler_init2(struct kunit *test)
{
        struct drm_mock_scheduler *sched;

        sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
        sched->base.credit_limit = 2;

        test->priv = sched;

        return 0;
}

static void drm_sched_scheduler_exit(struct kunit *test)
{
        struct drm_mock_scheduler *sched = test->priv;

        drm_mock_sched_fini(sched);
}

static void drm_sched_scheduler_queue_overhead(struct kunit *test)
{
        struct drm_mock_scheduler *sched = test->priv;
        struct drm_mock_sched_entity *entity;
        const unsigned int job_us = 1000;
        const unsigned int jobs = 1000;
        const unsigned int total_us = jobs * job_us;
        struct drm_mock_sched_job *job, *first;
        ktime_t start, end;
        bool done;
        int i;

        /*
         * Deep queue job at a time processing (single credit).
         *
         * This measures the overhead of picking and processing a job at a time
         * by comparing the ideal total "GPU" time of all submitted jobs versus
         * the time actually taken.
         */

        KUNIT_ASSERT_EQ(test, sched->base.credit_limit, 1);

        entity = drm_mock_sched_entity_new(test,
                                           DRM_SCHED_PRIORITY_NORMAL,
                                           sched);

        for (i = 0; i <= jobs; i++) {
                job = drm_mock_sched_job_new(test, entity);
                if (i == 0)
                        first = job; /* Extra first job blocks the queue */
                else
                        drm_mock_sched_job_set_duration_us(job, job_us);
                drm_mock_sched_job_submit(job);
        }

        done = drm_mock_sched_job_wait_scheduled(first, HZ);
        KUNIT_ASSERT_TRUE(test, done);

        start = ktime_get();
        i = drm_mock_sched_advance(sched, 1); /* Release the queue */
        KUNIT_ASSERT_EQ(test, i, 1);

        /* Wait with a safe margin to avoid every failing. */
        done = drm_mock_sched_job_wait_finished(job,
                                                usecs_to_jiffies(total_us) * 5);
        end = ktime_get();
        KUNIT_ASSERT_TRUE(test, done);

        pr_info("Expected %uus, actual %lldus\n",
                total_us,
                ktime_to_us(ktime_sub(end, start)));

        drm_mock_sched_entity_free(entity);
}

static void drm_sched_scheduler_ping_pong(struct kunit *test)
{
        struct drm_mock_sched_job *job, *first, *prev = NULL;
        struct drm_mock_scheduler *sched = test->priv;
        struct drm_mock_sched_entity *entity[2];
        const unsigned int job_us = 1000;
        const unsigned int jobs = 1000;
        const unsigned int total_us = jobs * job_us;
        ktime_t start, end;
        bool done;
        int i;

        /*
         * Two entitites in inter-dependency chain.
         *
         * This measures the overhead of picking and processing a job at a time,
         * where each job depends on the previous one from the diffferent
         * entity, by comparing the ideal total "GPU" time of all submitted jobs
         * versus the time actually taken.
         */

        KUNIT_ASSERT_EQ(test, sched->base.credit_limit, 1);

        for (i = 0; i < ARRAY_SIZE(entity); i++)
                entity[i] = drm_mock_sched_entity_new(test,
                                                      DRM_SCHED_PRIORITY_NORMAL,
                                                      sched);

        for (i = 0; i <= jobs; i++) {
                job = drm_mock_sched_job_new(test, entity[i & 1]);
                if (i == 0)
                        first = job; /* Extra first job blocks the queue */
                else
                        drm_mock_sched_job_set_duration_us(job, job_us);
                if (prev)
                        drm_sched_job_add_dependency(&job->base,
                                                     dma_fence_get(&prev->base.s_fence->finished));
                drm_mock_sched_job_submit(job);
                prev = job;
        }

        done = drm_mock_sched_job_wait_scheduled(first, HZ);
        KUNIT_ASSERT_TRUE(test, done);

        start = ktime_get();
        i = drm_mock_sched_advance(sched, 1); /* Release the queue */
        KUNIT_ASSERT_EQ(test, i, 1);

        /* Wait with a safe margin to avoid every failing. */
        done = drm_mock_sched_job_wait_finished(job,
                                                usecs_to_jiffies(total_us) * 5);
        end = ktime_get();
        KUNIT_ASSERT_TRUE(test, done);

        pr_info("Expected %uus, actual %lldus\n",
                total_us,
                ktime_to_us(ktime_sub(end, start)));

        for (i = 0; i < ARRAY_SIZE(entity); i++)
                drm_mock_sched_entity_free(entity[i]);
}

static struct kunit_case drm_sched_scheduler_overhead_tests[] = {
        KUNIT_CASE_SLOW(drm_sched_scheduler_queue_overhead),
        KUNIT_CASE_SLOW(drm_sched_scheduler_ping_pong),
        {}
};

static struct kunit_suite drm_sched_scheduler_overhead = {
        .name = "drm_sched_scheduler_overhead_tests",
        .init = drm_sched_scheduler_init,
        .exit = drm_sched_scheduler_exit,
        .test_cases = drm_sched_scheduler_overhead_tests,
};

/*
 * struct drm_sched_client_params - describe a workload emitted from a client
 *
 * A simulated client will create an entity with a scheduling @priority and emit
 * jobs in a loop where each iteration will consist of:
 *
 * 1. Submit @job_cnt jobs, each with a set duration of @job_us.
 * 2. If @sync is true wait for last submitted job to finish.
 * 3. Sleep for @wait_us micro-seconds.
 * 4. Repeat.
 */
struct drm_sched_client_params {
        enum drm_sched_priority priority;
        unsigned int job_cnt;
        unsigned int job_us;
        bool sync;
        unsigned int wait_us;
};

struct drm_sched_test_params {
        const char *description;
        unsigned int num_clients;
        struct drm_sched_client_params client[2];
};

static const struct drm_sched_test_params drm_sched_cases[] = {
        {
                .description = "Normal priority and normal priority",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
        },
        {
                .description = "Normal priority and low priority",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_LOW,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
        },
        {
                .description = "High priority and normal priority",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_HIGH,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
        },
        {
                .description = "High priority and low priority",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_HIGH,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_LOW,
                        .job_cnt = 1,
                        .job_us = 8000,
                        .wait_us = 0,
                        .sync = false,
                },
        },
        {
                .description = "50% and 50%",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 1500,
                        .wait_us = 1500,
                        .sync = true,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 2500,
                        .wait_us = 2500,
                        .sync = true,
                },
        },
        {
                .description = "50% and 50% low priority",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 1500,
                        .wait_us = 1500,
                        .sync = true,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_LOW,
                        .job_cnt = 1,
                        .job_us = 2500,
                        .wait_us = 2500,
                        .sync = true,
                },
        },
        {
                .description = "50% high priority and 50%",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_HIGH,
                        .job_cnt = 1,
                        .job_us = 1500,
                        .wait_us = 1500,
                        .sync = true,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 2500,
                        .wait_us = 2500,
                        .sync = true,
                },
        },
        {
                .description = "Low priority hog and interactive client",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_LOW,
                        .job_cnt = 3,
                        .job_us = 2500,
                        .wait_us = 500,
                        .sync = false,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 500,
                        .wait_us = 10000,
                        .sync = true,
                },
        },
        {
                .description = "Heavy rendering and interactive client",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 3,
                        .job_us = 2500,
                        .wait_us = 2500,
                        .sync = true,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 1000,
                        .wait_us = 9000,
                        .sync = true,
                },
        },
        {
                .description = "Very heavy rendering and interactive client",
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 4,
                        .job_us = 50000,
                        .wait_us = 1,
                        .sync = true,
                },
                .client[1] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 1,
                        .job_us = 1000,
                        .wait_us = 9000,
                        .sync = true,
                },
        },
};

static void
drm_sched_desc(const struct drm_sched_test_params *params, char *desc)
{
        strscpy(desc, params->description, KUNIT_PARAM_DESC_SIZE);
}

KUNIT_ARRAY_PARAM(drm_sched_scheduler_two_clients,
                  drm_sched_cases,
                  drm_sched_desc);

/*
 * struct test_client_stats - track client stats
 *
 * For each client executing a simulated workload we track some timings for
 * which we are interested in the minimum of all iterations (@min_us), maximum
 * (@max_us) and the overall total for all iterations (@tot_us).
 */
struct test_client_stats {
        unsigned int min_us;
        unsigned int max_us;
        unsigned long tot_us;
};

/*
 * struct test_client - a simulated userspace client submitting scheduler work
 *
 * Each client executing a simulated workload is represented by one of these.
 *
 * Each of them instantiates a scheduling @entity and executes a workloads as
 * defined in @params. Based on those @params the theoretical execution time of
 * the client is calculated as @ideal_duration, while the actual wall time is
 * tracked in @duration (calculated based on the @start and @end client time-
 * stamps).
 *
 * Numerical @id is assigned to each for logging purposes.
 *
 * @worker and @work are used to provide an independent execution context from
 * which scheduler jobs are submitted.
 *
 * During execution statistics on how long it took to submit and execute one
 * iteration (whether or not synchronous) is kept in @cycle_time, while
 * @latency_time tracks the @cycle_time minus the ideal duration of the one
 * cycle.
 *
 * Once the client has completed the set number of iterations it will write the
 * completion status into @done.
 */
struct test_client {
        struct kunit    *test; /* Backpointer to the kunit test. */

        struct drm_mock_sched_entity    *entity;
        struct kthread_worker           *worker;
        struct kthread_work             work;

        struct drm_sched_client_params  params;

        unsigned int    id;
        ktime_t         duration;
        ktime_t         ideal_duration;
        unsigned int    cycles;
        unsigned int    cycle;
        ktime_t         start;
        ktime_t         end;
        bool            done;

        struct test_client_stats        cycle_time;
        struct test_client_stats        latency_time;
};

static void
update_stats(struct test_client_stats *stats, unsigned int us)
{
        if (us > stats->max_us)
                stats->max_us = us;
        if (us < stats->min_us)
                stats->min_us = us;
        stats->tot_us += us;
}

static unsigned int
get_stats_avg(struct test_client_stats *stats, unsigned int cycles)
{
        return div_u64(stats->tot_us, cycles);
}

static void drm_sched_client_work(struct kthread_work *work)
{
        struct test_client *client = container_of(work, typeof(*client), work);
        const long sync_wait = MAX_SCHEDULE_TIMEOUT;
        unsigned int cycle, work_us, period_us;
        struct drm_mock_sched_job *job = NULL;

        work_us = client->params.job_cnt * client->params.job_us;
        period_us = work_us + client->params.wait_us;
        client->cycles =
                DIV_ROUND_UP((unsigned int)ktime_to_us(client->duration),
                             period_us);
        client->ideal_duration = us_to_ktime(client->cycles * period_us);

        client->start = ktime_get();

        for (cycle = 0; cycle < client->cycles; cycle++) {
                ktime_t cycle_time;
                unsigned int batch;
                unsigned long us;

                if (READ_ONCE(client->done))
                        break;

                cycle_time = ktime_get();
                for (batch = 0; batch < client->params.job_cnt; batch++) {
                        job = drm_mock_sched_job_new(client->test,
                                                     client->entity);
                        drm_mock_sched_job_set_duration_us(job,
                                                           client->params.job_us);
                        drm_mock_sched_job_submit(job);
                }

                if (client->params.sync)
                        drm_mock_sched_job_wait_finished(job, sync_wait);

                cycle_time = ktime_sub(ktime_get(), cycle_time);
                us = ktime_to_us(cycle_time);
                update_stats(&client->cycle_time, us);
                if (ktime_to_us(cycle_time) >= (long)work_us)
                        us = ktime_to_us(cycle_time) - work_us;
                else if (WARN_ON_ONCE(client->params.sync)) /* GPU job took less than expected. */
                        us = 0;
                update_stats(&client->latency_time, us);
                WRITE_ONCE(client->cycle, cycle);

                if (READ_ONCE(client->done))
                        break;

                if (client->params.wait_us)
                        fsleep(client->params.wait_us);
                else if (!client->params.sync)
                        cond_resched(); /* Do not hog the CPU if fully async. */
        }

        client->done = drm_mock_sched_job_wait_finished(job, sync_wait);
        client->end = ktime_get();
}

static const char *prio_str(enum drm_sched_priority prio)
{
        switch (prio) {
        case DRM_SCHED_PRIORITY_KERNEL:
                return "kernel";
        case DRM_SCHED_PRIORITY_LOW:
                return "low";
        case DRM_SCHED_PRIORITY_NORMAL:
                return "normal";
        case DRM_SCHED_PRIORITY_HIGH:
                return "high";
        default:
                return "???";
        }
}

static bool client_done(struct test_client *client)
{
        return READ_ONCE(client->done); /* READ_ONCE to document lockless read from a loop. */
}

static void drm_sched_scheduler_two_clients_test(struct kunit *test)
{
        const struct drm_sched_test_params *params = test->param_value;
        struct drm_mock_scheduler *sched = test->priv;
        struct test_client client[2] = { };
        unsigned int prev_cycle[2] = { };
        unsigned int i, j;
        ktime_t start;

        /*
         * Same job stream from two clients.
         */

        for (i = 0; i < ARRAY_SIZE(client); i++)
                client[i].entity =
                        drm_mock_sched_entity_new(test,
                                                  params->client[i].priority,
                                                  sched);

        for (i = 0; i < ARRAY_SIZE(client); i++) {
                client[i].test = test;
                client[i].id = i;
                client[i].duration = ms_to_ktime(1000);
                client[i].params = params->client[i];
                client[i].cycle_time.min_us = ~0U;
                client[i].latency_time.min_us = ~0U;
                client[i].worker =
                        kthread_create_worker(0, "%s-%u", __func__, i);
                if (IS_ERR(client[i].worker)) {
                        for (j = 0; j < i; j++)
                                kthread_destroy_worker(client[j].worker);
                        KUNIT_FAIL(test, "Failed to create worker!\n");
                }

                kthread_init_work(&client[i].work, drm_sched_client_work);
        }

        for (i = 0; i < ARRAY_SIZE(client); i++)
                kthread_queue_work(client[i].worker, &client[i].work);

        /*
         * The clients (workers) can be a mix of async (deep submission queue),
         * sync (one job at a time), or something in between. Therefore it is
         * difficult to display a single metric representing their progress.
         *
         * Each struct drm_sched_client_params describes the actual submission
         * pattern which happens in the following steps:
         *  1. Submit N jobs
         *  2. Wait for last submitted job to finish
         *  3. Sleep for U micro-seconds
         *  4. Goto 1. for C cycles
         *
         * Where number of cycles is calculated to match the target client
         * duration from the respective struct drm_sched_test_params.
         *
         * To asses scheduling behaviour what we output for both clients is:
         *  - pct: Percentage progress of the jobs submitted
         *  - cps: "Cycles" per second (where one cycle is one complete
         *         iteration from the above)
         *  -  qd: Number of outstanding jobs in the client/entity
         */

        pr_info(" [pct] - Job sumission progress\n"
" [cps] - Cycles per second\n"
"  [qd] - Number of outstanding jobs in the client/entity\n");
        pr_info("%s:\n\t            pct1 cps1 qd1;  pct2 cps2 qd2\n",
                params->description);
        start = ktime_get();
        while (!client_done(&client[0]) || !client_done(&client[1])) {
                const unsigned int period_ms = 100;
                const unsigned int frequency = 1000 / period_ms;
                unsigned int pct[2], qd[2], cycle[2], cps[2];

                for (i = 0; i < ARRAY_SIZE(client); i++) {
                        qd[i] = spsc_queue_count(&client[i].entity->base.job_queue);
                        cycle[i] = READ_ONCE(client[i].cycle);
                        cps[i] = DIV_ROUND_UP(100 * frequency *
                                              (cycle[i] - prev_cycle[i]),
                                              100);
                        if (client[i].cycles)
                                pct[i] = DIV_ROUND_UP(100 * (1 + cycle[i]),
                                                      client[i].cycles);
                        else
                                pct[i] = 0;
                        prev_cycle[i] = cycle[i];
                }

                if (client_done(&client[0]))
                        pr_info("\t+%6lldms:               ; %3u %5u %4u\n",
                                ktime_to_ms(ktime_sub(ktime_get(), start)),
                                pct[1], cps[1], qd[1]);
                else if (client_done(&client[1]))
                        pr_info("\t+%6lldms: %3u %5u %4u;\n",
                                ktime_to_ms(ktime_sub(ktime_get(), start)),
                                pct[0], cps[0], qd[0]);
                else
                        pr_info("\t+%6lldms: %3u %5u %4u; %3u %5u %4u\n",
                                ktime_to_ms(ktime_sub(ktime_get(), start)),
                                pct[0], cps[0], qd[0],
                                pct[1], cps[1], qd[1]);

                msleep(period_ms);
        }

        for (i = 0; i < ARRAY_SIZE(client); i++) {
                kthread_flush_work(&client[i].work);
                kthread_destroy_worker(client[i].worker);
        }

        for (i = 0; i < ARRAY_SIZE(client); i++)
                KUNIT_ASSERT_TRUE(test, client[i].done);

        for (i = 0; i < ARRAY_SIZE(client); i++) {
                pr_info("    %u: prio=%s sync=%u elapsed_ms=%lldms (ideal_ms=%lldms) cycle_time(min,avg,max)=%u,%u,%u us latency_time(min,avg,max)=%u,%u,%u us",
                        i,
                        prio_str(params->client[i].priority),
                        params->client[i].sync,
                        ktime_to_ms(ktime_sub(client[i].end, client[i].start)),
                        ktime_to_ms(client[i].ideal_duration),
                        client[i].cycle_time.min_us,
                        get_stats_avg(&client[i].cycle_time, client[i].cycles),
                        client[i].cycle_time.max_us,
                        client[i].latency_time.min_us,
                        get_stats_avg(&client[i].latency_time, client[i].cycles),
                        client[i].latency_time.max_us);
                drm_mock_sched_entity_free(client[i].entity);
        }
}

static struct kunit_case drm_sched_scheduler_two_clients_tests[] = {
        KUNIT_CASE_PARAM_ATTR(drm_sched_scheduler_two_clients_test,
                              drm_sched_scheduler_two_clients_gen_params,
                              { .speed = KUNIT_SPEED_SLOW }),
        {}
};

static struct kunit_suite drm_sched_scheduler_two_clients1 = {
        .name = "drm_sched_scheduler_two_clients_one_credit_tests",
        .init = drm_sched_scheduler_init,
        .exit = drm_sched_scheduler_exit,
        .test_cases = drm_sched_scheduler_two_clients_tests,
};

static struct kunit_suite drm_sched_scheduler_two_clients2 = {
        .name = "drm_sched_scheduler_two_clients_two_credits_tests",
        .init = drm_sched_scheduler_init2,
        .exit = drm_sched_scheduler_exit,
        .test_cases = drm_sched_scheduler_two_clients_tests,
};

static const struct drm_sched_test_params drm_sched_many_cases[] = {
        {
                .description = "2 clients",
                .num_clients = 2,
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 4,
                        .job_us = 1000,
                        .wait_us = 0,
                        .sync = true,
                },
        },
        {
                .description = "3 clients",
                .num_clients = 3,
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 4,
                        .job_us = 1000,
                        .wait_us = 0,
                        .sync = true,
                },
        },
        {
                .description = "7 clients",
                .num_clients = 7,
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 4,
                        .job_us = 1000,
                        .wait_us = 0,
                        .sync = true,
                },
        },
        {
                .description = "13 clients",
                .num_clients = 13,
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 4,
                        .job_us = 1000,
                        .wait_us = 0,
                        .sync = true,
                },
        },
        {
                .description = "31 clients",
                .num_clients = 31,
                .client[0] = {
                        .priority = DRM_SCHED_PRIORITY_NORMAL,
                        .job_cnt = 2,
                        .job_us = 1000,
                        .wait_us = 0,
                        .sync = true,
                },
        },
};

KUNIT_ARRAY_PARAM(drm_sched_scheduler_many_clients,
                  drm_sched_many_cases,
                  drm_sched_desc);

static void drm_sched_scheduler_many_clients_test(struct kunit *test)
{
        const struct drm_sched_test_params *params = test->param_value;
        struct drm_mock_scheduler *sched = test->priv;
        const unsigned int clients = params->num_clients;
        unsigned int i, j, delta_total = 0, loops = 0;
        struct test_client *client;
        unsigned int *prev_cycle;
        ktime_t start;
        char *buf;

        /*
         * Many clients with deep-ish async queues.
         */

        buf = kunit_kmalloc(test, PAGE_SIZE, GFP_KERNEL);
        KUNIT_ASSERT_NOT_NULL(test, buf);
        client = kunit_kcalloc(test, clients, sizeof(*client), GFP_KERNEL);
        KUNIT_ASSERT_NOT_NULL(test, client);
        prev_cycle = kunit_kcalloc(test, clients, sizeof(*prev_cycle),
                                   GFP_KERNEL);
        KUNIT_ASSERT_NOT_NULL(test, prev_cycle);

        for (i = 0; i < clients; i++)
                client[i].entity =
                        drm_mock_sched_entity_new(test,
                                                  DRM_SCHED_PRIORITY_NORMAL,
                                                  sched);

        for (i = 0; i < clients; i++) {
                client[i].test = test;
                client[i].id = i;
                client[i].params = params->client[0];
                client[i].duration = ms_to_ktime(1000 / clients);
                client[i].cycle_time.min_us = ~0U;
                client[i].latency_time.min_us = ~0U;
                client[i].worker =
                        kthread_create_worker(0, "%s-%u", __func__, i);
                if (IS_ERR(client[i].worker)) {
                        for (j = 0; j < i; j++)
                                kthread_destroy_worker(client[j].worker);
                        KUNIT_FAIL(test, "Failed to create worker!\n");
                }

                kthread_init_work(&client[i].work, drm_sched_client_work);
        }

        for (i = 0; i < clients; i++)
                kthread_queue_work(client[i].worker, &client[i].work);

        start = ktime_get();
        pr_info("%u clients:\n\tt\t\tcycle:\t  min    avg    max : ...\n", clients);
        for (;;) {
                unsigned int min = ~0;
                unsigned int max = 0;
                unsigned int total = 0;
                bool done = true;
                char pbuf[16];

                memset(buf, 0, PAGE_SIZE);
                for (i = 0; i < clients; i++) {
                        unsigned int cycle, cycles;

                        /* Read current progress from the threaded worker. */
                        cycle = READ_ONCE(client[i].cycle);
                        cycles = READ_ONCE(client[i].cycles);

                        snprintf(pbuf, sizeof(pbuf), " %3d", cycle);
                        strncat(buf, pbuf, PAGE_SIZE);

                        total += cycle;
                        if (cycle < min)
                                min = cycle;
                        if (cycle > max)
                                max = cycle;

                        if (!min || (cycle + 1) < cycles)
                                done = false;
                }

                loops++;
                delta_total += max - min;

                pr_info("\t+%6lldms\t\t  %3u  %3u  %3u :%s\n",
                        ktime_to_ms(ktime_sub(ktime_get(), start)),
                        min, DIV_ROUND_UP(total, clients), max, buf);

                if (done)
                        break;

                msleep(100);
        }

        pr_info("    avg_max_min_delta(x100)=%u\n",
                loops ? DIV_ROUND_UP(delta_total * 100, loops) : 0);

        for (i = 0; i < clients; i++) {
                kthread_flush_work(&client[i].work);
                kthread_destroy_worker(client[i].worker);
        }

        for (i = 0; i < clients; i++)
                drm_mock_sched_entity_free(client[i].entity);
}

static struct kunit_case drm_sched_scheduler_many_clients_tests[] = {
        KUNIT_CASE_PARAM_ATTR(drm_sched_scheduler_many_clients_test,
                              drm_sched_scheduler_many_clients_gen_params,
                              { .speed = KUNIT_SPEED_SLOW }),
        {}
};

static struct kunit_suite drm_sched_scheduler_many_clients = {
        .name = "drm_sched_scheduler_many_clients_tests",
        .init = drm_sched_scheduler_init2,
        .exit = drm_sched_scheduler_exit,
        .test_cases = drm_sched_scheduler_many_clients_tests,
};

kunit_test_suites(&drm_sched_scheduler_overhead,
                  &drm_sched_scheduler_two_clients1,
                  &drm_sched_scheduler_two_clients2,
                  &drm_sched_scheduler_many_clients);