/ NETWORK

NS-3에서 Tracing 사용하기

NS-3에서는 tracing 기능을 통해 시뮬레이션 도중 필요한 정보를 편리하게 출력할 수 있다.

debug 빌드에서 제공하는 logging 기능이나, std::cout 등을 사용할 수도 있지만, 그러한 방식을 사용했을 때 발생할 수 있는 여러 well-known problem들이 많기 때문에 ns-3에서는 공식적으로 tracing 기능을 제공하고 있다고 한다. 예를 들어 NS_LOG 컴포넌트는 스크립트 단위로 이용 가능하므로 수많은 정보 중 필요한 정보만을 얻어내려면 추가적인 파싱 과정이 필요해지는 번거로움이 있다.

참고로 NS-3 공식에서 시뮬레이션 정보를 추출하기 위해 가장 권장하고 있는 방법이 바로 이 Tracing이다.

사용법

간단한 사용 시 AttributeConfig subsystem들과 궁합이 좋다고 한다.

Trace Source and Sink

ns-3에서는 trace source와 sink라는 개념을 정의하고 있다. Trace source는 트레이싱 대상이 될 정보를 제공하는 객체를 의미하고, trace sink는 trace source와 연결되어, 트레이싱 대상이 되는 정보를 받아서 처리 및 출력하는 객체를 의미한다. trace source가 producer라면, trace sink는 consumer라고 할 수 있다.

이런 방식은 trace source를 수정하지 않고 새로운 sink를 정의함으로써 시뮬레이터의 코어를 수정하고 재컴파일하지 않아도 되는 편리함을 제공한다.

구체적으로, source는 시뮬레이션 도중에 발생하는 이벤트를 signal로 발생시키고, 내부 데이터에 접근할 수 있도록 한다. 이를테면 TCP 모델의 congestion window가 trace source의 좋은 예시가 될 수 있다. 이 경우 congestion window가 변화할 때마다 연결된 trace sink는 이전 값과 새 값에 대한 정보를 받는다.

하나의 source에 대해 0개 이상의 sink가 존재할 수 있다. trace source를 1:多 정보 제공 링크로 보면 이해하기 쉽다. source에 연결된 trace sink가 없으면, 아무런 정보도 출력되지 않는다.

기본적으로, trace sink는 callback에 해당한다. 즉, 아무런 inter-module dependency 없이 특정 상황에 특정 함수를 호출 가능한 함수 포인터를 의미한다. trace sink가 관심을 보이는 trace source가 있으면, 자기 자신을 해당 trace source가 내부적으로 갖고 있는 callback list에 등록한다. ns-3의 Callback에 대한 자세한 설명은 공식 ns-3 매뉴얼을 참조하면 된다.

코드 작성

이번에 다룰 전체 예제 코드는 ns-3 튜토리얼에서 기본적으로 제공하는 fourth.cc에 해당한다.

#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h"
#include "ns3/trace-source-accessor.h"

위에서 잠깐 언급했듯이 Tracing 기능은 AttributeConfig과 밀접한 연관이 있으므로 가장 위의 두 줄을 추가하여 관련 헤더 파일을 include한다.

traced-value.hvalue semantics를 따르는 데이터를 추적하기 위한 선언이 작성되어 있다. Value semantics란, 일반적으로 객체에 대한 포인터 대신 객체 자체를 전달할 수 있음을 뜻한다. 이것이 진짜 의미하는 바는 TracedValue에 가해진 변화를 쉽게 추적(trace)할 수 있다는 것이다.

Tracing 시스템은 Attributes와 연결되어 있고, AttributesObject들과 함께 작동하므로 반드시 trace source는 ns-3 Object 내에 있어야만 한다. 아래의 Code snippet에서는 간단한 Object를 선언 및 정의한다.

class MyObject : public Object
{
public:
  static TypeId GetTypeId()
  {
    static TypeId tid = TypeId("MyObject")
      .SetParent(Object::GetTypeId())
      .SetGroupName("MyGroup")
      .AddConstructor<MyObject>()
      .AddTraceSource("MyInteger",
                      "An integer value to trace.",
                      MakeTraceSourceAccessor(&MyObject::m_myInt),
                      "ns3::TracedValueCallback::Int32")
      ;
    return tid;
  }

  MyObject() {}
  TracedValue<int32_t> m_myInt;
};

중요한 부분은 .AddTraceSourceTracedValue m_myInt의 선언이다.

AddTraceSource는 trace sink가 Config 시스템을 통해 상호작용 가능한 hook을 제공한다. 첫 번째 argument는 Config 시스템에 노출되는 trace source의 이름이고, 두 번째 argument는 trace source에 대한 설명이다. 세번째 argument의 argument &MyObject::m_myInt 에 주목해 보자. 이것은 class에 추가될 TracedValue에 해당한다. 네 번째 argument는 TracedValue 타입에 대한 typedef에 해당한다. 이것은 알맞은 callback 함수 시그니쳐에 대한 documentation을 생성하는 데에 사용된다고 한다.

TracedValue<> 선언은 callback 과정을 작동시키는 인프라를 제공한다. 기저의 value가 변경되면 TracedValue 메커니즘은 그 변수의 기존 값과 새 값을 모두 제공할 것이다. 이 경우에는 int32_t 값에 해당한다. 대응하는 trace sink 함수 traceSink는 다음과 같은 signature를 필요로 할 것이다.

void (*traceSink)(int32_t oldValue, int32_t newValue);

이 trace source에 hooking하는 모든 trace sink는 반드시 이 signature를 필요로 할 것이다. 즉 위의 argument 및 리턴 형식을 따라야 한다. fourth.cc를 계속 살펴보면 아래 코드가 있다.

void
IntTrace(int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

이것은 대응하는 trace sink의 정의에 해당한다. 이것은 위에 소개된 callback 함수 signature에 직접적으로 대응된다. 이것이 연결되면, TracedValue 변화 시마다 이 함수가 호출될 것이다.

여기까지 trace source와 trace sink에 대해 살펴봤으니, source를 sink에 연결하는 코드를 살펴보는 일만 남았다. 이건 main 함수에서 일어난다.

int
main(int argc, char *argv[])
{
  Ptr<MyObject> myObject = CreateObject<MyObject>();
  myObject->TraceConnectWithoutContext("MyInteger", MakeCallback(&IntTrace));

  myObject->m_myInt = 1234;
}

먼저 trace source가 있는 MyObject 인스턴스를 instantiate 한다.

TraceConnectWithoutContext는 trace source 와 trace sink간 연결을 형성한다. 첫 번째 argument는 trace source의 이름이다. 두 번째 argument의 MakeCallback 템플릿 함수를 보자. 이 함수는 ns-3 Callback 객체를 생성한 후 trace sink, 즉 IntTrace함수와 associate 해 준다. TraceConnect는 일반적으로 제공한 함수 및 trace source 이름에 의해 참조되는 trace 대상 변수의 오버로드된 operator() 사이의 관계를 생성한다. 이 관계가 생성된 후, trace source는 제공된 callback 함수를 “발사” 할 수 있다.

  myObject->m_myInt = 1234;

TracedValuem_myInt에 값을 할당하면, 즉 = 연산자를 invoke하게 되면 기존 값과 새로운 값이 argument로 전달된 IntTrace 함수가 호출될 것이다.

요약하자면, trace source는 callback의 리스트들을 가진 변수에 해당한다. trace sink는 callback의 타겟으로 사용되는 함수에 해당한다. Attribute와 object 타입 정보 시스템은 trace source를 trace sink에 연결하는 방식을 제공하는 데에 사용된다. trace source를 “hitting” 하는 행위는 trace source에 대해 operator를 실행하여 callback을 발동시키는 것에 해당한다. 이는 source에 의해 parameter가 전달되어 trace sink callback이 호출되는 결과를 가져온다.

Config과 연결

사실 위 간단한 예시에서 본 TraceConnectWithoutContext 호출은 시스템에서 거의 사용되지 않는다.

대신 Config subsystem을 통해서, Config path라고 불리는 것을 써서 시스템의 trace source를 선택하는 게 일반적이다.

Config이 사용된 예시는 이 게시글에서 확인 가능하다.

해당 게시글에서 나타난 다음의 예시 namespace path를 보자.

"/NodeList/7/$ns3::MobilityModel/CourseChange"

path의 마지막 부분은 tracing 대상이 되는 Attribute 에 해당하고, 앞의 부분은 Object에 해당한다. $ prefix는 해당 segment에 명시된 이름은 _Object__가 아니라, object type이라는 것을 나타내는 것이다. 최종적으로는 해당 type에 해당하는 실제 오브젝트를 가져오게 된다.

fourth.cc 에서 보여준 예시에서는 TracedValue라는 알려진 Callback trigger(?)를 사용했지만, CourseChange라는 trace source는 TracedCallback을 사용해서 직접 callback 리스트를 관리하고 있다. (차이점 복습)


tracing할 대상 tracing source가 무엇인지, Config path가 무엇인지 확인하려면 NS-3 공식 API 문서를 참조하라고 한다. 하지만 문서가 없는 플러그인 같은 경우, 소스 코드를 통해 확인할 수밖에 없다.

참고문헌

ns-3 튜토리얼: Tweaking

ns-3 튜토리얼: Tracing