1294.97 자식 실행 순서 시각화를 통한 디버깅

1294.97 자식 실행 순서 시각화를 통한 디버깅

1. 실행 순서 시각화의 목적

자식 실행 순서 시각화는 Sequence와 Fallback 제어 노드가 매 Tick에서 자식 노드를 어떤 순서로, 어디까지 실행하는지를 시각적으로 표현하는 디버깅 기법이다. 반환 상태 추적이 “무엇이 반환되었는가“에 초점을 맞춘다면, 실행 순서 시각화는 “어떤 자식이 실행되었고 어떤 자식이 실행되지 않았는가“에 초점을 맞춘다. 이 기법은 특히 SequenceWithMemory의 건너뛰기 동작, ReactiveSequence의 재평가 패턴, Fallback의 조기 종료 지점을 진단하는 데 효과적이다(Faconti, 2022).

2. 실행 순서 기록기의 구현

2.1 Tick별 실행 이벤트 기록

struct ExecutionEvent {
    int tick_number;
    int execution_order;  // Tick 내 실행 순서
    std::string node_name;
    BT::NodeStatus result;
};

class ExecutionOrderTracker {
public:
    void onNodeExecuted(int tick, const std::string& name,
                        BT::NodeStatus result) {
        events_.push_back({tick, order_in_tick_++, name, result});
    }
    
    void onTickBegin() {
        order_in_tick_ = 0;
    }
    
    const std::vector<ExecutionEvent>& events() const {
        return events_;
    }

private:
    std::vector<ExecutionEvent> events_;
    int order_in_tick_ = 0;
};

2.2 추적용 래퍼 노드

class TrackedAction : public BT::SyncActionNode {
public:
    TrackedAction(const std::string& name,
                  const BT::NodeConfig& config,
                  ExecutionOrderTracker& tracker)
        : SyncActionNode(name, config), tracker_(tracker) {}
    
    BT::NodeStatus tick() override {
        BT::NodeStatus result = inner_action_->tick();
        tracker_.onNodeExecuted(current_tick_, name(), result);
        return result;
    }

private:
    ExecutionOrderTracker& tracker_;
    int current_tick_ = 0;
};

3. 텍스트 기반 시각화

3.1 타임라인 다이어그램

Tick |  action1  |  action2  |  action3  |  Sequence
-----|-----------|-----------|-----------|----------
  1  |  [1] S    |  [2] R    |           |  R
  2  |           |  [1] R    |           |  R
  3  |           |  [1] S    |  [2] R    |  R
  4  |           |           |  [1] S    |  S

각 셀의 [n]은 해당 Tick 내에서의 실행 순서를 나타낸다. 빈 셀은 해당 Tick에서 실행되지 않았음을 의미한다. 이 다이어그램은 SequenceWithMemory의 동작을 명확히 보여준다:

  • Tick 2에서 action1이 실행되지 않음 → Memory에 의한 건너뛰기
  • Tick 3에서 action1이 실행되지 않음 → 여전히 건너뜀
  • Tick 4에서 action1, action2 모두 미실행 → Memory 인덱스가 2에서 시작

3.2 ReactiveSequence의 타임라인

Tick |  condition |  action1  |  action2  |  RSeq
-----|------------|-----------|-----------|-------
  1  |  [1] S     |  [2] R    |           |  R
  2  |  [1] S     |  [2] R    |           |  R
  3  |  [1] S     |  [2] S    |  [3] R    |  R
  4  |  [1] S     |  [2] S    |  [3] S    |  S

ReactiveSequence에서는 매 Tick condition이 [1]로 실행된다. Memory 변형과 달리 condition이 한 번도 건너뛰어지지 않는다.

3.3 비교 시각화

[SequenceWithMemory]
Tick 1: action1[S] → action2[R]
Tick 2:              action2[R]        ← action1 건너뜀
Tick 3:              action2[S] → action3[R]

[ReactiveSequence]
Tick 1: cond[S] → action1[R]
Tick 2: cond[S] → action1[R]          ← cond 재평가
Tick 3: cond[S] → action1[S] → action2[R]

두 변형의 실행 패턴을 병렬 배치하면 Memory와 Reactive의 차이가 직관적으로 드러난다.

4. Fallback의 실행 순서 시각화

4.1 기본 Fallback 타임라인

Tick |  alt1     |  alt2     |  alt3     |  Fallback
-----|-----------|-----------|-----------|----------
  1  |  [1] F    |  [2] R    |           |  R
  2  |           |  [1] R    |           |  R
  3  |           |  [1] F    |  [2] R    |  R
  4  |           |           |  [1] S    |  S

FallbackNode(WithMemory)에서 alt1이 FAILURE 후 건너뛰어지고, alt2가 RUNNING을 유지하다가 FAILURE를 반환하면 alt3으로 진행한다.

4.2 ReactiveFallback 타임라인

Tick |  alt1     |  alt2     |  alt3     |  RFb
-----|-----------|-----------|-----------|------
  1  |  [1] F    |  [2] R    |           |  R
  2  |  [1] F    |  [2] R    |           |  R
  3  |  [1] S    |  [2] Halt |           |  S

ReactiveFallback에서 Tick 3의 alt1이 SUCCESS로 변하면, alt2가 Halt되고 즉시 SUCCESS를 반환한다. Halt 이벤트가 시각화에 포함된다.

5. 시각화 도구의 구현

5.1 타임라인 텍스트 생성기

class TimelineVisualizer {
public:
    void addEvent(int tick, const std::string& node,
                  BT::NodeStatus status, int order) {
        timeline_[tick][node] = {status, order};
    }
    
    void addHaltEvent(int tick, const std::string& node) {
        timeline_[tick][node] = {BT::NodeStatus::IDLE, -1};
    }
    
    void print(const std::vector<std::string>& node_names) const {
        // 헤더 출력
        std::cout << std::setw(6) << "Tick";
        for (const auto& name : node_names) {
            std::cout << " | " << std::setw(12) << name;
        }
        std::cout << std::endl;
        
        // 구분선
        std::cout << std::string(6 + (node_names.size() * 15), '-')
                  << std::endl;
        
        // 데이터 출력
        for (const auto& [tick, events] : timeline_) {
            std::cout << std::setw(6) << tick;
            for (const auto& name : node_names) {
                auto it = events.find(name);
                if (it != events.end()) {
                    auto [status, order] = it->second;
                    if (order == -1) {
                        std::cout << " | " << std::setw(12) << "Halt";
                    } else {
                        std::string cell = "[" + std::to_string(order) + "] "
                                         + statusChar(status);
                        std::cout << " | " << std::setw(12) << cell;
                    }
                } else {
                    std::cout << " | " << std::setw(12) << "";
                }
            }
            std::cout << std::endl;
        }
    }

private:
    static std::string statusChar(BT::NodeStatus s) {
        switch (s) {
            case BT::NodeStatus::SUCCESS: return "S";
            case BT::NodeStatus::FAILURE: return "F";
            case BT::NodeStatus::RUNNING: return "R";
            case BT::NodeStatus::SKIPPED: return "K";
            default: return "?";
        }
    }
    
    std::map<int, std::map<std::string,
        std::pair<BT::NodeStatus, int>>> timeline_;
};

6. Groot2를 활용한 그래픽 시각화

6.1 실시간 트리 시각화

Groot2는 행동 트리를 그래프 형태로 실시간 시각화한다. 각 노드는 현재 상태에 따라 색상이 변경된다:

상태별 색상 매핑:
  IDLE    → 회색 (미실행)
  RUNNING → 황색 (실행 중)
  SUCCESS → 녹색 (성공)
  FAILURE → 적색 (실패)

Sequence 노드의 자식 실행 순서는 좌→우 배치에 의해 시각적으로 표현되며, 현재 실행 중인 자식이 황색으로 강조된다. 이를 통해 Memory 인덱스의 현재 위치를 직관적으로 파악할 수 있다.

6.2 로그 재생

// 기록된 로그의 Groot2 재생
BT::FileLogger2 logger(tree, "execution_trace.btlog");

// 실행
while (running) {
    tree.tickOnce();
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
}

기록된 .btlog 파일을 Groot2에서 재생하면, Tick 단위로 트리의 상태 변화를 역방향/순방향으로 탐색할 수 있다. 이 기능은 간헐적 오류의 재현과 분석에 유용하다.

7. 실행 순서 이상 패턴의 식별

7.1 패턴 1: 예상외 건너뛰기

기대:
Tick 2: action1[S] → action2[R]

실제:
Tick 2:              action2[R]     ← action1이 건너뛰어짐

SequenceNode를 의도했으나 ReactiveSequence가 아닌 상황에서 action1이 건너뛰어지면, 이는 정상적 Memory 동작이다. 그러나 ReactiveSequence를 의도했다면 XML 태그를 확인해야 한다.

7.2 패턴 2: 예상외 재평가

기대 (SequenceWithMemory):
Tick 2:              action2[R]

실제:
Tick 2: action1[S] → action2[R]    ← action1이 재평가됨

Memory 동작을 의도했으나 자식이 재평가되면, <ReactiveSequence> 태그가 사용되었을 가능성이 높다.

7.3 패턴 3: 예상외 Halt

Tick 3: cond[S] → action1[R]
Tick 4: cond[S] → action1[R]
Tick 5: cond[F]                     ← 조건 변화
        action1[Halt]               ← 예상외 Halt

ReactiveSequence에서 조건 변화에 의해 행동 노드가 Halt되는 것은 정상 동작이나, 빈번한 Halt가 관찰되면 조건의 안정성을 검토해야 한다.

7.4 패턴 4: 실행되지 않는 자식

Tick 1: action1[F]
        Sequence[F]
        action2 미실행, action3 미실행     ← 조기 종료

Sequence에서 첫 번째 자식의 FAILURE가 후속 자식의 실행을 차단한다. 이것이 의도된 동작인지, 아니면 첫 번째 자식의 조기 실패가 문제인지를 판단해야 한다.

8. 실행 횟수 기반 분석

8.1 자식별 실행 횟수 히스토그램

전체 100 Tick에서의 실행 횟수:
  condition:  100회 (매 Tick 실행)      ████████████████████
  action1:     85회                     █████████████████
  action2:     42회                     ████████
  action3:     15회                     ███
  action4:      3회                     █

ReactiveSequence의 자식 실행 횟수는 위치에 따라 감소하는 패턴을 보인다. 앞쪽 자식은 매 Tick 실행되지만, 뒤쪽 자식은 앞쪽 자식의 연속적 SUCCESS에 의존하므로 실행 빈도가 낮다. 이 분포가 예상과 크게 다르면 제어 흐름의 이상을 의심할 수 있다.

8.2 Fallback 대안별 도달 빈도

전체 50 사이클에서의 대안 도달 횟수:
  alt1 (Primary):     50회 (항상 시도)    ██████████████████████████████
  alt2 (Recovery1):   12회 (24%)         ███████
  alt3 (Recovery2):    3회 (6%)          ██
  alt4 (SafeStop):     0회 (0%)          

alt4에 한 번도 도달하지 않았다면 최후 수단이 실제로 작동하는지 검증이 필요하다. 반대로 alt4에 빈번하게 도달한다면 상위 대안들의 신뢰성을 개선해야 한다.


참고 문헌

  • Colledanchise, M., & Ogren, P. (2018). Behavior Trees in Robotics and AI: An Introduction. CRC Press.
  • Faconti, D. (2022). BehaviorTree.CPP documentation and API reference. https://www.behaviortree.dev/