21.10.1.1.1. `perf_alloc(PC_ELAPSED, "module_run_latency")` 카운터 할당 후 `perf_begin()`, `perf_end()` 매크로를 이용해 `Run()` 루프의 실행 소요 시간을 나노초 단위로 측정하는 방법

21.10.1.1.1. perf_alloc(PC_ELAPSED, "module_run_latency") 카운터 할당 후 perf_begin(), perf_end() 매크로를 이용해 Run() 루프의 실행 소요 시간을 나노초 단위로 측정하는 방법

우리는 이전 장에서 성능 카운터(Performance Counter)의 뼈대를 어떻게 세우고 터미널에서 어떻게 확인하는지 전체적인 숲을 보았다. 이제 현업 개발자들의 방식대로, 내 C++ 소스 코드 파일(Header와 CPP)을 열고 정확히 어느 줄(Line)에 이 타이머 객체(perf_alloc)를 삽입하고 어떠한 매크로(perf_begin, perf_end)로 코드를 감싸는지 그 해부학적 디테일을 완성해 보자.

결론부터 말하자면, 성능 측정 코드는 모듈의 비즈니스 로직(센서 읽기, 판단, 제어)을 단 한 줄도 건드리지 않으면서 바깥 껍데기만 살짝 감싸는 형태로 우아하게 밀어 넣어야 한다.

1. 헤더 파일: 타이머 변수 은닉하기

가장 먼저 PayloadAutoDrop.hpp (클래스 선언부) 파일을 연다. 모듈이 살아있는 동안 영원히 기록을 누적할 64비트짜리 특수 구조체 포인터(perf_counter_t)를 프라이빗(Private) 영역 깊숙이 숨겨준다.

#pragma once
// 성능 카운터 시스템을 사용하기 위한 필수 인클루드
#include <perf/perf_counter.h> 

class PayloadAutoDrop : public ModuleBase<PayloadAutoDrop>, public ModuleParams {
public:
    PayloadAutoDrop();
    ~PayloadAutoDrop();
    // ... 기존 메서드들 ...

private:
    void Run() override;

    // [핵심 추가] 내 메인 루프의 실행 시간을 잴 스톱워치 객체
    perf_counter_t _loop_perf{nullptr}; 
};

2. 생성자와 소멸자: 지독한 메모리 관리

C++ 개발의 기본은 자원(Resource)의 철저한 대여와 반납이다. PayloadAutoDrop.cpp 파일의 생성자(Constructor)에서 커널에게 “내 스톱워치 하나 만들어주소” 하고 구걸(perf_alloc)해야 한다.

PayloadAutoDrop::PayloadAutoDrop() :
    ModuleParams(nullptr)
{
    // PC_ELAPSED: "시작부터 끝날 때까지 걸린 시간을 재주세요" 라는 뜻.
    // "payload_run_latency": NSH 터미널에서 `perf`를 쳤을 때 화면에 뜰 내 이름표.
    _loop_perf = perf_alloc(PC_ELAPSED, "payload_run_latency");
    
    if (_loop_perf == nullptr) {
        // 커널 메모리가 꽉 차서 카운터 할당을 거절당하는 극소수의 경우를 대비
        PX4_ERR("Failed to allocate performance counter!");
    }
}

이제 이 객체가 죽을 때, 즉 시스템 셧다운이나 강제 종료 시 호출되는 소멸자(Destructor)에서 빌렸던 스톱워치를 커널에 깔끔하게 반납(perf_free)한다.

PayloadAutoDrop::~PayloadAutoDrop() {
    // 힙 메모리 릭(Leak)을 막기 위한 필수 호출
    perf_free(_loop_perf);
}

3. 마이크로 타게팅: perf_begin()perf_end()

마지막 하이라이트다. 수만 줄의 코드 중 과연 “내가 진정으로 소요 시간을 알고 싶은 핵심 코어가 어디인가?“를 잘 짚어내야 한다.
우리의 목표는 QGC 파라미터가 튀거나, FSM 조건문에 진입하거나, 아니면 uORB 메시지를 긁어오는 그 찰나의 순간들이 혹시라도 스케줄링 간격(20ms)을 넘겨버리는 대참사(Deadline Miss)를 일으키진 않는지 감시하는 것이다.

따라서 20ms마다 주기적으로 호출되는 심장부인 Run() 함수의 최상단과 최하단을 매크로로 감싼다.

void PayloadAutoDrop::Run() {
    
    // [타이머 시작]
    // 이 매크로가 불리는 순간, CPU의 하드웨어 클록 틱(Tick)을 읽어와 내부에 임시 저장한다.
    // 이 연산 자체는 나노초(ns) 레벨이므로 시스템에 전혀 무리를 주지 않는다.
    perf_begin(_loop_perf);

    // ---------------------------------------------------------
    // 여기서부터 우리가 앞서 짰던 수백 줄짜리 FSM 비즈니스 로직 시작!
    
    // 1. 센서 및 uORB 토픽 업데이트 (예: 고도, 나침반 등)
    if (_local_pos_sub.updated()) { ... }
    
    // 2. 파라미터 업데이트 확인
    if (_parameter_update_sub.updated()) { ... }
    
    // 3. FSM State Machine Switch-Case 문 진입
    switch (_current_state) {
        case AppState::TRIGGER:
            // 투하 로직 (여기서 만약 무한 루프나 지연 버그가 있다면 걸린다!)
            break;
        ...
    }
    // ---------------------------------------------------------
    
    // [타이머 종료]
    // 현재 하드웨어 클록 틱과 시작 시점의 틱을 빼서, 
    // 나노초 단위의 '진짜 소요 시간'을 계산한 뒤 "_loop_perf" 누적통에 던져 넣는다.
    perf_end(_loop_perf);
}

이제 코드를 빌드(make px4_fmu-vX default)하고 드론 포트에 꽂아보자.
내 코드가 구동될 때마다 이 보이지 않는 나노초 단위의 누적 기록기(perf)는 숫자를 모아줄 것이다. 만약 내 Run() 블록 안의 코드 누군가가 1초(1,000,000us) 넘게 통행로를 막고 브레이크를 밟고 있었다면, NSH perf 명령어 출력의 max 수치에 여지없이 그 죄악이 기록될 것이다.

단일 모듈 단위에서의 현미경 룩업(Lookup)을 넘어섰다면, 이제 기기를 한발 물러서서 조망해 보자.
내 모듈이 혹시 CPU 코어를 100% 잡아먹어 다른 중요한 데몬들을 질식시키고 있지는 않은가? 시스템 전체 관점에서 내 코드를 심판대 위에 올리는 top, free 명령어 활용 트러블슈팅을 21.10.2장에서 맛보자.