プロセスツリーを使ってマルチプロセスなプログラムのメモリ使用量を測る

ウェブブラウザのように複数のプロセスで構成されている linux アプリのメモリ使用量を出来るだけ正確に知りたい。こういうツールは既に存在するはずだと思って色々探したけど無かったので作ってみた。 https://github.com/propella/multimem

実行例

```
$ ./multimem $(pidof weston)
(PID) Name                                   SWAP      USS      PSS      RSS
----------------------------------------------------------------------------
(1000) weston                                   0    22504    36281    55708
  (1293) ssh-agent                              ?        ?        ?        ?
  (1462) weston-desktop-                        0     2188     3787     9832
    (2849) weston-terminal                      0     5312     7513    14924
      (2850) bash                               0     2108     2370     5864
        (22555) weston-smoke                    0     2416     3038     7384
        (22520) weston-simple-e                 0    14372    26144    41660
        (22507) weston-flower                   0     1524     2161     6524
  (1461) weston-keyboard                        0     2780     3884     9336
----------------------------------------------------------------------------
Total                                          ?0   ?53204   ?85178  ?151232
```

途中メモリマップを読めないプロセスがあると、? として表示している。この場合合計値も不正確なので ? と表示した。

## 要件

* 共有メモリをダブって計測しないように Proportional set size (PSS) を使う。
    * https://selenic.com/repo/smem からコードを拝借する。
* サブプロセスのメモリを全部足した値を表示する。
* 引数として PID をとる。
    * またはコマンド文字列を受け取り実行して、その PID を対象とする。
* ある間隔で値を表示し続ける。

## smem の構造

`smem` コマンドはカーネルが出力するメモリの統計情報を見やすく表示する Python スクリプトだ。PSS を取得するために `smem` を利用したいので、コードの中身を多少読んでおく。なんか。。。非常に汚くてビックリ。。。カーネルハッカーでも Python は下手くそなのだと微笑ましく思った。

中を見ると、ようするに、`/proc/(pid)/smaps` というファイルから各メモリ領域の使い方を読めるのだが、その中の Pss という項目を足せばそれがプロセスの PSS になるというだけだった。PSS というのは他のプロセスと共有しているメモリを共有しているプロセスの数で割った物だが、私はてっきりカーネルは計算に必要な情報だけ出して Python スクリプトで割り算をやってると思いこんでいた。案外簡単だった。

* `procdata`
    * このオブジェクトを通して `/proc` ファイルシステムにアクセスする。
    * 普通の `/proc` の他、`--source` オプションで保存しておいた `/proc` を読むことも出来る。
* `showpids`
    * 何もオプションを指定しないとこの関数を呼ぶ。
* `filters`
    * PID またはプロセス名が正規表現に引っかかれば False。普通逆やろ。
* `processtotals`
    * このコマンドの出力データを返す
    * `totals[pid][key] = サイズ` の形になる
* `pidtotals`
    * 一つのプロセスの統計データを返す
    * pidmaps によりメモリマップを取得
    * メモリマップの中の統計データを全部足しあわせてそのプロセスの統計データとして返す。
* `pidmaps`
    * 一つのプロセスのメモリマップを表す
    * `maps[start][key] = サイズ` の形になる。
    * 行の末尾が `kB` で無ければメモリ領域とみなして start, end を更新 (16進文字列を数字に変換)。
        * メモリ領域の最後のフィールドが 5 文字以上あれば名前を保存。
    * 行の末尾が `kB` ならサイズとみなして辞書に入れる。
        * キーを全て小文字にする。
* `mapdata`
    * `/proc/(pid)/smaps` を読んで文字列を返す。

## 子プロセスの PID を再帰的に求める。

子プロセスの PID を直接取得する方法は無いみたいだけど、`/proc/(pid)/status` を読むと `PPid:` という項目に親プロセスが現れる。なので最初に 子PID -> 親PID の辞書を作って、そこから 親PID -> [子PID, ...] の辞書を作ればあとは再帰的に全ての子プロセスを求める事が出来る。

## 子プロセスのメモリサイズも含めて計測する。

まず最初に `multimem 1234` のように、PID を引数に取り自分と子プロセスのメモリ使用量を再帰的に足して出力するコマンドを作った。

```
# ./multimem 23234
  PID Command                         Swap      USS      PSS      RSS 
23234 /usr/bin/qt5/qmlscene /usr/        0   253384   290481   353312 
```

受け取った PID と子プロセスの PID を全て入れたリストを元に、smem の `pidtotals` 関数を呼び出し、項目を全て足せば良い。これで目的は達成したのだが、なんだか物足りない。

まず、折角サブプロセスも対象になっているのに、表示だけでは本当にちゃんと計算出来ているのか心許ない。出来れば ps の表示と比較してちゃんと正しい値が取れているか検証した。それには、サブプロセスそれぞれのメモリサイズも表示する事にして、上の実行例のようになった。

## PSS だけを一定時間おきに表示する。

こういうツールは実際には他のツールと組み合わせて使うことが多い。例えば、一体間隔で実行し、最大利用量を求めたりグラフを作ったりする。この場合あまり凝った表示は却って都合が悪いので `--brief` というオプションを付けた。このオプションを付けると PSS だけを表示する。これで sh と組み合わせて経過を記録出来る。

```
$ while ./multimem $(pidof -s emacs) --brief; do sleep 2; done
51882
51881
51882
...
```

参考

* pstree: プロセスツリーを表示するツール
* htop: top のかっこいい版。process tree を見る事が出来る。
    * https://askubuntu.com/questions/9642/how-can-i-monitor-the-memory-usage
* ELC: How much memory are applications really using?: https://lwn.net/Articles/230975/
* smem: https://selenic.com/repo/smem

暇をみて https://github.com/anishathalye/seashells (popular python project をググって見つけた) 等を参考にして pip パッケージにしたい。