ボトルネックを探すためのデバッグコードを楽にするためのクラス_C#編

# 修正履歴
- 2019-06-05 古いバージョンでも動くように調整した

# なにこれ
C＃既存コードのボトルネックをけっこうガチで探したので、それ用の汎用クラスを記憶の範囲で再現つつ改良しつつ遺しておきます
一通りの言語で作っておきたい感がなくもない

# 本文

## コンセプト
- 複数個所の所要時間を測定できる
- そこが何回呼ばれて総計何秒で1回あたり何秒かを測定できる
- 測定したい箇所がずんどこ増えても平気

## 使い方

### 概略
- 引数にIDを指定して、start/stopをする。
- IDを使い分ければ、複数の箇所で計測できる。
- start時にはそのIDに名前を指定できる。しなくてもいい。

```c#

var TimeKeeperManager tkm = new TimeKeeperManager(); // 初期化
tkm.start(1); // ID1の計測を開始
hogehoge1() //　何らかの処理
tkm.stop(1) // ID1の計測を停止
tkm.start(2); // ID2の計測を開始
hogehoge2() //　何らかの処理
tkm.stop(2) // ID2の計測を停止
Console.WriteLine(tkm.report()); // 個々の計測結果をまとめて出す
```


## コード本体
```c#:TimeKeeperSet.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace TimeKeeperSet {

    /// <summary>
    /// 時間計測用クラスをまとめて扱うクラス
    /// </summary>
    public class TimeKeeperManager {
        string title;
        private SortedDictionary<double, TimeKeeper> timeKeepers = new SortedDictionary<double, TimeKeeper>();

        /// <summary>
        /// 表題なしでインスタンスを生成する
        /// </summary>
        public TimeKeeperManager() : this(""){} 
        /// <summary>
        /// 表題とともにインスタンスを生成する
        /// </summary>
        /// <param name="title">インスタンスにつける表題</param>
        public TimeKeeperManager(String title) {
            this.title = title;
        }

        /// <summary>
        /// 計測開始する
        /// </summary>
        /// <param name="id">時計計測クラスの識別子</param>
        public void start(double id) {
            this.start (id, "");
        }

        /// <summary>
        /// 表題指定つきで計測開始する
        /// </summary>
        /// <param name="id">時計計測クラスの識別子</param>
        /// <param name="title">時計計測クラスの表題</param>
        public void start (double id, string title) {
            if (!timeKeepers.ContainsKey(id)) {
                timeKeepers.Add(id, new TimeKeeper(title));
            } else {
                if (!timeKeepers[id].title.Equals(title)) {
                    timeKeepers[id].isTitleChanged = true;
                }
            }
            timeKeepers[id].start();
        }

        /// <summary>
        /// idを指定して計測停止する
        /// </summary>
        /// <param name="id">時計計測クラスの識別子</param>
        public void stop(double id) {
            timeKeepers[id].stop();
        }

        /// <summary>
        /// 計測対象すべての結果を出力する
        /// </summary>
        /// <returns>計測結果を説明する文字列</returns>
        public string report() {
            StringBuilder sb = new StringBuilder();
            sb.Append("------------------------------------\n");
            sb.Append(string.Format("Timekeepers {0} report\n", title));
            foreach(KeyValuePair<double, TimeKeeper> kvp in timeKeepers) {
                sb.Append(kvp.Key + ": " + kvp.Value.report() + "\n");
            }
            return sb.ToString();
        }

        /// <summary>
        /// 時間計測用クラス
        /// </summary>
        class TimeKeeper {
            /// <summary>
            /// このインスタンスの表題
            /// </summary>
            public String title {get;set;}
            /// <summary>
            /// 表題が変えられた場合にtrueとなるフラグ
            /// </summary>
            public bool isTitleChanged {get;set;}

            /// <summary>
            /// startした回数
            /// </summary>
            private int countStart = 0;
            /// <summary>
            /// stopした回数
            /// </summary>
            private int countStop = 0;
            /// <summary>
            /// startしてからstopするまでの経過時間を保有する
            /// </summary>
            private Stopwatch sw = new Stopwatch();
            /// <summary>
            /// 表題とともにインスタンスを生成する
            /// </summary>
            /// <param name="title">インスタンスにつける表題</param>
            public TimeKeeper (String title) {
                this.title = title;
                this.isTitleChanged = false;
            }
            /// <summary>
            /// 表題なしでインスタンスを生成する
            /// </summary>
            public TimeKeeper () : this(""){}

            /// <summary>
            /// 経過時間計測を開始する
            /// </summary>
            public void start() {
                sw.Start();
                countStart++;
            }

            /// <summary>
            /// 経過時間計測を終了する
            /// </summary>
            public void stop() {
                sw.Stop();
                countStop++;
            }

            /// <summary>
            /// 計測結果レポートを出力する
            /// </summary>
            /// <returns>計測結果を説明する文字列</returns>
            public string report() {

                float elapsed = sw.ElapsedMilliseconds;
                System.Text.StringBuilder sb = new System.Text.StringBuilder();
                sb.Append(string.Format("start={0}", countStart));
                sb.Append(string.Format(", stop={0}", countStop));
                sb.Append(string.Format(", totalSec={0:F2}", elapsed/1000));
                sb.Append(string.Format(", aveSec={0:F2}", (elapsed/1000/countStart)));
                if (title != "") {
                    sb.Append(string.Format(", title={0}", title));
                }
                if (isTitleChanged) {
                    sb.Append(", WARNING: title has changed.");
                }
                if (countStart != countStop) {
                    sb.Append(", WARNING: count start/stop not match.");
                }
                return sb.ToString();
            }
        }
    }
}
```

使用サンプルコード

```c#:TimeKeeperSetSample.cs
using System;
using TimeKeeperSet;
using System.Threading.Tasks;

namespace HelloWorld
{
    class Hello 
    {
        static void Main() 
        {
            TimeKeeperManager tkm = new TimeKeeperManager("aaa");
            tkm.start(0, "総合計"); // タイトルがreportに表示される
            tkm.start(1); // タイトルがreportに表示されない
            phase01();
            tkm.stop(1);
            tkm.start(2);
            for (int i = 0; i < 3; i++) {
                tkm.start(2.1); // id=2の子項目的に追加した体
                phase03();
                tkm.stop(2.1); // id=2の子項目的に追加した体
                for (int j = 0; j < 2; j++) {
                    tkm.start(2.21); // id=2の子項目的に追加した体
                    phase04();
                    tkm.stop(2.21); // id=2の子項目的に追加した体
                }
                tkm.start(2.2);
                phase05();
                tkm.stop(2.2);
            }
            tkm.stop(2);
            tkm.start(6, "phase6");
            phase06();
            tkm.stop(6);
            tkm.start(7, "comment ver 1");
            phase07();
            tkm.stop(7);
            tkm.start(7, "comment ver 2"); // 同じIDを使ってしまったパターン（レポートに警告が出る）
            phase08();
            tkm.stop(7);
            tkm.start(10);
            phase09();
            tkm.stop(10);
            tkm.start(9); // IDの呼び出し順が昇順でないパターン（レポートはID順に出る）・かつ、stopしたままで終了するパターン（レポートに警告が出る）
            phase10();
            
            tkm.stop(0);
            Console.WriteLine(tkm.report());
        }

        static void wait() { System.Threading.Thread.Sleep(300); }
        static void phase01() => wait();
        static void phase02() => wait();
        static void phase03() => wait();
        static void phase04() => wait();
        static void phase05() => wait();
        static void phase06() => wait();
        static void phase07() => wait();
        static void phase08() => wait();
        static void phase09() => wait();
        static void phase10() => wait();
    }
}
```

結果

```
Timekeepers aaa report
0: start=1, stop=1, totalSec=5.41, aveSec=5.41, title=総合計
1: start=1, stop=1, totalSec=0.30, aveSec=0.30
2: start=1, stop=1, totalSec=3.61, aveSec=3.61
2.1: start=3, stop=3, totalSec=0.90, aveSec=0.30
2.2: start=3, stop=3, totalSec=0.90, aveSec=0.30
2.21: start=6, stop=6, totalSec=1.80, aveSec=0.30
6: start=1, stop=1, totalSec=0.30, aveSec=0.30, title=phase6
7: start=2, stop=2, totalSec=0.60, aveSec=0.30, title=comment ver 1, WARNING: title has changed.
9: start=1, stop=0, totalSec=0.31, aveSec=0.31, WARNING: count start/stop not match.
10: start=1, stop=1, totalSec=0.30, aveSec=0.30
```