FreeRTOSのCPU使用率測定機能の使い方

2023/11/2 修正:タイトル修正、誤記修正
2023/11/3 追加:APIのリンク追加、説明追加
2023/11/4 追加:IdleタスクのAffinity設定について調べた結果を追記
2023/11/7 追加:IdleタスクにAffinityを設定してCPU使用率を測定した結果を追記
2023/11/10 追加:CPU使用率測定機能を別ファイル化
2023/11/12 追加:ESP32に適用した結果(リンクエラー発生)を追記
2023/12/12 修正:関数uxTaskGetSnapshotAllを使用する方法を検討中に変更

## 1. はじめに
FreeRTOSにはオプションでCPU使用率を計測する機能を持っています。この機能を使ってCPU使用率を計測しするテストプログラムを作成しました。使用した環境は、以下の通りです。
| 項目 | 内容 |
|:---|:-:|
| board | Xiao rp2040 |
| platform | Arduino(earlephilhower版)|

## 2. FreeRTOSの設定変更
FreeRTOSでCPU使用率を計測するための設定方法は、下記リンク先を参考にしました。

https://mcuoneclipse.com/2018/08/02/tutorial-using-runtime-statistics-with-amazon-freertos-v10/

しかし、Arduino(earlephilhower版)に含まれるFreeRTOSを調べたところ。上記の設定の多くはすでに実装されており、計測結果を表示する部分のみを実装すれば良いことが判明しました。

## 3. CPU使用率表示機能の実装
FreeRTOSには、各タスクの状態を文字列に格納する[vTaskList関数](https://www.freertos.org/a00021.html#vTaskList)が用意されています。これにCPU使用率表示機能を追加するのが最も容易ですが、この方法ではFreeRTOS内のコードを修正することになります。今後のバージョンアップ時の対応を考えると、FreeRTOS外のコードで実現するほうが良いと考え、アプリ側にこの機能を実装する方針としました。
CPU使用率表示機能を表示する関数は、[vTaskList関数](https://www.freertos.org/a00021.html#vTaskList)の実装を参考にして、以下のように実装しました。
1. [uxTaskGetSystemState関数](https://www.freertos.org/uxTaskGetSystemState.html)を使用して、全タスクの情報が格納された構造体(struct xTASK_STATUS)を取得
1. 各タスクについて必要な情報を表示
なお、各タスクのCPU使用率(%)を計算するとき、32bitでは短時間(clock 125MHzの時、30秒程度)でオーバーフローしてしまいます。このため64bitで時間を積算し、それをもとにCPU使用率を計算するようにしました。
スケッチ全体を以下に示します。このスケッチはテスト用のため、main処理は1つのコアについては、CPUを使用し続けるようにしています。

```c:cpu_usage.ino

#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
#include <string>
#include <unordered_map>

struct RunTime64 {
  public:
    inline RunTime64(uint32_t cnt = 0) : runTime32{ cnt }, runTime64{ cnt } {}
    inline void add(uint32_t cnt) {
      runTime64 += static_cast<uint32_t>(cnt - runTime32);
      runTime32 = cnt;
    }
    inline uint64_t getValue() {
      return runTime64;
    }
  private:
    uint32_t runTime32;
    uint64_t runTime64;
};

void
show_task_list(Stream &stream)
{
  TaskStatus_t * pxTaskStatusArray = NULL;
  UBaseType_t uxArraySize;
  UBaseType_t numTasks;
  uint32_t totalRunTime32;

  uxArraySize = 8;
  while (true) {
    pxTaskStatusArray = static_cast<TaskStatus_t *>(pvPortMalloc( uxArraySize * sizeof( TaskStatus_t )));
    if (pxTaskStatusArray == NULL) {
      break;
    }
    numTasks = uxTaskGetSystemState(pxTaskStatusArray, uxArraySize, &totalRunTime32);
    if (numTasks > 0)  {
      break;
    }
    vPortFree(pxTaskStatusArray);
    uxArraySize += 8;
  }
  if (pxTaskStatusArray == NULL) {
    return;
  }
  static RunTime64 totalRunTime;
  totalRunTime.add(totalRunTime32);

  TaskStatus_t * pxTaskStatus = pxTaskStatusArray;
  float fTotalRunTime = static_cast<float>(totalRunTime.getValue());
  stream.println();
  for (uint32_t i = 0; i < numTasks; i++) {
    static constexpr char tskRUNNING_CHAR   { 'X' };
    static constexpr char tskREADY_CHAR     { 'R' };
    static constexpr char tskBLOCKED_CHAR   { 'B' };
    static constexpr char tskSUSPENDED_CHAR { 'S' };
    static constexpr char tskDELETED_CHAR   { 'D' };
    static const std::unordered_map<eTaskState, char> statusMap { { eRunning,   tskRUNNING_CHAR },
                                                                  { eReady,     tskREADY_CHAR },
                                                                  { eBlocked,   tskBLOCKED_CHAR },
                                                                  { eSuspended, tskSUSPENDED_CHAR },
                                                                  { eDeleted,   tskDELETED_CHAR },
                                                                };
    static std::unordered_map<uint32_t, RunTime64> totalTaskRunTime;
    char cStatus;
    std::string taskname(pxTaskStatus->pcTaskName, configMAX_TASK_NAME_LEN);
    const auto statusIter = statusMap.find(pxTaskStatus->eCurrentState);
    if (statusIter == statusMap.end()) {
      cStatus = ' ';
    } else {
      cStatus = statusIter->second;
    }
    const uint32_t taskNumber = static_cast<uint32_t>(pxTaskStatus->xTaskNumber);
    auto taskRunTimeIter = totalTaskRunTime.find(taskNumber);
    RunTime64 * taskTimeRef;
    if (taskRunTimeIter != totalTaskRunTime.end()) {
      taskTimeRef = &taskRunTimeIter->second;
    } else {
      totalTaskRunTime.emplace(static_cast<uint32_t>(pxTaskStatus->xTaskNumber), RunTime64());
      taskTimeRef = &(totalTaskRunTime.find(taskNumber)->second);
    }
    taskTimeRef->add(static_cast<uint32_t>(pxTaskStatus->ulRunTimeCounter));
    stream.printf("%-10s: %6u %c %6u %8.3f %08x\n", taskname.c_str(),         // タスク名
                  static_cast<uint32_t>(pxTaskStatus->xTaskNumber),           // タスク番号
                  cStatus,                                                    // 状態
                  static_cast<uint32_t>(pxTaskStatus->uxCurrentPriority),     // 優先度
                  (static_cast<float>(taskTimeRef->getValue()) * 100.0) /  fTotalRunTime, // CPU使用率
                  pxTaskStatus->uxCoreAffinityMask);                          // タスクの実行が許可されたコア番号のビットマスク
    pxTaskStatus++;
  }
  
  vPortFree(pxTaskStatusArray);
}

void
setup()
{
  Serial.begin(115200);
  while (!Serial)
  {
    ;
  }
}

void
loop()
{
  for (uint32_t i = 0; i < (1024*1024*20); i++) {
    __NOP();
  }
  show_task_list(Serial);
}

```

## 4. 動作確認結果
動作させてみたところ、以下のような結果となりました。上記のプログラムにおいて、メインループは常にCPUを使用するようになっており、Core0で動作しているCORE0とUSBタスクのCPU使用率を足すと100%に近い値となります。
なお、出力を見るとIDLE0とIDLE1というタスクがあります。名前からすると、それぞれCore0とCore1用のアイドルタスクとして作られているようですが、uxCoreAffinityMaskの値を見る限り各Core専用にはなっていません。
```
CORE0     :      2 X      4   95.241 00000001
IDLE0     :      6 X      0   49.965 ffffffff
IDLE1     :      7 R      0   50.032 ffffffff
USB       :      1 B      6    4.756 00000001
IdleCore0 :      3 S      7    0.000 00000001
Tmr Svc   :      5 B      2    0.000 ffffffff
IdleCore1 :      4 S      7    0.000 00000002

CORE0     :      2 X      4   95.241 00000001
IDLE0     :      6 X      0   49.965 ffffffff
IDLE1     :      7 R      0   50.032 ffffffff
USB       :      1 B      6    4.756 00000001
Tmr Svc   :      5 B      2    0.000 ffffffff
IdleCore1 :      4 S      7    0.000 00000002
IdleCore0 :      3 S      7    0.000 00000001
```

## 5. IDLE0とIDLE1について

Idleタスクを生成しているのは、以下の部分です。

https://github.com/earlephilhower/FreeRTOS-Kernel/blob/29fdcf092a8dd25605c7c15246b05ee153bbdaa6/tasks.c#L2727-L2797

ここで[vTaskCoreAffinitySet関数](https://www.freertos.org/symmetric-multiprocessing-introduction.html#vtaskcoreaffinityset)を呼び、タスクが実行できるコアを指定してやれば、コアに対応したアイドルタスクになります。これによって、それぞれのアイドルタスクのCPU使用率がわかるようになるはずです。
なお出力にはIdleCore0とIdleCore1というタスクもありますが、ソースコード中のコメントから判断すると、フラッシュ書き込み中用のタスクのようです。

2013/11/7 追記：
ファイル**C:\Users\%USERNAME%\AppData\Local\Arduino15\packages\rp2040\hardware\rp2040\3.6.0\libraries\FreeRTOS\lib\FreeRTOS-Kernel\tasks.c**を修正し、タスクIDLE0およびIDLE1に対して、[vTaskCoreAffinitySet関数](https://www.freertos.org/symmetric-multiprocessing-introduction.html#vtaskcoreaffinityset)を使い、動作するコアを指定してみました。
```diff_c
diff --git a/lib/FreeRTOS-Kernel/tasks.c b/lib/FreeRTOS-Kernel/tasks.c
index 2110255..d4d3715 100644
--- a/lib/FreeRTOS-Kernel/tasks.c
+++ b/lib/FreeRTOS-Kernel/tasks.c
@@ -2742,6 +2742,9 @@ static BaseType_t prvCreateIdleTasks( void )
                                                                     portPRIVILEGE_BIT,     /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                                                     pxIdleTaskStackBuffer,
                                                                     pxIdleTaskTCBBuffer ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
+                #if ( configUSE_CORE_AFFINITY == 1 )
+                    vTaskCoreAffinitySet(xIdleTaskHandle[ xCoreID ], 1 << 0);
+                #endif
                 }
 
                 #if ( configNUM_CORES > 1 )
@@ -2757,6 +2760,9 @@ static BaseType_t prvCreateIdleTasks( void )
                                                                         portPRIVILEGE_BIT,                 /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                                                         xIdleTaskStackBuffers[ xCoreID - 1 ],
                                                                         &xIdleTCBBuffers[ xCoreID - 1 ] ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
+                    #if ( configUSE_CORE_AFFINITY == 1 )
+                        vTaskCoreAffinitySet(xIdleTaskHandle[ xCoreID ], 1 << 1);
+                    #endif
                     }
                 #endif /* if ( configNUM_CORES > 1 ) */
 
@@ -2780,6 +2786,12 @@ static BaseType_t prvCreateIdleTasks( void )
                                            ( void * ) NULL,
                                            portPRIVILEGE_BIT,             /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                            &xIdleTaskHandle[ xCoreID ] ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
+                #if ( configUSE_CORE_AFFINITY == 1 )
+                    if( xReturn == pdPASS )
+                    {
+                        vTaskCoreAffinitySet(xIdleTaskHandle[ xCoreID ], 1 << 0);
+                    }
+                #endif
                 }
 
                 #if ( configNUM_CORES > 1 )
@@ -2791,6 +2803,12 @@ static BaseType_t prvCreateIdleTasks( void )
                                                ( void * ) NULL,
                                                portPRIVILEGE_BIT,             /* In effect ( tskIDLE_PRIORITY | portPRIVILEGE_BIT ), but tskIDLE_PRIORITY is zero. */
                                                &xIdleTaskHandle[ xCoreID ] ); /*lint !e961 MISRA exception, justified as it is not a redundant explicit cast to all supported compilers. */
+                    #if ( configUSE_CORE_AFFINITY == 1 )
+                        if( xReturn == pdPASS )
+                        {
+                            vTaskCoreAffinitySet(xIdleTaskHandle[ xCoreID ], 1 << 1);
+                        }
+                    #endif
                     }
                 #endif
             }
```
上記の修正後、cpu_usage.inoをビルド・実行した結果は以下のようになりました。
```
CORE0     :      2 X      4   95.356 00000001
IDLE0     :      6 R      0    0.002 00000001
IDLE1     :      7 X      0   99.995 00000002
USB       :      1 B      6    4.638 00000001
IdleCore1 :      4 S      7    0.000 00000002
IdleCore0 :      3 S      7    0.000 00000001
Tmr Svc   :      5 B      2    0.000 ffffffff
```
IDLE0のCPU使用率はほぼ0%となり、一方IDLE1のCPU使用率はほぼ100%となりました。これらの値は、それぞれのコアのCPU使用率を表していると考えてよさそうです。

## 6. CPU使用率測定機能をクラス化

他のアプリケーションに組み込めるよう上記機能を別ファイルとしました。出力表示は、CPU使用率が高いほうから表示するようにしました。

```c++:CpuUsage.hpp
#pragma once
#include <Arduino.h>
#include <FreeRTOS.h>
#include <task.h>
#include <string>
#include <unordered_map>

struct RunTime64 {
  public:
    inline RunTime64(uint32_t cnt = 0) : runTime32{ cnt }, runTime64{ cnt } {}
    inline void add(uint32_t cnt) {
      runTime64 += static_cast<uint32_t>(cnt - runTime32);
      runTime32 = cnt;
    }
    inline uint64_t getValue() const {
      return runTime64;
    }
    inline void clear() {
      runTime64 = 0;
    }
  private:
    uint32_t runTime32;
    uint64_t runTime64;
};

class CpuUsage {
  public:
    CpuUsage();
    bool measure(bool do_clear = false);
    void print(Stream &stream);
  protected:
    static inline constexpr char tskRUNNING_CHAR   { 'X' };
    static inline constexpr char tskREADY_CHAR     { 'R' };
    static inline constexpr char tskBLOCKED_CHAR   { 'B' };
    static inline constexpr char tskSUSPENDED_CHAR { 'S' };
    static inline constexpr char tskDELETED_CHAR   { 'D' };
    static const std::unordered_map<eTaskState, char> statusMap;
    struct TaskInfo {
      public:
        inline TaskInfo(const char * name, uint32_t id) : taskName { name, configMAX_TASK_NAME_LEN }, taskId{ id }
        {
        }
        const std::string taskName;
        const uint32_t taskId;
        char taskState;
        uint32_t currentPriority;
        uint32_t coreAffinityMask;
        RunTime64 runTimeCounter;
    };
    std::unordered_map<uint32_t, TaskInfo> taskInfoTable;
    RunTime64 totalRunTime;
};

```

```c++:CpuUsage.cpp
#include "CpuUsage.hpp"
#include <map>
#include <functional>

std::unordered_map<eTaskState, char> const CpuUsage::statusMap = {
  { eRunning, tskRUNNING_CHAR },
  { eReady, tskREADY_CHAR },
  { eBlocked, tskBLOCKED_CHAR },
  { eSuspended, tskSUSPENDED_CHAR },
  { eDeleted, tskDELETED_CHAR },
};

CpuUsage::CpuUsage()
  : totalRunTime{ 0 } {
}

bool
CpuUsage::measure(bool do_clear)
{
  TaskStatus_t *pxTaskStatusArray = NULL;
  UBaseType_t uxArraySize;
  UBaseType_t numTasks;
  uint32_t totalRunTime32;

  uxArraySize = 8;
  while (true) {
    pxTaskStatusArray = static_cast<TaskStatus_t *>(pvPortMalloc(uxArraySize * sizeof(TaskStatus_t)));
    if (pxTaskStatusArray == NULL) {
      break;
    }
    numTasks = uxTaskGetSystemState(pxTaskStatusArray, uxArraySize, &totalRunTime32);
    if (numTasks > 0) {
      break;
    }
    vPortFree(pxTaskStatusArray);
    uxArraySize += 8;
  }
  if (pxTaskStatusArray == NULL) {
    return false;
  }
  if (do_clear) {
    totalRunTime.clear();
  }
  totalRunTime.add(totalRunTime32);
  TaskStatus_t *pxTaskStatus = pxTaskStatusArray;
  //float fTotalRunTime = static_cast<float>(totalRunTime.getValue());
  for (uint32_t i = 0; i < numTasks; i++) {
    const uint32_t taskNumber = static_cast<uint32_t>(pxTaskStatus->xTaskNumber);
    auto taskInfoIter = taskInfoTable.find(taskNumber);
    TaskInfo *taskInfoRef;
    if (taskInfoIter != taskInfoTable.end()) {
      taskInfoRef = &taskInfoIter->second;
    } else {
      taskInfoTable.emplace(static_cast<uint32_t>(pxTaskStatus->xTaskNumber),
                            TaskInfo(pxTaskStatus->pcTaskName, static_cast<uint32_t>(pxTaskStatus->xTaskNumber)));
      taskInfoRef = &(taskInfoTable.find(taskNumber)->second);
    }
    if (do_clear) {
      taskInfoRef->runTimeCounter.clear();
    }
    taskInfoRef->runTimeCounter.add(static_cast<uint32_t>(pxTaskStatus->ulRunTimeCounter));
    char cStatus;
    const auto statusIter = statusMap.find(pxTaskStatus->eCurrentState);
    if (statusIter == statusMap.end()) {
      cStatus = ' ';
    } else {
      cStatus = statusIter->second;
    }
    taskInfoRef->taskState = cStatus;
    taskInfoRef->currentPriority = static_cast<uint32_t>(pxTaskStatus->uxCurrentPriority);
    taskInfoRef->coreAffinityMask = static_cast<uint32_t>(pxTaskStatus->uxCoreAffinityMask);
    pxTaskStatus++;
  }
  vPortFree(pxTaskStatusArray);
  return true;
}

void
CpuUsage::print(Stream &stream)
{
  std::multimap<float, const CpuUsage::TaskInfo &, std::greater_equal<float>> outputInfo;
  const float fTotalRunTime = static_cast<float>(totalRunTime.getValue());

  // ソートのため、CPU使用率をキーにしてmultimapに格納する
  for (const auto &iter : taskInfoTable) {
    const TaskInfo &taskInfo = iter.second;
    const float cpuUsage = (static_cast<float>(taskInfo.runTimeCounter.getValue()) * 100.0) / fTotalRunTime;
    outputInfo.emplace(cpuUsage, taskInfo);
  }
  //
  stream.println();
  for (const auto &iter : outputInfo) {
    const float cpuUsage = iter.first;
    const TaskInfo &taskInfo = iter.second;
    stream.printf("%-10s: %6u %c %6u %8.3f %08x\n",
                  taskInfo.taskName.c_str(),
                  static_cast<uint32_t>(taskInfo.taskId),
                  taskInfo.taskState,
                  taskInfo.currentPriority,
                  cpuUsage,
                  taskInfo.coreAffinityMask);
  }
}

```

```c++:CpuUsage.ino
#include "CpuUsage.hpp"

static CpuUsage cpuUsage;

void
setup()
{
  Serial.begin(115200);
  while (!Serial)
  {
    ;
  }
  Serial.printf("build [%s %s]\n", __DATE__, __TIME__);
} 

void
loop()
{
  for (uint32_t i = 0; i < (F_CPU / 4); i++) {
    __NOP();
  }
  cpuUsage.measure();
  cpuUsage.print(Serial);
}
```
実行結果は以下のようになりました。
```
IDLE1     :      7 X      0   99.997 00000002
CORE0     :      2 X      4   95.439 00000001
USB       :      1 B      6    4.558 00000001
IDLE0     :      6 R      0    0.000 00000001
Tmr Svc   :      5 B      2    0.000 ffffffff
IdleCore1 :      4 S      7    0.000 00000002
IdleCore0 :      3 S      7    0.000 00000001
```

## 6. 他のCPUでの動作
FreeRTOSを採用している環境であれば動作するはずと考え、ESP32(M5Stack basic)用に一部を修正し試したところ、リンク時にuxTaskGetSystemStateが未定義となってしまいました。ESP32用FreeRTOSにはuxTaskGetSnapshotAllという関数が提供されており、これを使えないかを検討中です。