[vlc-devel] [PATCH 2/6] executor: add unit tests
Romain Vimont
rom1v at videolabs.io
Tue Sep 8 10:22:48 CEST 2020
On Tue, Sep 08, 2020 at 10:10:58AM +0200, Alexandre Janniaux wrote:
> Hi,
>
> Well done for this test :)
>
> On Tue, Sep 08, 2020 at 09:39:10AM +0200, Steve Lhomme wrote:
> > On 2020-09-07 17:40, Romain Vimont wrote:
> > > ...
> > > +static void test_multiple_runnables(void)
> > > +{
> > > + vlc_executor_t *executor = vlc_executor_New(3);
> > > + assert(executor);
> > > +
> > > + struct data shared_data;
> > > + InitData(&shared_data);
> > > +
> > > + struct vlc_runnable runnables[300];
> >
> > This is very inconvenient that you have to allocate and fill 300 times the
> > same structure exactly the same way. A single instance that you submit 300
> > would be better.
>
> Since each task is tracked by the corresponding runnable
> I don't think it's possible, but maybe the current syntax
> is a recipee for failure without stronger assertion that
> you don't submit an already submitted task?
Yes, I agree, because when I wrote the test, I was tempted to use the
same runnable, since I initialize all of them exactly the same way (only
the "node" is different, but we don't see it).
> However, why 300 and not 10? Or 2? I guess the goal is to
> run multiple jobs while having more jobs than the number of
> threads so you could add a comment to rationalize the choice
> a little.
I had no idea which numbers to choose. I just picked random values. :D
Here, the tasks are almost immediate anyway, so 10 or 5000 does not
change anything for the test.
> > > + for (int i = 0; i < 300; ++i)
> > > + {
> > > + struct vlc_runnable *runnable = &runnables[i];
> > > + runnable->run = RunIncrement;
> > > + runnable->userdata = &shared_data;
> > > + vlc_executor_Submit(executor, runnable);
> > > + }
> > > +
> > > + vlc_mutex_lock(&shared_data.lock);
> > > + while (shared_data.ended < 300)
> > > + vlc_cond_wait(&shared_data.cond, &shared_data.lock);
> > > + vlc_mutex_unlock(&shared_data.lock);
> >
> > Since the runnables are in the stack, they could keep track whether they are
> > finished running or not, and be signaled by the executor when they are. But
> > I understand that means adding more things to the runnable even when the
> > feature is not used.
>
> In particular, it means adding new synchronization while
> it's not needed if you chain tasks.
>
> > > +
> > > + assert(shared_data.ended == 300);
> > > +
> > > + vlc_executor_Delete(executor);
> > > +}
> > > +
> > > +static void test_blocking_delete(void)
> > > +{
> > > + vlc_executor_t *executor = vlc_executor_New(1);
> > > + assert(executor);
> > > +
> > > + struct data data;
> > > + InitData(&data);
> > > +
> > > + data.delay = VLC_TICK_FROM_MS(100);
> > > +
> > > + struct vlc_runnable runnable = {
> > > + .run = RunIncrement,
> > > + .userdata = &data,
> > > + };
> >
> > Usually I prefer to initialize without the field names. That makes
> > refactoring safer as you can:
> > - change the field name without changing all user code
> > - add callbacks and be warned (or fail to compile) if a user is missing an
> > implementation (or NULL)
>
> I prefer to initialize with a designated initializer, it
> zero-initialize the other fields when needed. and especially
> here where you don't want to specify the intializer for the
> vlc_list. :)
>
> > > +
> > > + vlc_executor_Submit(executor, &runnable);
> > > +
> > > + /* Wait for the runnable to be started */
> > > + vlc_mutex_lock(&data.lock);
> > > + while (data.started == 0)
> > > + vlc_cond_wait(&data.cond, &data.lock);
> > > + vlc_mutex_unlock(&data.lock);
> > > +
> > > + /* The runnable sleeps for about 100ms */
> >
> > You may make it longer than 100ms as the test sometimes run in parallel with
> > other things on busy systems.
>
> If it could not take too much time, we'd probably appreciate
> it on Linux where tests are taking some time already though.
+1
> I don't know if it's possible to test the correct interleave
> of execution without risking a race too, and without sleep,
> but that would be very appreciated.
>
> > > +
> > > + vlc_executor_Delete(executor);
> > > +
> > > + vlc_mutex_lock(&data.lock);
> > > + /* The executor must wait the end of running tasks on delete */
> > > + assert(data.ended == 1);
> > > + vlc_mutex_unlock(&data.lock);
> > > +}
> > > +
> > > +static void test_cancel(void)
> > > +{
> > > + vlc_executor_t *executor = vlc_executor_New(4);
> > > + assert(executor);
> > > +
> > > + struct data shared_data;
> > > + InitData(&shared_data);
> > > +
> > > + shared_data.delay = VLC_TICK_FROM_MS(100);
> > > +
> > > + /* Submit 40 tasks taking at least 100ms on an executor with at most 4
> > > + * threads */
>
> Why 40 too here? given that the total account for 1sec, it
> seems a bit overkill.
Same as for 300, I just picked a value. The tasks are canceled after
150ms anyway.
> > > +
> > > + struct vlc_runnable runnables[40];
> > > + for (int i = 0; i < 40; ++i)
> > > + {
> > > + struct vlc_runnable *runnable = &runnables[i];
> > > + runnable->run = RunIncrement;
> > > + runnable->userdata = &shared_data;
> > > + vlc_executor_Submit(executor, runnable);
> > > + }
> > > +
> > > + /* Wait a bit (in two lines to avoid harmful_delay() warning) */
> > > + vlc_tick_t delay = VLC_TICK_FROM_MS(150);
> > > + vlc_tick_sleep(delay);
> > > +
> > > + int canceled = 0;
> > > + for (int i = 0; i < 40; ++i)
> > > + {
> > > + if (vlc_executor_Cancel(executor, &runnables[i]))
> > > + ++canceled;
> > > + }
> > > +
> > > + vlc_mutex_lock(&shared_data.lock);
> > > + /* All started must not be canceled (but some non-started-yet may not be
> > > + * canceled either) */
> > > + assert(canceled + shared_data.started <= 40);
>
> Shouldn't we test a == 40 then?
Not here, because even if vlc_executor_Cancel() avoids a race-condition
(if it return false, we know that run() will be called), the "started"
value is incremented in the run() implementation (and may be not
executed yet).
> It seems to me that this assertion should be improved for this
> and thus take into account the tasks that have started but not
> incremented the counter yet.
:) but you can do that only after the _Delete (or WaitIdle()).
> Otherwise you might have task
> cancelled counting for two and task started counting for zero.
(I think you may not have a task canceled counting for two though)
>
> > > + vlc_mutex_unlock(&shared_data.lock);
> > > +
> > > + vlc_executor_Delete(executor);
> > > +
> > > + /* Every started task must also be ended */
> > > + assert(shared_data.started == shared_data.ended);
> > > + /* Every task is either canceled or ended */
> > > + assert(canceled + shared_data.ended == 40);
Like this ^
> > > +}
> > > +
> > > +struct doubler_task
> > > +{
> > > + vlc_executor_t *executor;
> > > + int *array;
> > > + size_t count;
> > > + struct vlc_runnable runnable;
> > > +};
> > > +
> > > +static void DoublerRun(void *);
> > > +
> > > +static bool
> > > +SpawnDoublerTask(vlc_executor_t *executor, int *array, size_t count)
> > > +{
> > > + struct doubler_task *task = malloc(sizeof(*task));
> > > + if (!task)
> > > + return false;
> > > +
> > > + task->executor = executor;
> > > + task->array = array;
> > > + task->count = count;
> > > + task->runnable.run = DoublerRun;
> > > + task->runnable.userdata = task;
> > > +
> > > + vlc_executor_Submit(executor, &task->runnable);
> > > +
> > > + return true;
> > > +}
> > > +
> > > +static void DoublerRun(void *userdata)
> > > +{
> > > + struct doubler_task *task = userdata;
> > > +
> > > + if (task->count == 1)
> > > + task->array[0] *= 2; /* double the value */
> > > + else
> > > + {
> > > + /* Spawn tasks doubling halves of the array recursively */
> > > + bool ok;
> > > +
> > > + ok = SpawnDoublerTask(task->executor, task->array, task->count / 2);
> > > + assert(ok);
> > > +
> > > + ok = SpawnDoublerTask(task->executor, task->array + task->count / 2,
> > > + task->count - task->count / 2);
> > > + assert(ok);
> > > + }
> > > +
> > > + free(task);
> > > +}
> > > +
> > > +static void test_task_chain(void)
> > > +{
> > > + vlc_executor_t *executor = vlc_executor_New(4);
> > > + assert(executor);
> > > +
> > > + /* Numbers from 0 to 99 */
> > > + int array[100];
> > > + for (int i = 0; i < 100; ++i)
> > > + array[i] = i;
> > > +
> > > + /* Double all values in the array from tasks spawning smaller tasks
> > > + * recursively, until the array has size 1, where the single value is
> > > + * doubled */
> > > + SpawnDoublerTask(executor, array, 100);
> > > +
> > > + vlc_executor_WaitIdle(executor);
> > > + vlc_executor_Delete(executor);
> > > +
> > > + /* All values must have been doubled */
> > > + for (int i = 0; i < 100; ++i)
> > > + assert(array[i] == 2 * i);
> > > +}
> > > +
> > > +int main(void)
> > > +{
> > > + test_single_runnable();
> > > + test_multiple_runnables();
> > > + test_blocking_delete();
> > > + test_cancel();
> > > + test_task_chain();
> > > + return 0;
> > > +}
> > > --
> > > 2.28.0
More information about the vlc-devel
mailing list