ESP32 Arduino: FreeRTOS queues performance test

In this post we will do a simple analysis of the performance of inserting and consuming items from a FreeRTOS queue. The tests were performed using a DFRobot’s ESP-WROOM-32 device integrated in a ESP32 FireBeetle board.


Introduction

In this post we will do a simple analysis of the performance of inserting and consuming items from a FreeRTOS queue. Note that we will use the exact same code from the previous post about inter-task communication using FreeRTOS queues, although we are now going to execute some time measurements.

We will be using the millis function in order to get the time measurements. For curiosity, this function uses the FreeRTOS xTaskGetTickCount function in its implementation.

Note that there is the micros function also available for measuring elapsed time with more precision, although at the time of writing I was facing some issues with the obtained values while using it. Thus, I’ve used the millis function which, although less precise, was giving realistic values.

One important thing to mention is that this will be a simple test to check how FreeRTOS queues handle the insertion and consumption of big amounts of data, and thus the use case we will use may note be a typical one for queues. Also, the method we will use for measuring the performance is not the most precise, since our aim is only to obtain a rough sense of the time taken to execute the operation.

The tests shown here were performed using a DFRobot’s ESP-WROOM-32 device integrated in a ESP32 FireBeetle board.


The code

We will need to first declare a global variable of type QueueHandle_t, which will be our task handle. We will also put the maximum number of items that can be on the queue at a given time in a global variable, which we will use below for its initialization. This way, we can easily control the size of the queue if we want to test with different sizes.

We will also store 6 variables needed for the calculation of both the queue insertion and consumption. We will have a variable for the start and end of each operation and another for holding the diference, which will correspond to the execution time.

QueueHandle_t queue;
int queueSize = 10000;
unsigned long startProducing, endProducing, startConsuming, endConsuming, producingTime, consumingTime;

The setup function will start with the initialization of the serial port, followed by the initialization of the queue. For simplicity, we will use a queue of integers, with a maximum size equal to the one specified in the queueSize global variable.

The creation of the queue is followed by an error checking, just to confirm that it was possible to create it. If you need a detailed guide on how to get started with FreeRTOS queues and their creation, please consult here.

Serial.begin(112500);

queue = xQueueCreate( queueSize, sizeof( int ) );

if(queue == NULL){
  Serial.println("Error creating the queue");
}

Following the creation of the queue, we will handle the creation of the producer and consumer tasks. Please consult here for a tutorial on how to use FreeRTOS tasks on the Arduino core. We will specify the tasks functions later.

xTaskCreate(
                    producerTask,     /* Task function. */
                    "Producer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    10,               /* Priority of the task. */
                    NULL);            /* Task handle. */
xTaskCreate(
                    consumerTask,     /* Task function. */
                    "Consumer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    10,               /* Priority of the task. */
                    NULL);            /* Task handle. */

Note that both tasks were created with a priority of 10, which is greater than the priority of the setup function task, which as a value of 1 (as analyzed here). Thus, the following code should only execute after both tasks end, as long as they don’t perform any blocking call.

So, remaining code of the setup function will take care of calculating both the time taken to produce and consume the items and print these values. Naturally, this is achieved by subtracting the time when the operation started from the time when the operation ended, which we will set in each task.

producingTime = endProducing - startProducing;
Serial.println(producingTime);

consumingTime = endConsuming - startConsuming;
Serial.println(consumingTime);

Now we will specify the tasks functions code. It will be the same as before, only with some calls to the millis function to get the start and ending time of the operations.

void producerTask( void * parameter )
{
    startProducing = millis();

    for( int i = 0;i<queueSize;i++ ){
      xQueueSend(queue, &i, portMAX_DELAY);
    }

    endProducing = millis();

    vTaskDelete( NULL );

}

void consumerTask( void * parameter)
{
    startConsuming = millis();

    int element;

    for( int i = 0;i<queueSize;i++ ){
        xQueueReceive(queue, &element, portMAX_DELAY);
    }

    endConsuming = millis();

    vTaskDelete( NULL );

}

The final source code can be seen below. Note that you can experiment with the queue size to check the execution time with different values. It also has some extra prints for a better readability.

QueueHandle_t queue;
int queueSize = 10000;
unsigned long startProducing, endProducing, startConsuming, endConsuming, producingTime, consumingTime; 

void setup() {

  Serial.begin(112500);

  queue = xQueueCreate( queueSize, sizeof( int ) );

  if(queue == NULL){
    Serial.println("Error creating the queue");
  }

  xTaskCreate(
                    producerTask,     /* Task function. */
                    "Producer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    10,               /* Priority of the task. */
                    NULL);            /* Task handle. */

  xTaskCreate(
                    consumerTask,     /* Task function. */
                    "Consumer",       /* String with name of task. */
                    10000,            /* Stack size in words. */
                    NULL,             /* Parameter passed as input of the task */
                    10,               /* Priority of the task. */
                    NULL);            /* Task handle. */

    producingTime = endProducing - startProducing;
    Serial.print("Producing time: ");
    Serial.println(producingTime);

    consumingTime = endConsuming - startConsuming;
    Serial.print("Consuming time: ");
    Serial.println(consumingTime);
}

void loop() {
  delay(100000);
}

void producerTask( void * parameter )
{
    startProducing = millis();

    for( int i = 0;i<queueSize;i++ ){
      xQueueSend(queue, &i, portMAX_DELAY);
    }

    endProducing = millis();

    vTaskDelete( NULL );

}

void consumerTask( void * parameter)
{
    startConsuming = millis();

    int element;

    for( int i = 0; i<queueSize; i++ ){
        xQueueReceive(queue, &element, portMAX_DELAY);
    }

    endConsuming = millis();

    vTaskDelete( NULL );

}


Testing the code

To test the code, simply upload it to your ESP32 board and open the serial monitor. You should get an output similar to figure 1, which shows the time for executing both operations.

FreeRTOS tasks performance ESP32 Arduino.png

Figure 1 – Output of the FreeRTOS queues performance analysis program.

The values shown in figure 1 are from the tests with a queueSize of 10000 integers, which is a very considerable value. From the results, we can see that the producing and consuming times are approximately the same (around 30 ms), which is a good value taking in consideration that queues are thread safe and thus we don’t need to worry about racing conditions.

Of course that using a queue for one task to produce a big amount of elements and another to consume them afterwards may not be an optimal solution for a real scenario application, since this can be achieved with simpler data structures.

Nonetheless, the objective was just to take a look on the performance of queues to handle these kind of operations with a big amount of data.


Related Posts

Advertisements
This entry was posted in ESP32 and tagged , , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s