ns3のトレーシングシステム

【　[ns-3.30の使い方](https://qiita.com/dorapon2000/items/72dcc08eb3c8857f5603) → [1](https://qiita.com/dorapon2000/items/5c0c0a399aeee629be63) → [2](https://qiita.com/dorapon2000/items/4b51c1776f65b3fc7d26) → [3](https://qiita.com/dorapon2000/items/87c1037f7da7c1676837) → [4](https://qiita.com/dorapon2000/items/4b95e5658d537c2b5e72) → [5](https://qiita.com/dorapon2000/items/fe8151f7ee6bcc30c72e) → [6] → [7](https://qiita.com/dorapon2000/items/fadabf05a9c947bee87c)　】

ネットワークシミュレータであるns3の説明をいくつかにわけて投稿しています．この投稿は「6. トレーシングシステム」です．

# 6. トレーシングシステム

ns3ではシミュレーション中の様々な変数をトレースし，ファイルに出力することができます．例えば，移動するノードの座標やTCPの輻輳窓サイズなどです．この記事では，ns3のトレーシングシステムについて説明します．

- ノードの座標をトレースする
- トレースの仕組み
- トレースに関するコードの書き方
- ファイルに出力
- mid-levelヘルパーの使い方
- まとめ

### 文献

- https://www.nsnam.org/docs/release/3.30/tutorial/html/tracing.html
- https://www.nsnam.org/docs/release/3.30/manual/html/tracing.html

## ノードの座標をトレースする

[third.cc](https://gitlab.com/nsnam/ns-3-dev/blob/ns-3.30/examples/tutorial/third.cc)ではモバイルノードがランダムウォークしていました．scratch/mythird.cc（examples/tutorial/third.ccを単にコピーしたもの）を書き換えて，通信をしているモバイルノード（n7）の座標を画面に出力させます．

40行目NS_LOG_COMPONENT_DEFINE()の直後に以下のコードを追加します．

```c++:mythird.cc
void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  Vector position = model->GetPosition ();
  NS_LOG_UNCOND (context <<
    " x = " << position.x << ", y = " << position.y);
}
```

191行目Simulator::Run()の直前に以下のコードを追加します．

```c++:mythird.cc
std::ostringstream oss;
oss <<
  "/NodeList/" << wifiStaNodes.Get (nWifi - 1)->GetId () <<
  "/$ns3::MobilityModel/CourseChange";

Config::Connect (oss.str (), MakeCallback (&CourseChange));
```

<img width="776" alt="trace_mythirdv2_diff.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/98fc7a1c-4ddd-8d77-d404-e0f95a489995.png">


これで実行してみます．

```sh
$ ./waf --run mythird
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10, y = 0
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.3841, y = 0.923277
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.2049, y = 1.90708
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.8136, y = 1.11368
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.8452, y = 2.11318
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.9797, y = 3.10409
At time 2s client sent 1024 bytes to 10.1.2.4 port 9
At time 2.01796s server received 1024 bytes from 10.1.3.3 port 49153
At time 2.01796s server sent 1024 bytes to 10.1.3.3 port 49153
At time 2.03364s client received 1024 bytes from 10.1.2.4 port 9
/NodeList/7/$ns3::MobilityModel/CourseChange x = 11.3273, y = 4.04175
/NodeList/7/$ns3::MobilityModel/CourseChange x = 12.013, y = 4.76955
/NodeList/7/$ns3::MobilityModel/CourseChange x = 12.4317, y = 5.67771
/NodeList/7/$ns3::MobilityModel/CourseChange x = 11.4607, y = 5.91681
...
```

無事，n7の座標情報が表示されました．

## トレースの仕組み

上述の追加コードで行なっていたことを端的に説明すると，「自作したトレースシンク関数CourseChange()をトレースソースCourseChangeへフックし，変数の値が変わるたびにコールバック[^1]としてトレースシンク関数が呼ばれる．」です．トレースソース・トレースシンク・フックについてそれぞれ説明します．

トレースソースとは，ns3側で用意されているトレースできる変数の名前のことです．上の例ではMobilityModelクラスが持つCourseChangeというトレースソースを利用して，座標情報を取得しましした．つまり__API__です．トレースシンクは，トレースソースから情報を取得して処理する関数で，40行目以下に追加したCourseChange()のことです．トレースシンクがどのトレースソースから情報を取得する関数であるかを関連づける行為をフックと呼びます．フックされたトレースシンク関数は，トレース対象である変数の値が変わるたびにコールバックとして呼び出されます．

<img width="583" alt="tracing_coursechange.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/9acdba0b-acb6-5c85-8d5c-ca1be90c53f9.png">


mythird.ccのコードを見ていきましょう．

### トレースシンク

```c++:mythird.cc
void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  Vector position = model->GetPosition ();
  NS_LOG_UNCOND (context <<
    " x = " << position.x << ", y = " << position.y);
}
```

トレースシンク関数です．引数の[model](https://www.nsnam.org/docs/release/3.30/doxygen/classns3_1_1_mobility_model.html)から位置情報を取得しNS_LOG_UNCONDで画面に出力しています．関数の型はトレースソースごとに決まっていて，調べ方は後述します．contextは次で説明するコンフィグパスです．

### コンフィグパスとフック

```c++:mythird.cc
std::ostringstream oss;
oss <<
  "/NodeList/" << wifiStaNodes.Get (nWifi - 1)->GetId () <<
  "/$ns3::MobilityModel/CourseChange";

Config::Connect (oss.str (), MakeCallback (&CourseChange));
```

モバイルノードの座標を出力すると言えど，どのモバイルノードの座標かを示す必要があります．ns3ではそれをコンフィグパスというファイルパスのような仕組みを使って特定します．上のコードのossには`/NodeList/7/$ns3::MobilityModel/CourseChange`という値が入り（ノード数をオプションで変更していない場合），「シミュレーションに参加する全ノードの7番目（n7）のMobilityModelオブジェクトのトレースソースCourseChange」を意味します．ノード番号はPtr\<Node\>のGetId()メソッドで取得できます．$マークはns3オブジェクトを表します．以上で，欲しい情報はn7の座標であることが特定できました．

コンフィグパスのトレースソースと自作したトレースシンク関数をConfig::Connect()関数でフックします．フックするトレースシンク関数はコールバック関数である必要があるため，ns3のMakeCallback()関数でコールバックに変換しています．コンフィグパスの調べ方は後述します．

トレースシンク関数をフックしたことにより，トレース対象の変数m_courseChangeTraceの値が変わるたびにCourseChange()が呼ばれ，画面に座標情報が出力されることになります．

> 自作したトレースシンク関数CourseChange()をトレースソースCourseChangeへフックし，変数の値が変わるたびにコールバックとしてトレースシンク関数が呼ばれる．

理解できたでしょうか．

<img width="194" alt="hook.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/cf42ef1d-e76f-0ee7-bf1e-dfd4c8296901.png">


## トレースに関する3つの疑問

3つの疑問に答えながらトレースの手順を説明します．

1. どのようなトレースソースがあるか
2. 書くべきトレースシンク関数の引数の型はなにか
3. コンフィグパスは何か

### 疑問1. どのようなトレースソースがあるか

なによりもまず，どんなトレースソースがあるか分からなければ利用できません．[API Documentation](https://www.nsnam.org/docs/release/3.30/doxygen/index.html)の[All TraceSources](https://www.nsnam.org/docs/release/3.30/doxygen/_trace_source_list.html)に全トレースソースが掲載されています．

![alltracesources.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/9c2c3a09-591d-7757-525f-303b2d385b6f.png)


クラスのドキュメントにもクラスに属するトレースソースがリストされています．

<img width="387" alt="movilitymodel_doc_to_tracesource.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/84e40d96-6c47-4d72-92fa-d1499ccff635.png">


お目当てのトレースソースがなければ残念ながら自作するしかありません．トレースソースの作り方はモジュール開発の記事で扱います．

### 疑問2. 書くべきトレースシンク関数の型は何か

トレースソースがわかれば，対応するトレースシンク関数を書く必要がありますが，関数の型はどうすればいいでしょうか．トレースソースの説明の[Callback signature](https://www.nsnam.org/docs/release/3.30/doxygen/classns3_1_1_mobility_model.html#a1bcfc7397046b2b04713a52884a417af)をクリックするとわかります．

![mobilitymodel_tracesource.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/0a347257-7049-c835-13ee-b78a270c07cc.png)


![coursechange_callbacksignature.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/9fa7da82-fa34-1b97-222c-5f0d37d34533.png)


`Ptr<const MobilityModel> model`が引数であることがわかりました．mythird.ccに追加したトレースシンクCourseChange()の型は，それにcontextを追加したものです．

```c++:mythird.cc
CourseChange (std::string context, Ptr<const MobilityModel> model)
```

コンフィグパスを表すcontextはConfig::Connect()でフックした際に自然と挿入される引数です．いらない場合は，フックする際にConfig::ConnectWithoutContext()を使い，トレースシンク関数の引数からcontextを削ります．

```c++
CourseChange (Ptr<const MobilityModel> model)
```

### 疑問3. コンフィグパスは何か

トレースシンク関数が書いたあとはフックするだけです．コンフィグパスはTraceSourcesの少し上にあります．

![mobilitymodel_configpaths.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/a66cfda6-8634-5e9d-41c0-3f2555df77d2.png)

[i]にはノード番号が入ります．したがって，ノード7の座標を知りたいときのパスは`/NodeList/7/$ns3::MobilityModel/CourseChange`です．mythird.ccに記述したパスと一致しています．

中には`/NodeList/[i]/DeviceList/[i]/$`とネットデバイス番号まで指定するものもあります．例えば，ノード番号3でネットデバイス番号0のコンフィグパスは`/NodeList/3/DeviceList/0`です．モビリティモデルは，デバイスごとではなくノードごとに設定するものなので，デバイス番号は必要ありません．また，全てのノードや全てのネットデバイスを表すには\*を使います．`/NodeList/*/DeviceList/*`

## ファイルに出力

実践的には，画面ではなくファイルに出力したいことが多いと思います．ns3ではファイルに出力するために，mid-levelヘルパーというヘルパー関数があります．

CourseChange()を以下のように変更してください．

```c++:myfirst.cc
void
CourseChange (Ptr<OutputStreamWrapper> stream, std::string context, Ptr<const MobilityModel> model)
{
  Vector position = model->GetPosition ();
  *stream->GetStream () << context << " x = " << position.x << ", y = " << position.y << std::endl;
}
```

Config::Connectの部分を以下のように変更してください．

```c++:mythird.cc
  AsciiTraceHelper asciiTraceHelper;
  Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("third.tr");

  std::ostringstream oss;
  oss <<
    "/NodeList/" << wifiStaNodes.Get (nWifi - 1)->GetId () <<
    "/$ns3::MobilityModel/CourseChange";

  Config::Connect (oss.str (), MakeBoundCallback (&CourseChange, stream));
```

<img width="558" alt="trace_mythirdv3_diff.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/b0a27fea-cdc1-0cdf-f3db-86fc8ca60eab.png">


実行してみます．

```sh
$ ./waf --run mythird.cc
At time 2s client sent 1024 bytes to 10.1.2.4 port 9
At time 2.01796s server received 1024 bytes from 10.1.3.3 port 49153
At time 2.01796s server sent 1024 bytes to 10.1.3.3 port 49153
At time 2.03364s client received 1024 bytes from 10.1.2.4 port 9
$ head -5 third.tr
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10, y = 0
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.3841, y = 0.923277
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.2049, y = 1.90708
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.8136, y = 1.11368
/NodeList/7/$ns3::MobilityModel/CourseChange x = 10.8452, y = 2.11318
```

座標情報がthird.trというファイルに出力されていることが確認できます．[gnuplot](http://www.gnuplot.info/)で動いた道筋を図にしてみます．

```sh
$ gnuplot
gnuplot> set terminal png size 640,480
gnuplot> set output "third.png"
gnuplot> plot 'third.tr' using 4:7 title "n7 Walk Path" with linespoints
gnuplot> exit
$ xdg-open third.png
```

![third.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32402/999f2c21-9135-4bdf-0288-b0fcb1aa64ec.png)


モバイルノードn7はスタート地点(10,0)から確かにランダムウォークしているようです．

## mid-levelヘルパーの使い方

1. AsciiTraceHelperを使ってOutputStreamWrapperを生成
2. OutputStreamWrapperをトレースシンク関数へ追加の引数として渡す
3. トレースシンク関数内でOutputStreamWrapper::GetStream()を使って標準出力のように記述する
4. 出力先がファイルになっている

OutputStreamWrapperは単に[std::ostream](https://cpprefjp.github.io/reference/ostream.html)をラップしているだけ，などと難しいことは述べません．CreateFileStream()の引数にファイル名を指定し，OutputStreamWrapper::GetStream()を使えばファイル出力ができる，ということを抑えればもう使い始められます．MakeBoundCallback()はトレースシンク関数の引数を増やしています．

トレースソースから提供される情報がパケットのときは，pcapファイルへ出力することもできます．詳しい仕組みを知りたい方は，公式チュートリアルの[Tracing#using-mid-level-helpers](https://www.nsnam.org/docs/release/3.30/tutorial/html/tracing.html#using-mid-level-helpers)を見てください．

## まとめ

特定の変数を追跡したい場合はトレーシングシステムが便利です．変更内容を追跡したい変数で，わざわざstd::coutする必要がありません．mid-levelヘルパーと組み合わせることで，ファイルやpcapファイルへも出力可能です．ぜひ使いこなしてください．

[次の記事](https://qiita.com/dorapon2000/items/fadabf05a9c947bee87c)では，ネットワークシミュレーションの要であるスループットやパケット損失率をフローモニタを使って測定します．



[^1]: いつ実行されるかわからないAの実行後にB関数を実行するためには，AにBを登録しておき，A実行の最後でBを呼び出すことで実現できる．この仕組みをコールバックといい，Bをコールバック関数という．