AEGLOSLAB ROS 2 Jazzy 대전

제2부 실행 모델·클라이언트 라이브러리 내부

2.5 타이머의 정밀도와 드리프트

2.5.1 이 장에서 배우는 것 / 선수 지식

create_wall_timer(10ms, cb)라고 쓰면 콜백이 10 ms마다 실행되는가? 아니다. 평균은 대략 맞을 수 있지만, 매 주기는 흔들리고, 부하가 걸리면 지연되고, 지연된 것을 따라잡거나 버린다. 제어 루프를 ROS 2 타이머로 실행하는 시스템이라면 이 흔들림의 크기와 성질을 아는 것이 안전 문제다.

배우는 것:

  • 타이머의 내부 동작 — 다음 발화 시각이 어떻게 계산되는가.
  • 드리프트와 지터의 구분 — 두 가지 다른 문제이며 대처가 다르다.
  • 지연된 주기의 처리 — 따라잡는가, 버리는가, 무엇이 바람직한가.
  • 지연의 원인 사슬 — OS 스케줄러부터 콜백 실행까지 어디서 얼마나 늘어나는가.
  • 측정 방법 — 주기를 실제로 측정하고 분포를 보는 법.
  • 시뮬레이션 시간과 타이머 — 1.10의 함정을 정밀하게.
  • 대안 — 타이머로 안 되는 정밀도가 필요할 때.
  • 드리프트·지터의 실제 값(2.5.13) — executor별 꼬리 지연과 콜백 예산의 급변 지점.

선수 지식: 1.10(시계·use_sim_time), 2.2(executor 루프), 2.3(executor 선택), 2.4(콜백 그룹). 실시간 커널·스케줄링은 12.2, 지연 예산은 12.4에서 이어받는다.

정직: 타이머 구현 세부(다음 발화 시각 계산 방식·지연된 주기 처리)는 rcl_timer·rclcpp 구현에 근거하나 배포판에 따라 달라질 수 있어 (확인 필요)로 표시했다. 아래 수치 예시는 설명을 위한 것이며 측정값이 아니다. 실제 값은 하드웨어·커널·부하에 따라 자릿수가 다르다(BK-03). 자신의 시스템에서 2.5.6의 방법으로 재기 바란다.

2.5.2 타이머는 어떻게 발화하는가

타이머는 wait set의 한 엔티티이며(2.2.3), 다른 엔티티와 달리 시각으로 준비된다.

예 아니오 타이머 생성 period = T next_call_time = now + T executor가 wait 진입 timeout = next_call_time - now 타임아웃 또는 데이터로 깨어남 now >= next_ call_time? next_call_time 갱신 콜백 실행

핵심은 F단계 — next_call_time을 어떻게 갱신하는가이다. 두 가지 방식이 있다.

방식계산성질
절대 기준next += T드리프트 없음. 지연되면 따라잡기 발생
상대 기준next = now + T드리프트 누적. 따라잡기 없음

$T = 10\,\mathrm{ms}$, 콜백이 3 ms 걸리고 발화가 2 ms 늦었다고 하자.

  • 절대 기준: 0 → 10 → 20 → 30 … (실제 발화가 늦어도 목표는 고정)
  • 상대 기준: 0 → 12 → 24 → 36 … (매번 늦은 만큼 뒤로 밀림)

실행 확인(2026-08-16): Jazzy는 절대 기준이다 — 350 ms 블로킹으로 세 주기를 놓친 뒤에도 예정 시각이 100 ms 격자를 유지했다(2.5.12.2). 아래 서술은 그 결과를 전제로 읽으라. 다른 배포판·rclpy에서도 같은지는 확인하지 않았다(확인 필요). 절대 기준을 쓰되 지나치게 지연된 경우 재동기화하는 혼합 방식도 가능하다. 이 차이는 장시간 운용에서 크게 벌어지므로(1시간에 수백 ms~수 초), 정밀 주기가 필요하면 반드시 자신의 환경에서 확인해야 한다. 확인 방법은 2.5.6.

2.5.3 드리프트 vs 지터 — 다른 문제, 다른 대처

주기 문제 드리프트 평균 주기가 목표와 다름 지터 매 주기가 흔들림 원인: 상대 기준 갱신 클럭 정확도 원인: 스케줄링 executor 경합 · 부하 대처: 절대 기준 · 외부 시각 동기 대처: RT 우선순위 · executor 분리 · CPU 격리
축드리프트지터
정의장기 평균 주기의 체계적 편차개별 주기의 분산
증상1시간에 몇 초씩 밀림주기가 8~15 ms 사이에서 흔들림
영향시간 기준 계산의 누적 오차, 로그 동기 어긋남제어 성능 저하, 미분항 노이즈
대처절대 기준 갱신, 외부 시각 동기(1.10.8)RT 우선순위, CPU 격리, executor 분리

제어 루프에서는 지터의 영향이 더 크다. PID의 미분항이 $\Delta t$로 나누는데, $\Delta t$가 8 ms와 15 ms 사이에서 흔들리면 미분항이 그만큼 요동친다. 그래서 dt를 실측해 쓰는 것이 중요하다(1.10.4) — 그러면 지터가 제어 이득의 변동이 아니라 정확한 계산으로 흡수된다.

$$u_d = K_d \frac{e_k - e_{k-1}}{\Delta t_{\text{측정}}} \quad \text{(고정 } T \text{ 대신 실측값)}$$

견해: "타이머 주기 = dt"라고 쓴 제어 코드는 지터에 취약하다. 실측 dt를 쓰는 것은 몇 줄이고, 지터를 상당 부분 무해화한다. 다만 실측 dt가 이상값일 때(1.10.4의 방어) 처리를 함께 넣어야 한다.

2.5.4 지연된 주기 — 따라잡을 것인가 버릴 것인가

콜백이 15 ms 걸리는데 주기가 10 ms라면? 또는 시스템이 잠깐 멈춰 5주기를 놓쳤다면?

① 따라잡기(catch-up) — 지연된 만큼 연속 발화.

  • 장점: 총 실행 횟수가 보존된다. 계수 기반 로직에 맞다.
  • 단점: 폭주. 5주기를 놓쳤으면 콜백이 5번 연속 실행되고, 그동안 다른 콜백이 기아 상태가 된다(2.2.9). 제어에서는 위험하다 — 같은 명령이 연속으로 나간다.

② 건너뛰기(skip) — 지연된 주기를 버리고 다음 정상 시각으로.

  • 장점: 폭주 없음. 실시간 제어에 적합.
  • 단점: 실행 횟수가 줄어든다.

실행 확인(2026-08-16): Jazzy의 기본 동작은 ②(건너뛰기)다 — 세 주기를 놓쳤을 때 발화는 한 번뿐이었고 연속 폭주가 없었다(2.5.12.2). 다만 안전한 접근은 어느 쪽이든 콜백이 스스로 방어하는 것이며, 그 이유는 2.5.12.2 끝에 적었다.

void on_timer() {
  auto now = clock_->now();
  auto dt = (now - last_).seconds();
  last_ = now;

  // 정상 범위를 벗어나면 이번 주기를 건너뛴다
  if (dt < period_s_ * 0.5) {
    // 따라잡기로 인한 연속 발화 — 이번은 무시
    RCLCPP_DEBUG(get_logger(), "따라잡기 발화 감지 (dt=%.4f) — 건너뜀", dt);
    return;
  }
  if (dt > period_s_ * 3.0) {
    // 크게 밀림 — 상태를 재초기화하고 이번 계산은 생략
    RCLCPP_WARN(get_logger(), "주기 %.0f배 지연 (dt=%.4f) — 상태 재설정",
                dt / period_s_, dt);
    reset_state();
    return;
  }
  control_step(dt);
}

이 패턴은 제어 노드의 표준 방어다. 타이머의 정확한 정책을 몰라도 안전하게 동작한다.

2.5.5 지연의 원인 사슬

콜백이 목표 시각보다 늦게 실행되는 원인을 발생 순서대로 나열하면 어디를 고쳐야 할지 보인다.

목표 시각 도달 ① OS 타이머 해상도 · 스케줄러 tick ② executor 스레드가 다른 콜백 실행 중 ③ 스레드가 CPU를 못 얻음 (우선순위 · 다른 프로세스) ④ 깨어남 · 컨텍스트 스위치 ⑤ 콜백 진입 ⑥ 콜백 안에서 락 대기 · 페이지 폴트 · 할당 실제 작업
#원인전형적 크기대처
①스케줄러 해상도커널 설정 의존PREEMPT_RT·고해상도 타이머(12.2)
②다른 콜백 실행 중그 콜백의 실행 시간만큼executor·그룹 분리(2.3·2.4)
③CPU 경합부하 의존, 최악은 큼RT 우선순위·CPU 격리(12.2)
④컨텍스트 스위치마이크로초 수준코어 고정(affinity)
⑤executor 오버헤드엔티티 수 의존(2.2.7)executor 분리
⑥락·할당·페이지 폴트최악의 경우 매우 큼무할당 경로·메모리 잠금(12.3)

견해: 대부분의 시스템에서 지배적 원인은 ②와 ③이다. 즉 executor 구성과 CPU 경합이다. ①·④를 걱정하기 전에 "이 타이머가 다른 콜백과 executor를 공유하고 있는가" 를 먼저 확인하라. 전용 executor + 전용 스레드로 옮기는 것만으로 지터가 크게 줄어드는 경우가 많다(2.3.8).

⑥의 페이지 폴트가 숨은 위험 요인이다. 처음 접근하는 메모리 페이지에서 폴트가 나면 수십 마이크로초~밀리초가 소요될 수 있다. 실시간 경로에서는 mlockall()로 메모리를 잠그고 미리 워밍업하는 것이 표준 대응이다(12.3).

2.5.6 측정 — 실제 주기 분포 보기

평균만 봐서는 안 된다. 평균 10.0 ms인데 최악이 80 ms일 수 있고, 제어에서 중요한 것은 그 최악값이다.

class TimerMonitor {
public:
  void tick() {
    auto now = std::chrono::steady_clock::now();      // 단조 시계(1.10)
    if (last_.time_since_epoch().count() != 0) {
      double dt_ms = std::chrono::duration<double, std::milli>(now - last_).count();
      samples_.push_back(dt_ms);
      if (dt_ms > max_) max_ = dt_ms;
      if (dt_ms < min_) min_ = dt_ms;
    }
    last_ = now;
  }
  void report(const rclcpp::Logger & log) {
    if (samples_.empty()) return;
    std::sort(samples_.begin(), samples_.end());
    auto pct = [this](double p) { 
      return samples_[static_cast<size_t>(p * (samples_.size() - 1))]; };
    RCLCPP_INFO(log,
      "주기(ms) n=%zu min=%.2f p50=%.2f p99=%.2f p99.9=%.2f max=%.2f",
      samples_.size(), min_, pct(0.50), pct(0.99), pct(0.999), max_);
    samples_.clear(); min_ = 1e9; max_ = 0;
  }
private:
  std::chrono::steady_clock::time_point last_{};
  std::vector<double> samples_;
  double min_ = 1e9, max_ = 0;
};

보고할 것은 백분위수다. p50(중앙값)은 "보통 이렇다", p99·p99.9는 "가끔 이렇게까지 악화된다", max는 "최악". 제어 안전성을 논할 때 쓰는 값은 p99.9와 max이지 평균이 아니다.

함정: samples_를 실시간 콜백 안에서 push_back하면 재할당이 일어난다(12.3). 측정 코드가 측정 대상을 방해하는 전형적 사례다. reserve()로 미리 할당하거나 고정 크기 링버퍼를 쓴다.

외부 측정:

ros2 topic hz /some_topic --window 1000     # 발행 주기(CLI 측정 한계 있음 — 0.7.6)

가장 정확한 것은 트레이싱이다. ros2_tracing은 커널 수준에서 콜백 진입·이탈 시각을 기록하므로 측정 코드가 대상을 방해하지 않는다(11.6).

지연 분포를 보는 법:

지표의미
p50이 T보다 크다드리프트 — 구조적으로 주기를 못 지킨다
p50 ≈ T, p99가 크다지터 — 가끔 지연된다
max가 p99의 수 배드문 대형 지연 — GC·페이지 폴트·다른 프로세스
시간에 따라 증가누수·큐 적체

2.5.7 시뮬레이션 시간과 타이머

1.10.3에서 언급한 함정을 여기서 정밀하게 본다.

타이머 종류기준 시계시뮬 배속 영향
create_wall_timer(T, cb)벽시계(steady)없음 — 실시간 주기 유지
rclcpp::create_timer(node, clock, T, cb)지정한 시계use_sim_time이면 시뮬 시간

0.1배속 시뮬에서 무슨 일이 일어나는가:

wall timer 100 ms:
  실제 시간 100 ms마다 발화 → 시뮬 시간으로는 10 ms마다 발화
  → 시뮬 시간 기준으로 10배 빠르게 실행되는 셈

ROS clock timer 100 ms:
  시뮬 시간 100 ms마다 발화 → 실제로는 1초마다
  → 의도한 동작

제어 노드가 wall timer를 쓰면 시뮬에서 제어 주기가 실제와 완전히 달라진다. 시뮬로 튜닝한 게인이 실기에서 안 맞는 원인 중 하나가 이것이다(10.6).

견해: 시뮬레이션을 쓰는 시스템이라면 "제어·주기 로직에는 노드 시계 기반 타이머, 벽시계가 꼭 필요한 것(하드웨어 폴링·워치독)에만 wall timer" 를 규칙으로 두라. 그리고 이 규칙이 지켜지는지 코드 리뷰에서 확인하라 — create_wall_timer를 grep하는 것만으로 점검된다.

grep -rn "create_wall_timer" src/ | grep -v "watchdog\|hardware"

2.5.8 타이머로 안 되는 정밀도가 필요할 때

요구접근
수 ms 지터 허용일반 타이머 + executor 분리(2.3.8)
1 ms 이하 지터PREEMPT_RT + RT 우선순위 + CPU 격리(12.2)
수십 µs위 + 무할당 경로 + 메모리 잠금(12.3), ROS 2 밖 스레드 검토
마이크로초 이하ROS 2 타이머로는 불가. 하드웨어 타이머·인터럽트·FPGA·MCU(6.8)

"ROS 2 밖 스레드" 패턴: 제어 루프를 ROS 2 executor가 아니라 자체 RT 스레드로 실행하고, ROS 2와는 락프리 큐로만 통신한다. ros2_control이 정확히 이 구조다(6.6) — 제어 루프는 별도 스레드에서 실행되고, ROS 인터페이스는 그 주변에 배치된다.

// 개념 — RT 스레드가 직접 주기를 관리
void rt_loop() {
  set_realtime_priority(90);            // 12.2
  mlockall(MCL_CURRENT | MCL_FUTURE);   // 12.3
  struct timespec next;
  clock_gettime(CLOCK_MONOTONIC, &next);
  while (running_) {
    next.tv_nsec += period_ns_;
    normalize(next);
    clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, nullptr);  // 절대 기준
    control_step();                     // 할당·락·로깅 금지
  }
}

clock_nanosleep에 TIMER_ABSTIME을 쓰는 것이 핵심이다 — 절대 시각 기준이라 드리프트가 누적되지 않는다(2.5.2의 절대 기준 방식).

견해: 이 경로로 가기 전에 요구사항을 다시 확인하라. "제어 주기가 1 ms여야 한다"는 요구가 실제로는 "제어가 안정적이어야 한다"인 경우가 많고, 후자는 dt 실측(2.5.3)과 executor 분리로 해결되는 경우가 흔하다. RT 경로는 개발·검증·유지 비용이 크다.

2.5.9 타이머 관련 실무 팁

① 여러 주기가 필요하면 타이머를 나눈다 — 하나의 빠른 타이머 안에서 카운터로 분주하는 것보다, 주기별 타이머를 두는 편이 명확하고 executor가 스케줄링을 돕는다. 단, 엔티티 수가 늘어나는 비용은 있다(2.2.7).

② 타이머를 일시 정지·재개할 수 있다.

timer_->cancel();     // 정지
timer_->reset();      // 재시작 (다음 발화 시각 재계산)
bool stopped = timer_->is_canceled();

라이프사이클 노드에서 필수다(1.12.3).

③ 일회성 타이머 — 타임아웃 구현에 쓴다.

timeout_timer_ = create_wall_timer(3s, [this]() {
  timeout_timer_->cancel();       // 스스로 정지 → 일회성
  handle_timeout();
});

④ 콜백 안에서 주기를 바꿀 수 없다 — 타이머를 다시 만들어야 한다(1.9.6의 post_set 예).

⑤ 첫 발화 시각 — 타이머는 생성 시점부터 한 주기 뒤에 처음 발화한다. 즉시 한 번 실행이 필요하면 명시적으로 호출한다.

2.5.12 실측 — TimerInfo로 확인한 Jazzy 타이머의 실제 정책

번호에 대하여: 이 절은 나중에 추가되었다. 규약(CLD-03)에 따라 번호는 끝에 붙이고, 읽는 흐름을 위해 함정 카탈로그 앞에 놓았다.

2.5.2와 2.5.4가 각각 (확인 필요)로 남긴 두 질문 — 절대 기준인가 상대 기준인가, 지연된 주기를 따라잡는가 버리는가 — 을 실측으로 확정한다.

2.5.12.1 rclcpp::TimerInfo — Jazzy가 준 측정 수단

Jazzy에서 타이머 콜백이 예정 시각과 실제 호출 시각을 함께 받을 수 있게 됐다(0.3.4). 헤더는 이렇다 — 원문 확인.

namespace rclcpp {
struct TimerInfo {
  Time expected_call_time;   // 이 발화가 "예정되어 있던" 시각
  Time actual_call_time;     // 실제로 콜백이 불린 시각
};
}

콜백을 인자 없는 형태로 쓰던 것을 const TimerInfo &를 받는 형태로 바꾸기만 하면 된다(rclcpp::TimerInfoCallbackType).

timer_ = create_timer(100ms, [this](const rclcpp::TimerInfo & info) {
  auto lateness = (info.actual_call_time - info.expected_call_time).seconds();
  if (lateness > 0.010) {
    RCLCPP_WARN(get_logger(), "타이머 %.1f ms 지연", lateness * 1000);
  }
  control_step();
});

이 한 줄이 2.5.6의 측정 코드를 상당 부분 대체한다. 직접 이전 시각을 들고 있으며 dt를 계산하는 대신, 프레임워크가 "예정과 실제"를 알려 준다 — 지연을 측정하는 정확한 방법이다. 다만 이것은 발화 지연을 측정하는 것이지 콜백 소요 시간이 아니다(그것은 여전히 직접 측정해야 한다).

2.5.12.2 실측 — 3주기를 일부러 놓쳐 보면

100 ms 타이머를 설정하고 다섯 번째 콜백에서 350 ms를 블로킹해 세 주기를 놓치게 했다.

#01 expected=0.100 actual=0.100 (지연 0.000)
…
#05 expected=0.500 actual=0.500 (지연 0.000)
  ↳ 350ms 블로킹 (3주기 유실 유도)
#06 expected=0.600 actual=0.850 (지연 0.250)
#07 expected=0.900 actual=0.900 (지연 0.000)
#08 expected=1.000 actual=1.000 (지연 0.000)

실행 확인(2026-08-16). Jazzy 컨테이너에서 위 코드를 컴파일·실행한 결과다.

세 가지가 한꺼번에 드러난다.

질문답근거
절대 기준인가 상대 기준인가절대 기준expected가 0.100·0.200…0.900·1.000으로 100 ms 격자를 유지한다. 350 ms 블로킹 뒤에도 격자가 어긋나지 않았다
지연된 주기를 따라잡는가아니다 — 버린다0.600·0.700·0.800 세 주기가 지났는데 발화는 한 번(#06)뿐이고, 다음은 0.900이다. 연속 폭주가 없다
지연된 발화의 expected는?가장 오래된 놓친 주기의 시각#06의 expected가 0.600(놓친 첫 주기)이고 actual이 0.850이다 — 지연 0.250이 그대로 보인다

즉 Jazzy의 타이머는 "절대 기준 + 재동기화" 로 동작한다 — 2.5.2가 "혼합 방식도 가능하다"고 적어 둔 바로 그 형태다.

실무적으로 좋은 소식이다. 2.5.4가 경고한 따라잡기 폭주는 기본 동작에서 일어나지 않는다. 5주기를 놓쳐도 콜백이 5번 연속 실행되지 않으므로, 제어 루프가 지연된 뒤 같은 명령을 연달아 대량 송출하는 사고는 최소한 타이머 계층에서는 생기지 않는다.

그럼에도 2.5.4의 방어 코드는 여전히 값을 한다. 이유 셋. ① 이 결과는 Jazzy 기준이며 다른 배포판·rclpy·다른 실행기 구성에서 같다는 보장이 없다(확인 필요). ② 건너뛰기라도 한 번의 발화가 크게 늦게 올 수 있다(위의 0.250 s) — dt > period × 3 분기는 그것을 검출한다. ③ 이제는 TimerInfo로 지연을 직접 알 수 있으므로, 방어 조건을 추측이 아니라 실제 지연값으로 쓸 수 있다.

견해: 제어 노드라면 TimerInfo 콜백을 기본으로 쓰라. 인자 하나 추가하는 비용으로 "이 주기가 얼마나 늦었나"를 항상 알 수 있고, 그 값을 진단으로 내보내면(6.9) 현장에서 주기 문제를 사후에 재구성할 수 있다. 11.7.12의 토픽 통계가 수신 쪽 주기를 보여 준다면, TimerInfo는 실행 쪽 주기를 보여 준다.

2.5.13 드리프트·지터에 숫자를 부여했다 — 실측

2.5.12는 타이머의 정책(절대 기준·건너뛰기)을 확인했다. 이 절은 그래서 실제로 얼마나 정확한가를 측정한다. 목표 주기 10 ms, 1500회(15초), -O2(실행 확인, Jazzy).

2.5.13.1 결과

구성평균p50p99최대누적 오차
SingleThreadedExecutor · 빈 콜백10.00010.00010.04710.528+0.0 ms (0.000%)
MultiThreadedExecutor(4) · 빈 콜백10.00010.00010.04811.161+0.0 ms
StaticSingleThreadedExecutor · 빈 콜백10.0009.99910.04010.125−0.0 ms
Single · 콜백이 5 ms 소모10.0009.99910.07510.179+0.0 ms
Single · 콜백이 12 ms 소모12.07212.06612.12312.176+3,105 ms (+20.7%)

(단위 ms. 누적 오차 = 실제 총 경과 − 이상적 총 경과)

2.5.13.2 드리프트는 정말 0이었다

1500 주기 동안 누적 오차가 ±0.0 ms, 백분율로 0.000%다. 2.5.2의 "절대 기준 → 드리프트 없음"이 수치로 확인된다.

이 값을 상대 기준과 비교해 보면 차이가 실감된다. 매 주기 평균 0.05 ms씩만 지연돼도 상대 기준이라면 1시간에 $0.05\,\text{ms} \times 360{,}000 = 18\,\text{초}$가 어긋난다. Jazzy 타이머에서는 그 항이 전혀 없다.

그러므로 "장시간 운용에서 로그 타임스탬프가 조금씩 어긋난다" 같은 증상은 타이머 드리프트가 아니다. 시계 동기(1.10.8)나 스탬프 생성 지점(2.5.7)을 보라.

2.5.13.3 지터의 실제 크기 — 그리고 executor가 그것을 바꾼다

2.5.6은 "p50·p99·max를 보라"고 했지만 기준값이 없었다. 평범한 컨테이너·비-RT 커널에서의 값이 위 표다.

executorp99 − 목표최대 − 목표
StaticSingleThreadedExecutor+0.040 ms+0.125 ms
SingleThreadedExecutor+0.047 ms+0.528 ms
MultiThreadedExecutor(4)+0.048 ms+1.161 ms

세 executor의 p50·평균은 같고 꼬리만 다르다. 순서도 분명하다 — Static이 가장 좁고, Multi가 가장 넓다.

MultiThreadedExecutor가 타이머 하나에는 손해다. 스레드가 넷이어도 할 일은 하나인데, 깨우기·경합 비용이 꼬리에 더해진다(최대 지연이 Single의 2배). 2.3의 선택 기준에 이 축을 더한다 — "콜백이 서로 블로킹하지 않는가"가 기준이지 "스레드가 많으면 빠른가"가 아니다.

StaticSingleThreadedExecutor의 이득이 여기 있었다. 12.3.12.1은 이 executor가 할당 횟수에서는 이득이 없다(회당 9회로 동일)고 적었는데, 꼬리 지연에서는 가장 좋다. 두 축의 결과가 다르므로 함께 읽어야 한다.

2.5.13.4 콜백 실행 시간의 영향은 급격한 붕괴다

콜백 소모달성 주기누적 오차
0 ms10.000 ms0.000%
5 ms(주기의 50%)10.000 ms0.000%
12 ms(주기의 120%)12.072 ms+20.7%

주기의 절반을 써도 아무 영향이 없다. 그러다 주기를 넘는 순간 주기가 콜백 시간에 눌린다 — 12 ms 콜백이면 12.07 ms 주기가 되고, 따라잡기는 일어나지 않는다(2.5.12.2의 건너뛰기 정책과 일치).

예산 기준을 이렇게 세우라. "콜백이 주기의 몇 %를 쓰는가"를 측정하고, 100%를 넘기지 않는 한 주기는 지켜진다. 다만 넘는 순간의 저하가 완만하지 않으므로(주기가 곧바로 콜백 시간이 된다) 여유를 얇게 두지 말라 — 12.4의 지연 예산이 이 지점을 다룬다.

2.5.13.5 확인하지 못한 것

  • RT 커널·chrt 적용 시의 값은 측정하지 않았다(확인 필요) — 위는 일반 커널·컨테이너 값이고, 12.2의 설정을 적용하면 꼬리가 좁아질 것으로 보이나 확인하지 않았다.
  • 부하가 걸린 시스템에서의 값도 측정하지 않았다 — 측정 중 다른 작업이 거의 없었다.
  • rclpy 타이머는 측정하지 않았다(확인 필요).

2.5.10 함정 카탈로그

#함정증상대처
1주기 상수를 dt로 사용지터가 제어에 그대로 유입실측 dt(2.5.3)
2시뮬에서 wall timer제어 주기가 실제와 다름노드 시계 타이머
3평균 주기만 확인최악 지연을 못 봄p99·max 측정
4측정 코드가 할당측정이 대상을 방해reserve·링버퍼
5지연된 주기 미방어따라잡기 폭주 또는 상태 오염dt 범위 검사
6제어 타이머를 공용 executor에지터전용 executor·스레드
7콜백이 주기보다 김다른 콜백 기아(2.2.9)경량화·분리
8페이지 폴트 미고려드문 대형 지연mlockall·워밍업(12.3)
9라이프사이클에서 타이머 미정지비활성인데 동작cancel()(1.12.4)
10주기 변경을 파라미터로 시도반영 안 됨타이머 재생성
11µs 정밀도를 ROS 타이머에 기대요구 미달RT 스레드·하드웨어
12절대/상대 기준 확인 없이 장시간 운용누적 드리프트실측 확인(2.5.6)
13지연된 주기를 따라잡을 것으로 기대Jazzy는 건너뛴다(절대 기준 격자 유지)실측 표 2.5.12
14타이머 하나에 MultiThreadedExecutor꼬리 지연이 2배콜백이 서로 막지 않으면 단일로(2.5.13.3)
15장시간 어긋남을 타이머 드리프트로 의심드리프트는 0.000%시계 동기·스탬프 지점을 보라(2.5.13.2)

2.5.11 체크포인트

  • 타이머의 다음 발화 시각 계산 방식 두 가지와, 그 차이가 장시간 운용에서 어떻게 나타나는지 말할 수 있는가.
  • 드리프트와 지터를 정의로 구분하고, 각각의 원인과 대처를 댈 수 있는가. 제어에서 어느 쪽이 더 아픈가.
  • 실측 dt를 쓰는 것이 지터를 무해화하는 원리를 수식으로 설명할 수 있는가.
  • 지연된 주기를 따라잡는 방식과 버리는 방식의 장단은? 어느 쪽이든 안전한 콜백 방어 코드를 쓸 수 있는가.
  • 타이머 지연의 원인 여섯 단계를 순서대로 말하고, 대부분의 시스템에서 지배적인 둘이 무엇인지 답할 수 있는가.
  • 주기를 측정할 때 평균이 아니라 백분위수를 봐야 하는 이유는? 제어 안전성 논의에 쓰는 값은?
  • 측정 코드 자체가 측정을 방해하는 전형적 실수는 무엇인가.
  • 0.1배속 시뮬레이션에서 wall timer와 노드 시계 타이머가 각각 어떻게 실행되는지 설명할 수 있는가.
  • 코드베이스에서 시뮬 관련 타이머 문제를 한 줄 명령으로 점검하는 방법은?
  • 마이크로초 수준 정밀도가 필요할 때의 접근과, 그 경로로 가기 전에 확인할 것은 무엇인가.
  • clock_nanosleep에 TIMER_ABSTIME을 쓰는 이유는?
  • 1500 주기 동안 누적 드리프트는 얼마였는가. 그 값이 진단에 뜻하는 바는(2.5.13.2).
  • 세 executor의 p50은 같은데 무엇이 달랐는가. 타이머 하나에 어느 것을 쓸 것인가(2.5.13.3).
  • 콜백이 주기의 50%를 쓸 때와 120%를 쓸 때 달성 주기는 각각 어떻게 되는가(2.5.13.4).

다음 장 2.6은 지터의 숨은 원인 중 하나였던 메모리 — allocator, 할당이 일어나는 지점, loaned message — 를 다룬다.