ちょっとしたスケーラビリティちっくな話 ～ C#でオシロのcsvログファイルをグラフ描画するツールをつくったときに考えたことの整理


# 経緯

数100nsの世界のオシロの波形を数秒以上取ると、すぐにMB単位の巨大なログデータになってしまい、csvファイルから毎回読み込んで全部の点を線分として描画するのは、メモリリソースや処理負荷的によろしくない。とか色々考えたことをメモとして残しておく。

# やったことなど

## csvファイルの読み込みとログデータの変換

普段だと、テキトーなツールを使うときは

```cs

Regex r = new Regex(@"^ほげほげ・・・$");

string[] lines = File.ReadAllLines(fileName);
foreach ( string s in lines ) {
    string[] t = s.Split(new char[]{',');
    for ( int i=0;i<t.Length;i++ ) {
        Match m = r.Match(t[i]);
        ：
        ：
    }
    ：
    ：
}
```

みたいなコードを息を吐くように書いてますが、ファイルがでかいと数秒以上待たされる。ASCIIデータであっても地味に内部で文字コード変換が行われるので、メモリもファイルサイズの2倍くらいは使っていそう。
そこで、取りうる手段を考える。

- 描画するときに必要な部分をcsvファイルから都度読み出す。<br>⇒ 1行あたりのデータが固定長でない（値に依存して文字列の長さが変わる）ようなcsvファイルだったので、所望の要素番目のデータへのシーク（ランダムアクセス）が困難（ファイルの頭から毎回読むか、目次データ的なテーブルを持つ羽目になる）。廃案。
- 一旦読み込んで、<b>1要素 = float型1個分のデータなどの変換を施して、固定長レコードの別ファイルに変換しておく。</b>・・・これを採用した。(`BinaryWriter`クラスでごにょごにょ)

## オシロ風のグラフ描画処理

10万とか100万要素もあると、DrawLineとかで処理すると処理負荷が無駄にかかる。
かといって、<b>データを間引きして描画すると、オシロ波形上のヒゲなどを取りこぼしてしまい、見落としにつながる。</b>

ということで、画面横幅のピクセル数以上の要素数を持つときは、全要素を線分でつなぐような描画はせず、処理を切り替える。
具体的には、画面（というか描画先のBitmap）の1つのx座標(1ピクセル)に属す要素たちを1つの単位として扱い、その要素たち（区間）の最小値と最大値を求め、それらを`Point`の配列として`FillPolygon`で塗りつぶすようにした。（単位といっても、とくにclassとかは使わずに単純にfor文の開始と終了のインデックスをうまいこと計算して処理した。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/2b28cc74-2406-77fd-8e65-dc5945d0e121.png)

`FillPolygon`と`Point`配列のindexは工夫が要る。MAXの系列の左端を`Point`配列のindex = 0, 1, 2, ... , N-1の要素として扱い、MINの系列の左端をindex = 2N-1, 2N-2, ... , Nとして扱った。`FillPolygon`だけだと塗りつぶし領域に太さがないと描画されない現象が出たので、`DrawLines`を併用して輪郭も描画して対策した。

### ソースコード

```csharp

void DrawSeries(Graphics g, int width, float[] series, long drawStartI, long drawEndI, float lowerWhole, float upperWhole, int offsetY, int height, Brush brush, Pen pen)
{
    long n = drawEndI-drawStartI+1;

    if ( n >= width ) {
        var p = new Point[2*width];

        for ( int x=0;x<width;x++ ) {
            long startI = drawStartI +  x   *n/width;
            long endI   = drawStartI + (x+1)*n/width;
            
            float lower = series[startI];
            float upper = series[startI];
            
            for ( long i=startI+1 ; i<endI ; i++ ) {
                if ( series[i] < lower ) { lower = series[i]; }
                if ( series[i] > upper ) { upper = series[i]; }
            }
            
            float yLower = (lower-lowerWhole)*height/(upperWhole-lowerWhole);
            float yUpper = (upper-lowerWhole)*height/(upperWhole-lowerWhole);
            
            p[x]           = new Point(x,(int)(offsetY+height-yLower));
            p[2*width-1-x] = new Point(x,(int)(offsetY+height-yUpper));
        }
        g.FillPolygon(brush, p);
        g.DrawLines(pen, p);
    }
    else if(n>=2){
        var p = new Point[n];

        for ( long i=drawStartI;i<=drawEndI;i++ ) {
            float x = (i-drawStartI)*width/(n-1);
            float y = (series[i]-lowerWhole)*height/(upperWhole-lowerWhole);
            
            p[i-drawStartI] = new Point((int)x, (int)(offsetY+height-y));
        }
        g.DrawLines(pen, p);
    }
}

```

## 久々のオーバーフロー

C#の`int`型は32bitで、その正の数の最大値は 2,147,483,647 なので、46000ちょいの値同士を掛け算するなどで簡単にオーバーフローしてしまいます。巨大な配列の添え字をごにょごにょするときは、とりあえず`long`型を使いまくる。
C#では`checked{}`でくくると例外吐いてくれるので、開発途中は付けておいたほうが無難。（それだけを担保にしてはいけないが。）
ちなみに、`unsafe{}`外でも、配列範囲外アクセスしたときに例外で落ちないケースがある気がするが、そういうものなのか、気のせいなのか・・・？

# まとめ（？）

- 気にすることは使用メモリ量と処理時間・・・だけど利便性も残す。
- 規模が大きくなると処理方式（アルゴリズム）やデータ構造を考え直す必要がでてくる。初めからどの程度の規模のデータを扱うかは考慮しておいたほうがよい。オーダーの概念やキャッシュの概念などは知っておいたほうがよい。
- とはいえ、実感がわかないし予測もムズカシイので、簡単なアルゴリズムベースでプロトタイピングをして、ユーザビリティを感覚的に試すのもアリ。（簡単に試せる環境があれば。）
- オーバーフローやら配列の要素数の限界やら、数の許容範囲を気にする必要がでてくる。