Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
@@ -0,0 +1,8 @@
# SPDX-License-Identifier: GPL-2.0
drm-sched-tests-y := \
mock_scheduler.o \
tests_basic.o \
tests_scheduler.o
obj-$(CONFIG_DRM_SCHED_KUNIT_TEST) += drm-sched-tests.o
@@ -0,0 +1,370 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */
#include "sched_tests.h"
/*
* Here we implement the mock "GPU" (or the scheduler backend) which is used by
* the DRM scheduler unit tests in order to exercise the core functionality.
*
* Test cases are implemented in a separate file.
*/
/**
* drm_mock_sched_entity_new - Create a new mock scheduler entity
*
* @test: KUnit test owning the entity
* @priority: Scheduling priority
* @sched: Mock scheduler on which the entity can be scheduled
*
* Returns: New mock scheduler entity with allocation managed by the test
*/
struct drm_mock_sched_entity *
drm_mock_sched_entity_new(struct kunit *test,
enum drm_sched_priority priority,
struct drm_mock_scheduler *sched)
{
struct drm_mock_sched_entity *entity;
struct drm_gpu_scheduler *drm_sched;
int ret;
entity = kunit_kzalloc(test, sizeof(*entity), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, entity);
drm_sched = &sched->base;
ret = drm_sched_entity_init(&entity->base,
priority,
&drm_sched, 1,
NULL);
KUNIT_ASSERT_EQ(test, ret, 0);
entity->test = test;
return entity;
}
/**
* drm_mock_sched_entity_free - Destroys a mock scheduler entity
*
* @entity: Entity to destroy
*
* To be used from the test cases once done with the entity.
*/
void drm_mock_sched_entity_free(struct drm_mock_sched_entity *entity)
{
drm_sched_entity_destroy(&entity->base);
}
static void drm_mock_sched_job_complete(struct drm_mock_sched_job *job)
{
struct drm_mock_scheduler *sched =
drm_sched_to_mock_sched(job->base.sched);
lockdep_assert_held(&sched->lock);
job->flags |= DRM_MOCK_SCHED_JOB_DONE;
list_del(&job->link);
dma_fence_signal_locked(&job->hw_fence);
complete(&job->done);
}
static enum hrtimer_restart
drm_mock_sched_job_signal_timer(struct hrtimer *hrtimer)
{
struct drm_mock_sched_job *job =
container_of(hrtimer, typeof(*job), timer);
struct drm_mock_scheduler *sched =
drm_sched_to_mock_sched(job->base.sched);
struct drm_mock_sched_job *next;
ktime_t now = ktime_get();
unsigned long flags;
LIST_HEAD(signal);
spin_lock_irqsave(&sched->lock, flags);
list_for_each_entry_safe(job, next, &sched->job_list, link) {
if (!job->duration_us)
break;
if (ktime_before(now, job->finish_at))
break;
sched->hw_timeline.cur_seqno = job->hw_fence.seqno;
drm_mock_sched_job_complete(job);
}
spin_unlock_irqrestore(&sched->lock, flags);
return HRTIMER_NORESTART;
}
/**
* drm_mock_sched_job_new - Create a new mock scheduler job
*
* @test: KUnit test owning the job
* @entity: Scheduler entity of the job
*
* Returns: New mock scheduler job with allocation managed by the test
*/
struct drm_mock_sched_job *
drm_mock_sched_job_new(struct kunit *test,
struct drm_mock_sched_entity *entity)
{
struct drm_mock_sched_job *job;
int ret;
job = kunit_kzalloc(test, sizeof(*job), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, job);
ret = drm_sched_job_init(&job->base,
&entity->base,
1,
NULL,
1);
KUNIT_ASSERT_EQ(test, ret, 0);
job->test = test;
init_completion(&job->done);
INIT_LIST_HEAD(&job->link);
hrtimer_setup(&job->timer, drm_mock_sched_job_signal_timer,
CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
return job;
}
static const char *drm_mock_sched_hw_fence_driver_name(struct dma_fence *fence)
{
return "drm_mock_sched";
}
static const char *
drm_mock_sched_hw_fence_timeline_name(struct dma_fence *fence)
{
struct drm_mock_sched_job *job =
container_of(fence, typeof(*job), hw_fence);
return (const char *)job->base.sched->name;
}
static void drm_mock_sched_hw_fence_release(struct dma_fence *fence)
{
struct drm_mock_sched_job *job =
container_of(fence, typeof(*job), hw_fence);
hrtimer_cancel(&job->timer);
/* Containing job is freed by the kunit framework */
}
static const struct dma_fence_ops drm_mock_sched_hw_fence_ops = {
.get_driver_name = drm_mock_sched_hw_fence_driver_name,
.get_timeline_name = drm_mock_sched_hw_fence_timeline_name,
.release = drm_mock_sched_hw_fence_release,
};
static struct dma_fence *mock_sched_run_job(struct drm_sched_job *sched_job)
{
struct drm_mock_scheduler *sched =
drm_sched_to_mock_sched(sched_job->sched);
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
dma_fence_init(&job->hw_fence,
&drm_mock_sched_hw_fence_ops,
&sched->lock,
sched->hw_timeline.context,
atomic_inc_return(&sched->hw_timeline.next_seqno));
dma_fence_get(&job->hw_fence); /* Reference for the job_list */
spin_lock_irq(&sched->lock);
if (job->duration_us) {
ktime_t prev_finish_at = 0;
if (!list_empty(&sched->job_list)) {
struct drm_mock_sched_job *prev =
list_last_entry(&sched->job_list, typeof(*prev),
link);
prev_finish_at = prev->finish_at;
}
if (!prev_finish_at)
prev_finish_at = ktime_get();
job->finish_at = ktime_add_us(prev_finish_at, job->duration_us);
}
list_add_tail(&job->link, &sched->job_list);
if (job->finish_at)
hrtimer_start(&job->timer, job->finish_at, HRTIMER_MODE_ABS);
spin_unlock_irq(&sched->lock);
return &job->hw_fence;
}
/*
* Normally, drivers would take appropriate measures in this callback, such as
* killing the entity the faulty job is associated with, resetting the hardware
* and / or resubmitting non-faulty jobs.
*
* For the mock scheduler, there are no hardware rings to be resetted nor jobs
* to be resubmitted. Thus, this function merely ensures that
* a) timedout fences get signaled properly and removed from the pending list
* b) the mock scheduler framework gets informed about the timeout via a flag
* c) The drm_sched_job, not longer needed, gets freed
*/
static enum drm_gpu_sched_stat
mock_sched_timedout_job(struct drm_sched_job *sched_job)
{
struct drm_mock_scheduler *sched = drm_sched_to_mock_sched(sched_job->sched);
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
unsigned long flags;
if (job->flags & DRM_MOCK_SCHED_JOB_DONT_RESET) {
job->flags |= DRM_MOCK_SCHED_JOB_RESET_SKIPPED;
return DRM_GPU_SCHED_STAT_NO_HANG;
}
spin_lock_irqsave(&sched->lock, flags);
if (!dma_fence_is_signaled_locked(&job->hw_fence)) {
list_del(&job->link);
job->flags |= DRM_MOCK_SCHED_JOB_TIMEDOUT;
dma_fence_set_error(&job->hw_fence, -ETIMEDOUT);
dma_fence_signal_locked(&job->hw_fence);
}
spin_unlock_irqrestore(&sched->lock, flags);
dma_fence_put(&job->hw_fence);
drm_sched_job_cleanup(sched_job);
/* Mock job itself is freed by the kunit framework. */
return DRM_GPU_SCHED_STAT_RESET;
}
static void mock_sched_free_job(struct drm_sched_job *sched_job)
{
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
dma_fence_put(&job->hw_fence);
drm_sched_job_cleanup(sched_job);
/* Mock job itself is freed by the kunit framework. */
}
static void mock_sched_cancel_job(struct drm_sched_job *sched_job)
{
struct drm_mock_scheduler *sched = drm_sched_to_mock_sched(sched_job->sched);
struct drm_mock_sched_job *job = drm_sched_job_to_mock_job(sched_job);
unsigned long flags;
hrtimer_cancel(&job->timer);
spin_lock_irqsave(&sched->lock, flags);
if (!dma_fence_is_signaled_locked(&job->hw_fence)) {
list_del(&job->link);
dma_fence_set_error(&job->hw_fence, -ECANCELED);
dma_fence_signal_locked(&job->hw_fence);
}
spin_unlock_irqrestore(&sched->lock, flags);
/*
* The GPU Scheduler will call drm_sched_backend_ops.free_job(), still.
* Mock job itself is freed by the kunit framework.
*/
}
static const struct drm_sched_backend_ops drm_mock_scheduler_ops = {
.run_job = mock_sched_run_job,
.timedout_job = mock_sched_timedout_job,
.free_job = mock_sched_free_job,
.cancel_job = mock_sched_cancel_job,
};
/**
* drm_mock_sched_new - Create a new mock scheduler
*
* @test: KUnit test owning the job
* @timeout: Job timeout to set
*
* Returns: New mock scheduler with allocation managed by the test
*/
struct drm_mock_scheduler *drm_mock_sched_new(struct kunit *test, long timeout)
{
struct drm_sched_init_args args = {
.ops = &drm_mock_scheduler_ops,
.num_rqs = DRM_SCHED_PRIORITY_COUNT,
.credit_limit = U32_MAX,
.hang_limit = 1,
.timeout = timeout,
.name = "drm-mock-scheduler",
};
struct drm_mock_scheduler *sched;
int ret;
sched = kunit_kzalloc(test, sizeof(*sched), GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, sched);
ret = drm_sched_init(&sched->base, &args);
KUNIT_ASSERT_EQ(test, ret, 0);
sched->test = test;
sched->hw_timeline.context = dma_fence_context_alloc(1);
atomic_set(&sched->hw_timeline.next_seqno, 0);
INIT_LIST_HEAD(&sched->job_list);
spin_lock_init(&sched->lock);
return sched;
}
/**
* drm_mock_sched_fini - Destroys a mock scheduler
*
* @sched: Scheduler to destroy
*
* To be used from the test cases once done with the scheduler.
*/
void drm_mock_sched_fini(struct drm_mock_scheduler *sched)
{
drm_sched_fini(&sched->base);
}
/**
* drm_mock_sched_advance - Advances the mock scheduler timeline
*
* @sched: Scheduler timeline to advance
* @num: By how many jobs to advance
*
* Advancing the scheduler timeline by a number of seqnos will trigger
* signalling of the hardware fences and unlinking the jobs from the internal
* scheduler tracking.
*
* This can be used from test cases which want complete control of the simulated
* job execution timing. For example submitting one job with no set duration
* would never complete it before test cases advances the timeline by one.
*/
unsigned int drm_mock_sched_advance(struct drm_mock_scheduler *sched,
unsigned int num)
{
struct drm_mock_sched_job *job, *next;
unsigned int found = 0;
unsigned long flags;
LIST_HEAD(signal);
spin_lock_irqsave(&sched->lock, flags);
if (WARN_ON_ONCE(sched->hw_timeline.cur_seqno + num <
sched->hw_timeline.cur_seqno))
goto unlock;
sched->hw_timeline.cur_seqno += num;
list_for_each_entry_safe(job, next, &sched->job_list, link) {
if (sched->hw_timeline.cur_seqno < job->hw_fence.seqno)
break;
drm_mock_sched_job_complete(job);
found++;
}
unlock:
spin_unlock_irqrestore(&sched->lock, flags);
return found;
}
MODULE_DESCRIPTION("DRM mock scheduler and tests");
MODULE_LICENSE("GPL");
@@ -0,0 +1,224 @@
/* SPDX-License-Identifier: GPL-2.0 */
/* Copyright (c) 2025 Valve Corporation */
#ifndef _SCHED_TESTS_H_
#define _SCHED_TESTS_H_
#include <kunit/test.h>
#include <linux/atomic.h>
#include <linux/completion.h>
#include <linux/dma-fence.h>
#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/types.h>
#include <drm/gpu_scheduler.h>
/*
* DOC: Mock DRM scheduler data structures
*
* drm_mock_* data structures are used to implement a mock "GPU".
*
* They subclass the core DRM scheduler objects and add their data on top, which
* enables tracking the submitted jobs and simulating their execution with the
* attributes as specified by the test case.
*/
/**
* struct drm_mock_scheduler - implements a trivial mock GPU execution engine
*
* @base: DRM scheduler base class
* @test: Backpointer to owning the kunit test case
* @lock: Lock to protect the simulated @hw_timeline and @job_list
* @job_list: List of jobs submitted to the mock GPU
* @hw_timeline: Simulated hardware timeline has a @context, @next_seqno and
* @cur_seqno for implementing a struct dma_fence signaling the
* simulated job completion.
*
* Trivial mock GPU execution engine tracks submitted jobs and enables
* completing them strictly in submission order.
*/
struct drm_mock_scheduler {
struct drm_gpu_scheduler base;
struct kunit *test;
spinlock_t lock;
struct list_head job_list;
struct {
u64 context;
atomic_t next_seqno;
unsigned int cur_seqno;
} hw_timeline;
};
/**
* struct drm_mock_sched_entity - implements a mock GPU sched entity
*
* @base: DRM scheduler entity base class
* @test: Backpointer to owning the kunit test case
*
* Mock GPU sched entity is used by the test cases to submit jobs to the mock
* scheduler.
*/
struct drm_mock_sched_entity {
struct drm_sched_entity base;
struct kunit *test;
};
/**
* struct drm_mock_sched_job - implements a mock GPU job
*
* @base: DRM sched job base class
* @done: Completion signaling job completion.
* @flags: Flags designating job state.
* @link: List head element used by job tracking by the drm_mock_scheduler
* @timer: Timer used for simulating job execution duration
* @duration_us: Simulated job duration in micro seconds, or zero if in manual
* timeline advance mode
* @finish_at: Absolute time when the jobs with set duration will complete
* @lock: Lock used for @hw_fence
* @hw_fence: Fence returned to DRM scheduler as the hardware fence
* @test: Backpointer to owning the kunit test case
*
* Mock GPU sched job is used by the test cases to submit jobs to the mock
* scheduler.
*/
struct drm_mock_sched_job {
struct drm_sched_job base;
struct completion done;
#define DRM_MOCK_SCHED_JOB_DONE 0x1
#define DRM_MOCK_SCHED_JOB_TIMEDOUT 0x2
#define DRM_MOCK_SCHED_JOB_DONT_RESET 0x4
#define DRM_MOCK_SCHED_JOB_RESET_SKIPPED 0x8
unsigned long flags;
struct list_head link;
struct hrtimer timer;
unsigned int duration_us;
ktime_t finish_at;
struct dma_fence hw_fence;
struct kunit *test;
};
static inline struct drm_mock_scheduler *
drm_sched_to_mock_sched(struct drm_gpu_scheduler *sched)
{
return container_of(sched, struct drm_mock_scheduler, base);
};
static inline struct drm_mock_sched_entity *
drm_sched_entity_to_mock_entity(struct drm_sched_entity *sched_entity)
{
return container_of(sched_entity, struct drm_mock_sched_entity, base);
};
static inline struct drm_mock_sched_job *
drm_sched_job_to_mock_job(struct drm_sched_job *sched_job)
{
return container_of(sched_job, struct drm_mock_sched_job, base);
};
struct drm_mock_scheduler *drm_mock_sched_new(struct kunit *test,
long timeout);
void drm_mock_sched_fini(struct drm_mock_scheduler *sched);
unsigned int drm_mock_sched_advance(struct drm_mock_scheduler *sched,
unsigned int num);
struct drm_mock_sched_entity *
drm_mock_sched_entity_new(struct kunit *test,
enum drm_sched_priority priority,
struct drm_mock_scheduler *sched);
void drm_mock_sched_entity_free(struct drm_mock_sched_entity *entity);
struct drm_mock_sched_job *
drm_mock_sched_job_new(struct kunit *test,
struct drm_mock_sched_entity *entity);
/**
* drm_mock_sched_job_submit - Arm and submit a job in one go
*
* @job: Job to arm and submit
*/
static inline void drm_mock_sched_job_submit(struct drm_mock_sched_job *job)
{
drm_sched_job_arm(&job->base);
drm_sched_entity_push_job(&job->base);
}
/**
* drm_mock_sched_job_set_duration_us - Set a job duration
*
* @job: Job to set the duration for
* @duration_us: Duration in micro seconds
*
* Jobs with duration set will be automatically completed by the mock scheduler
* as the timeline progresses, unless a job without a set duration is
* encountered in the timelime in which case calling drm_mock_sched_advance()
* will be required to bump the timeline.
*/
static inline void
drm_mock_sched_job_set_duration_us(struct drm_mock_sched_job *job,
unsigned int duration_us)
{
job->duration_us = duration_us;
}
/**
* drm_mock_sched_job_is_finished - Check if a job is finished
*
* @job: Job to check
*
* Returns: true if finished
*/
static inline bool
drm_mock_sched_job_is_finished(struct drm_mock_sched_job *job)
{
return job->flags & DRM_MOCK_SCHED_JOB_DONE;
}
/**
* drm_mock_sched_job_wait_finished - Wait until a job is finished
*
* @job: Job to wait for
* @timeout: Wait time in jiffies
*
* Returns: true if finished within the timeout provided, otherwise false
*/
static inline bool
drm_mock_sched_job_wait_finished(struct drm_mock_sched_job *job, long timeout)
{
if (job->flags & DRM_MOCK_SCHED_JOB_DONE)
return true;
return wait_for_completion_timeout(&job->done, timeout) != 0;
}
/**
* drm_mock_sched_job_wait_scheduled - Wait until a job is scheduled
*
* @job: Job to wait for
* @timeout: Wait time in jiffies
*
* Returns: true if scheduled within the timeout provided, otherwise false
*/
static inline bool
drm_mock_sched_job_wait_scheduled(struct drm_mock_sched_job *job, long timeout)
{
KUNIT_ASSERT_EQ(job->test, job->flags & DRM_MOCK_SCHED_JOB_DONE, 0);
return dma_fence_wait_timeout(&job->base.s_fence->scheduled,
false,
timeout) != 0;
}
#endif
@@ -0,0 +1,563 @@
// SPDX-License-Identifier: GPL-2.0
/* Copyright (c) 2025 Valve Corporation */
#include <linux/delay.h>
#include "sched_tests.h"
#define MOCK_TIMEOUT (HZ / 5)
/*
* DRM scheduler basic tests should check the basic functional correctness of
* the scheduler, including some very light smoke testing. More targeted tests,
* for example focusing on testing specific bugs and other more complicated test
* scenarios, should be implemented in separate source units.
*/
static int drm_sched_basic_init(struct kunit *test)
{
test->priv = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
return 0;
}
static void drm_sched_basic_exit(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
drm_mock_sched_fini(sched);
}
static int drm_sched_timeout_init(struct kunit *test)
{
test->priv = drm_mock_sched_new(test, MOCK_TIMEOUT);
return 0;
}
static void drm_sched_basic_submit(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
struct drm_mock_sched_job *job;
unsigned int i;
bool done;
/*
* Submit one job to the scheduler and verify that it gets scheduled
* and completed only when the mock hw backend processes it.
*/
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_finished(job, HZ / 2);
KUNIT_ASSERT_FALSE(test, done);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
}
struct drm_sched_basic_params {
const char *description;
unsigned int queue_depth;
unsigned int num_entities;
unsigned int job_us;
bool dep_chain;
};
static const struct drm_sched_basic_params drm_sched_basic_cases[] = {
{
.description = "A queue of jobs in a single entity",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 1,
},
{
.description = "A chain of dependent jobs across multiple entities",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 1,
.dep_chain = true,
},
{
.description = "Multiple independent job queues",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 4,
},
{
.description = "Multiple inter-dependent job queues",
.queue_depth = 100,
.job_us = 1000,
.num_entities = 4,
.dep_chain = true,
},
};
static void
drm_sched_basic_desc(const struct drm_sched_basic_params *params, char *desc)
{
strscpy(desc, params->description, KUNIT_PARAM_DESC_SIZE);
}
KUNIT_ARRAY_PARAM(drm_sched_basic, drm_sched_basic_cases, drm_sched_basic_desc);
static void drm_sched_basic_test(struct kunit *test)
{
const struct drm_sched_basic_params *params = test->param_value;
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_job *job, *prev = NULL;
struct drm_mock_sched_entity **entity;
unsigned int i, cur_ent = 0;
bool done;
entity = kunit_kcalloc(test, params->num_entities, sizeof(*entity),
GFP_KERNEL);
KUNIT_ASSERT_NOT_NULL(test, entity);
for (i = 0; i < params->num_entities; i++)
entity[i] = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
for (i = 0; i < params->queue_depth; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= params->num_entities;
drm_mock_sched_job_set_duration_us(job, params->job_us);
if (params->dep_chain && 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_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
for (i = 0; i < params->num_entities; i++)
drm_mock_sched_entity_free(entity[i]);
}
static void drm_sched_basic_entity_cleanup(struct kunit *test)
{
struct drm_mock_sched_job *job, *mid, *prev = NULL;
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity[4];
const unsigned int qd = 100;
unsigned int i, cur_ent = 0;
bool done;
/*
* Submit a queue of jobs across different entities with an explicit
* chain of dependencies between them and trigger entity cleanup while
* the queue is still being processed.
*/
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 < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
if (prev)
drm_sched_job_add_dependency(&job->base,
dma_fence_get(&prev->base.s_fence->finished));
drm_mock_sched_job_submit(job);
if (i == qd / 2)
mid = job;
prev = job;
}
done = drm_mock_sched_job_wait_finished(mid, HZ);
KUNIT_ASSERT_TRUE(test, done);
/* Exit with half of the queue still pending to be executed. */
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static struct kunit_case drm_sched_basic_tests[] = {
KUNIT_CASE(drm_sched_basic_submit),
KUNIT_CASE_PARAM(drm_sched_basic_test, drm_sched_basic_gen_params),
KUNIT_CASE(drm_sched_basic_entity_cleanup),
{}
};
static struct kunit_suite drm_sched_basic = {
.name = "drm_sched_basic_tests",
.init = drm_sched_basic_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_basic_tests,
};
static void drm_sched_basic_cancel(struct kunit *test)
{
struct drm_mock_sched_entity *entity;
struct drm_mock_scheduler *sched;
struct drm_mock_sched_job *job;
bool done;
/*
* Check that drm_sched_fini() uses the cancel_job() callback to cancel
* jobs that are still pending.
*/
sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
entity = drm_mock_sched_entity_new(test, DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
drm_mock_sched_fini(sched);
KUNIT_ASSERT_EQ(test, job->hw_fence.error, -ECANCELED);
}
static struct kunit_case drm_sched_cancel_tests[] = {
KUNIT_CASE(drm_sched_basic_cancel),
{}
};
static struct kunit_suite drm_sched_cancel = {
.name = "drm_sched_basic_cancel_tests",
.init = drm_sched_basic_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_cancel_tests,
};
static void drm_sched_basic_timeout(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
struct drm_mock_sched_job *job;
bool done;
/*
* Submit a single job against a scheduler with the timeout configured
* and verify that the timeout handling will run if the backend fails
* to complete it in time.
*/
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_finished(job, MOCK_TIMEOUT / 2);
KUNIT_ASSERT_FALSE(test, done);
KUNIT_ASSERT_EQ(test,
job->flags & DRM_MOCK_SCHED_JOB_TIMEDOUT,
0);
done = drm_mock_sched_job_wait_finished(job, MOCK_TIMEOUT);
KUNIT_ASSERT_FALSE(test, done);
KUNIT_ASSERT_EQ(test,
job->flags & DRM_MOCK_SCHED_JOB_TIMEDOUT,
DRM_MOCK_SCHED_JOB_TIMEDOUT);
drm_mock_sched_entity_free(entity);
}
static void drm_sched_skip_reset(struct kunit *test)
{
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_entity *entity;
struct drm_mock_sched_job *job;
unsigned int i;
bool done;
/*
* Submit a single job against a scheduler with the timeout configured
* and verify that if the job is still running, the timeout handler
* will skip the reset and allow the job to complete.
*/
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job = drm_mock_sched_job_new(test, entity);
job->flags = DRM_MOCK_SCHED_JOB_DONT_RESET;
drm_mock_sched_job_submit(job);
done = drm_mock_sched_job_wait_scheduled(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_finished(job, 2 * MOCK_TIMEOUT);
KUNIT_ASSERT_FALSE(test, done);
KUNIT_ASSERT_EQ(test,
job->flags & DRM_MOCK_SCHED_JOB_RESET_SKIPPED,
DRM_MOCK_SCHED_JOB_RESET_SKIPPED);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
}
static struct kunit_case drm_sched_timeout_tests[] = {
KUNIT_CASE(drm_sched_basic_timeout),
KUNIT_CASE(drm_sched_skip_reset),
{}
};
static struct kunit_suite drm_sched_timeout = {
.name = "drm_sched_basic_timeout_tests",
.init = drm_sched_timeout_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_timeout_tests,
};
static void drm_sched_priorities(struct kunit *test)
{
struct drm_mock_sched_entity *entity[DRM_SCHED_PRIORITY_COUNT];
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_job *job;
const unsigned int qd = 100;
unsigned int i, cur_ent = 0;
enum drm_sched_priority p;
bool done;
/*
* Submit a bunch of jobs against entities configured with different
* priorities.
*/
BUILD_BUG_ON(DRM_SCHED_PRIORITY_KERNEL > DRM_SCHED_PRIORITY_LOW);
BUILD_BUG_ON(ARRAY_SIZE(entity) != DRM_SCHED_PRIORITY_COUNT);
for (p = DRM_SCHED_PRIORITY_KERNEL; p <= DRM_SCHED_PRIORITY_LOW; p++)
entity[p] = drm_mock_sched_entity_new(test, p, sched);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
drm_mock_sched_job_submit(job);
}
done = drm_mock_sched_job_wait_finished(job, HZ);
KUNIT_ASSERT_TRUE(test, done);
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static void drm_sched_change_priority(struct kunit *test)
{
struct drm_mock_sched_entity *entity[DRM_SCHED_PRIORITY_COUNT];
struct drm_mock_scheduler *sched = test->priv;
struct drm_mock_sched_job *job;
const unsigned int qd = 1000;
unsigned int i, cur_ent = 0;
enum drm_sched_priority p;
/*
* Submit a bunch of jobs against entities configured with different
* priorities and while waiting for them to complete, periodically keep
* changing their priorities.
*
* We set up the queue-depth (qd) and job duration so the priority
* changing loop has some time to interact with submissions to the
* backend and job completions as they progress.
*/
for (p = DRM_SCHED_PRIORITY_KERNEL; p <= DRM_SCHED_PRIORITY_LOW; p++)
entity[p] = drm_mock_sched_entity_new(test, p, sched);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
drm_mock_sched_job_submit(job);
}
do {
drm_sched_entity_set_priority(&entity[cur_ent]->base,
(entity[cur_ent]->base.priority + 1) %
DRM_SCHED_PRIORITY_COUNT);
cur_ent++;
cur_ent %= ARRAY_SIZE(entity);
usleep_range(200, 500);
} while (!drm_mock_sched_job_is_finished(job));
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
}
static struct kunit_case drm_sched_priority_tests[] = {
KUNIT_CASE(drm_sched_priorities),
KUNIT_CASE_SLOW(drm_sched_change_priority),
{}
};
static struct kunit_suite drm_sched_priority = {
.name = "drm_sched_basic_priority_tests",
.init = drm_sched_basic_init,
.exit = drm_sched_basic_exit,
.test_cases = drm_sched_priority_tests,
};
static void drm_sched_test_modify_sched(struct kunit *test)
{
unsigned int i, cur_ent = 0, cur_sched = 0;
struct drm_mock_sched_entity *entity[13];
struct drm_mock_scheduler *sched[3];
struct drm_mock_sched_job *job;
const unsigned int qd = 1000;
/*
* Submit a bunch of jobs against entities configured with different
* schedulers and while waiting for them to complete, periodically keep
* changing schedulers associated with each entity.
*
* We set up the queue-depth (qd) and job duration so the sched modify
* loop has some time to interact with submissions to the backend and
* job completions as they progress.
*
* For the number of schedulers and entities we use primes in order to
* perturb the entity->sched assignments with less of a regular pattern.
*/
for (i = 0; i < ARRAY_SIZE(sched); i++)
sched[i] = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
for (i = 0; i < ARRAY_SIZE(entity); i++)
entity[i] = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched[i % ARRAY_SIZE(sched)]);
for (i = 0; i < qd; i++) {
job = drm_mock_sched_job_new(test, entity[cur_ent++]);
cur_ent %= ARRAY_SIZE(entity);
drm_mock_sched_job_set_duration_us(job, 1000);
drm_mock_sched_job_submit(job);
}
do {
struct drm_gpu_scheduler *modify;
usleep_range(200, 500);
cur_ent++;
cur_ent %= ARRAY_SIZE(entity);
cur_sched++;
cur_sched %= ARRAY_SIZE(sched);
modify = &sched[cur_sched]->base;
drm_sched_entity_modify_sched(&entity[cur_ent]->base, &modify,
1);
} while (!drm_mock_sched_job_is_finished(job));
for (i = 0; i < ARRAY_SIZE(entity); i++)
drm_mock_sched_entity_free(entity[i]);
for (i = 0; i < ARRAY_SIZE(sched); i++)
drm_mock_sched_fini(sched[i]);
}
static struct kunit_case drm_sched_modify_sched_tests[] = {
KUNIT_CASE(drm_sched_test_modify_sched),
{}
};
static struct kunit_suite drm_sched_modify_sched = {
.name = "drm_sched_basic_modify_sched_tests",
.test_cases = drm_sched_modify_sched_tests,
};
static void drm_sched_test_credits(struct kunit *test)
{
struct drm_mock_sched_entity *entity;
struct drm_mock_scheduler *sched;
struct drm_mock_sched_job *job[2];
bool done;
int i;
/*
* Check that the configured credit limit is respected.
*/
sched = drm_mock_sched_new(test, MAX_SCHEDULE_TIMEOUT);
sched->base.credit_limit = 1;
entity = drm_mock_sched_entity_new(test,
DRM_SCHED_PRIORITY_NORMAL,
sched);
job[0] = drm_mock_sched_job_new(test, entity);
job[1] = drm_mock_sched_job_new(test, entity);
drm_mock_sched_job_submit(job[0]);
drm_mock_sched_job_submit(job[1]);
done = drm_mock_sched_job_wait_scheduled(job[0], HZ);
KUNIT_ASSERT_TRUE(test, done);
done = drm_mock_sched_job_wait_scheduled(job[1], HZ);
KUNIT_ASSERT_FALSE(test, done);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_scheduled(job[1], HZ);
KUNIT_ASSERT_TRUE(test, done);
i = drm_mock_sched_advance(sched, 1);
KUNIT_ASSERT_EQ(test, i, 1);
done = drm_mock_sched_job_wait_finished(job[1], HZ);
KUNIT_ASSERT_TRUE(test, done);
drm_mock_sched_entity_free(entity);
drm_mock_sched_fini(sched);
}
static struct kunit_case drm_sched_credits_tests[] = {
KUNIT_CASE_SLOW(drm_sched_test_credits),
{}
};
static struct kunit_suite drm_sched_credits = {
.name = "drm_sched_basic_credits_tests",
.test_cases = drm_sched_credits_tests,
};
kunit_test_suites(&drm_sched_basic,
&drm_sched_timeout,
&drm_sched_cancel,
&drm_sched_priority,
&drm_sched_modify_sched,
&drm_sched_credits);
@@ -0,0 +1,874 @@
// 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);