グラフのリアルタイムの描画について(実現可能なの
でしょうか?)
ken
( 2009-04-21 12:23 )
お世話になっております。
VisualStidio2005、XPで開発をしています。
SHのボードとシリアル通信を行っていまして、そのデータを
PC側アプリでグラフを描画するようなシステム開発しております。
通信ボーレートは、38400bpsです。
1秒間に、30バイトのデータが100個:計3000バイト、常に送られてきます。
そのデータを折れ線グラフで描画しようとしているのですが、
描画が遅くなっていってしまいます。
そこで、いくつかのデータを飛ばして、データ数を少なくして表示してみたのですが、それでも遅れてしまい、何か良い方を考えているところです。
また、38400bpsで、1秒間に3000バイトのデータを受信して、
(*チェックサムなどいくつか処理を行っています。)
ピクチャーボックスを使用していますが、そのデータをリアルタイムに
グラフに描画することは、そもそも可能なのでしょうか?
*データ量が多すぎるなど仕様的に無理がありますでしょうか?
ご教授お願いいたします。
VisualStidio2005、XPで開発をしています。
SHのボードとシリアル通信を行っていまして、そのデータを
PC側アプリでグラフを描画するようなシステム開発しております。
通信ボーレートは、38400bpsです。
1秒間に、30バイトのデータが100個:計3000バイト、常に送られてきます。
そのデータを折れ線グラフで描画しようとしているのですが、
描画が遅くなっていってしまいます。
そこで、いくつかのデータを飛ばして、データ数を少なくして表示してみたのですが、それでも遅れてしまい、何か良い方を考えているところです。
また、38400bpsで、1秒間に3000バイトのデータを受信して、
(*チェックサムなどいくつか処理を行っています。)
ピクチャーボックスを使用していますが、そのデータをリアルタイムに
グラフに描画することは、そもそも可能なのでしょうか?
*データ量が多すぎるなど仕様的に無理がありますでしょうか?
ご教授お願いいたします。
コメント
もしかして、データ受信毎に、あるいは受信データ○件に1回毎に、グラフを描画しようとしているのですか?
それだったらロジックを見直したほうがよいと思います。
シンプルに考えてみて…
(1)受信したデータは、適当な配列かコレクションクラスに格納。(受信スレッドにて)
また、どんどん溜まってしまうので描画に必要な直近○件以外のデータは削除。
(※ここではグラフ描画しない)
(2)タイマーで定期的に(たとえば0.5秒毎)に、ピクチャーボックスのInvalidateメソッドを呼び出す。
(3)ピクチャーボックスのPaintイベントでは、直近○件のデータを使用して折れ線を描画。
(※格納データにアクセスする場合は、ちゃんと受信スレッドと同期をとること)
これで十分だと思いますが。
レスありがとうございます。
>もしかして、データ受信毎に、あるいは受信データ○件に1回毎に、グラフ
>を描画しようとしているのですか?それだったらロジックを見直したほうがよいと思います。
いえ、流れるように表示をしようと考えています。
ですので、500個の配列をもっていまして。
Array.Copyにてデータを後ろから詰めいる処理をしています。
(1)については、おっしゃる通りやっているように思えます。
>(2)タイマーで定期的に(たとえば0.5秒毎)に、ピクチャーボックスのInvalid>ateメソッドを呼び出す
こちらなのですが、Refreshでは、間違っていますでしょうか?
0.5にやっています。。
>(※格納データにアクセスする場合は、ちゃんと受信スレッドと同期をとること)
Paintのイベントにて、500をfor文でコピーしています。
同期というのは、どのようなことをするのでしょうか?
しばらく動作をさせておきますと、だんだん処理が遅くなっていって
しまいます。
お願いいたします。
私が懸念していたような描画の仕方ではなかったようですね。
いえRefreshとInvalidateの違いを理解されているなら、
どちらでも良いように思います。
マルチスレッドの場合、同じ変数(配列)に同時アクセス(操作)されないように同期を取るようにします。
たとえば、C#ならlockステートメント、VB.netならSyncLockステートメントが使えます。
もしかして、多くのデータを何度もコピーしているため
ガーベージコレクション(GC)が頻繁に発生しているのではないでしょうか?
プロファイラやGC.CollectionCount メソッドなどで、
どれくらい発生しているか見てみては?
GCが頻繁に発生しているのであれば、
データ格納領域を有効利用するよう(マネージヒープの使用量を増やさないよう)
工夫が必要になってくるでしょう。
たとえば
「リングバッファ」 って簡単に実装できるので、
これを使って直近500件分のデータだけ格納するとか。
あれ!?もしかして旧会議室のスレッドの続きってことですか?
タスクマネジャーの様に表示をしたい(NPlot使用)
NPlotというツールを使用しているのですね!?
よいのかどうか、なんとも言えませんが
フルGCはできるだけ発生しないようにはしたいですよね。
以下↓、ちょっと思いついたアイデアを紹介します。
(試していないですし、試すつもりもないので、参考程度にお願いします!!)
1.まず、独自リングバッファクラスを作成。
・この辺のソースを参考に(別に参考ソースなんか必要ないかもしれませんが)
http://www.koders.com/csharp/fid0EC368A4B8452EC8425CC5A0EDF9AFC6E175B1D1.aspx
・リングバッファクラスをジェネリック仕様に修正。
・バッファフルの場合はエラーにせず、一番古いデータを上書きするように修正。
・IListインターフェイスを実装する。(NPlotでDataSourceプロパティでセットするのに必要か!?)
2.受信スレッドにて、データをリングバッファに追加していく。
3.NPlotには、DataSourceにリングバッファを指定。
4.タイマーにて定期的にグラフをリフレッシュ。
という感じです。
私の勘では、これでガーベージコレクションの発生がかなり抑えられると思うのですが…
>1.まず、独自リングバッファクラスを作成。
> ・この辺のソースを参考に(別に参考ソースなんか必要ないかもしれませんが) http://www.koders.com/csharp/fid0EC368A4B8452EC8425CC5A0EDF9AFC6E175B1D1.aspx
・リングバッファクラスをジェネリック仕様に修正。
・バッファフルの場合はエラーにせず、一番古いデータを上書きするように修正。
すみません、C#を使用していますが、ジェネリック仕様とは、どうゆうことを
すればよろしいのでしょうか?
> ・IListインターフェイスを実装する。(NPlotでDataSourceプロパティでセッ>トするのに必要か!?)
IListについて調べ中です。
当初、ArrayCopyを使用しようと思ったのは、配列の後ろから格納して、グラフをタスクマネジャーのように流したように見せるためでした。
教えていただいたように、リングバッファにデータを格納して、
データソースにバインドした場合、リングバッファの一番最後に格納した
データが、表示の最後部に来ますでしょうか?
リングバッファ上のどこかに格納されているのかと思いますが・・
それを解決するための手段が、ILISTなのでしょうか?
お願いいたします。
ここのソース(http://www.koders.com/csharp/fid0EC368A4B8452EC8425CC5A0EDF9AFC6E175B1D1.aspx)
って、格納できるデータがbyte型なんです。
シリアル通信のデータが同じくbyte型で格納できるのでしたらそのまま使えますが
でなければ、格納できるデータ型を変更しなければなりません。
せっかくVisualStidio2005使っているのでしたら、ジェネリッククラスに直したいと思っただけです。
IListについてですが、NPlotのソースをちょっと見てみたのですが
○○PlotのDataSourceプロパティはObject型ですが、
そこからどうやってデータを取り出しているかというと、
IList/DataSet/DataTableなどのデータを取り出せる型にキャストしています。
逆にこれらの型に変換できなければ例外が発生するでしょう。
なので、DataSourceプロパティにセットできるようにするためには
IListインターフェイス(またはDataSet/DataTable)を実装する必要があります。
律儀に実装すると結構面倒なので、下記のプロパティ・メソッド以外はダミーで良いように思います。
Countプロパティ
インデクサ
グラフのどちらが前か後ろなのか知りませんが、
インデクサがリングバッファ上のどこの値を返すかだけの問題だと思います。
data[0]で一番新しいデータを返すようにするか?それとも一番古いデータを返すようにするか?
の実装を変更することは難しくないと思います。
レスありがとうございます。
リングバッファについては、
実装できたように思うのですが、
IListについては、いまいちよくわからないでいます。
>Countプロパティ
>インデクサ
とありますが、どうゆうことを行えばよろしいのでしょうか?
試行錯誤してサイトなどを調べながらやってみました、
IListの実装に当たるのかわかりませんが
下記のようにコーディングしたのですが、見当はずれでしょうか?
どのようにコーディングすると、リングバッファを、DataSourceプロパティにセット
することができるのでしょうか?
ご教授お願いします。
/////////////
double[] Data = new double[12000];
RingBuffer test = new RingBuffer();
/////////////
/* nPlotのDataSourceにセットする処理。
private void DataDraw()
{
//前回
//LinePlot._yDataSource = Data;
//ILIST 今回
LinePlot_y.DataSource = GetDataSource();
}
private IList<D> GetDataSource()
{
///ここにリングバッファ処理???///
//////////////////////////////
// List<D> list = new List<D>();
// list.Add(new D("hoge", 20));
// list.Add(new D("hehe", 30));
return list;
}
Countプロパティ、インデクサ 以外にも
IEnumerable.GetEnumerator メソッドの実装が必要のようです。
試すつもりはなかったのですが、サンプルコードを書いてみましたので参考にしてみてください。
※投稿文字数に制限があるようですので、分割投稿します。
Form1.cs
public partial class Form1 : Form
{
Thread _thread; // 受信スレッド(のつもり)
RingBuffer<int> _data = new RingBuffer<int>(500); // リングバッファデータ
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
if (_thread != null)
{
_thread.Abort();
}
// 受信スレッド開始
_thread = new Thread(new ThreadStart(ThreadProc));
_thread.Start();
// グラフ描画更新タイマー開始
timer1.Interval = 500;
timer1.Start();
}
public void ThreadProc()
{
// 受信データの代わりに、sin値を格納してみる
for (double d = 0; ; d += 0.01)
{
lock (_data)
{
_data.Write((int)(Math.Sin(d) * 100));
}
Thread.Sleep(10);
}
}
private void Form1_Load(object sender, EventArgs e)
{
// グラフの設定
NPlot.LinePlot LinePlot1 = new NPlot.LinePlot();
LinePlot1.DataSource = _data; // リングバッファデータをDataSouceとする
LinePlot1.Pen.Color = Color.Blue;
plotSurface2D1.Add(LinePlot1);
plotSurface2D1.YAxis1.WorldMin = -150;
plotSurface2D1.YAxis1.WorldMax = 150;
plotSurface2D1.XAxis1.WorldMin = 0;
plotSurface2D1.XAxis1.WorldMax = 600;
}
private void Form1_FormClosed(object sender, FormClosedEventArgs e)
{
if (_thread != null)
{
_thread.Abort();
}
}
private void timer1_Tick(object sender, EventArgs e)
{
// グラフを再描画
lock (_data)
{
plotSurface2D1.Refresh();
}
}
}
RingBuffer.cs(その1)
// リングバッファクラス
class RingBuffer<T> : System.Collections.IList
{
// リングバッファ用Enumeratorクラス
class RingBufferEnumerator : System.Collections.IEnumerator
{
int _currentPos = -1;
RingBuffer<T> target;
public RingBufferEnumerator(RingBuffer<T> target)
{
this.target = target;
}
#region IEnumerator メンバ
public object Current
{
get
{
return target[_currentPos];
}
}
public bool MoveNext()
{
_currentPos++;
return _currentPos < target.Count;
}
public void Reset()
{
_currentPos = 0;
}
#endregion
}
T[] _buffer;
int _size;
int _count;
int _readPos = 0;
int _writePos = 0;
RingBufferEnumerator _enumerator;
public RingBuffer(int size)
{
_size = size;
_count = 0;
_enumerator = new RingBufferEnumerator(this);
_buffer = new T[_size];
}
public int Count
{
get
{
return _count;
}
}
public void Clear()
{
_count = 0;
_writePos = 0;
_readPos = 0;
}
public T this[int index]
{
get
{
return _buffer[(_readPos + index) % _size];
}
set
{
_buffer[(_writePos + index) % _size] = value;
}
}
public void Write(T data)
{
_count++;
_buffer[_writePos] = data;
_writePos = (_writePos + 1) % _size;
if (_count > _size)
{
_count = _size;
_readPos = (_readPos + 1) % _size;
}
}
public T Read()
{
if (_count == 0)
{
throw new Exception("Buffer empty");
}
_count--;
int pos = _readPos;
_readPos = (_readPos + 1) % _size;
return _buffer[pos];
}
RingBuffer.cs(その2)
#region IList メンバ
public int Add(object value)
{
throw new NotImplementedException();
}
public bool Contains(object value)
{
throw new NotImplementedException();
}
public int IndexOf(object value)
{
throw new NotImplementedException();
}
public void Insert(int index, object value)
{
throw new NotImplementedException();
}
public bool IsFixedSize
{
get { throw new NotImplementedException(); }
}
public bool IsReadOnly
{
get { throw new NotImplementedException(); }
}
public void Remove(object value)
{
throw new NotImplementedException();
}
public void RemoveAt(int index)
{
throw new NotImplementedException();
}
object System.Collections.IList.this[int index]
{
get
{
return _buffer[(_readPos + index) % _size];
}
set
{
_buffer[(_writePos + index) % _size] = (T)value;
}
}
#endregion
#region ICollection メンバ
public void CopyTo(Array array, int index)
{
throw new NotImplementedException();
}
public bool IsSynchronized
{
get { throw new NotImplementedException(); }
}
public object SyncRoot
{
get { throw new NotImplementedException(); }
}
#endregion
#region IEnumerable メンバ
public System.Collections.IEnumerator GetEnumerator()
{
return _enumerator;
}
#endregion
}
レスありがとうございます。
大変助かりました。
ありがとうございます。
当初よりだいぶ改善されました。
タイマで、GC.CollectionCountを確認しながら
1時間ぐらい動作をさせていたところなのですが
カウント値が13000位になりました。
やはり、動かし始めのときより描画が遅くなってしまいます。
2,3,4時間と動作させた場合、GC発生回数が増えていくことが予想されると思いますが、そうなった場合、さらに描画の遅れが発生するように思うのですが。
ここの部分につきましては、何か良い方法は、ありませんでしょうか?
測定のため、長時間動かしたいと思っています。
以上です。
昨日サンプルプログラムを作ってプロファイルしてみていたのですが、
NPlotがプロットのために、非ジェネリック版のIListインターフェイス(のインデクサ)経由でデータを取り出しているので
このとき、どうしてもボックス化(⇒マネージヒープの消費)が発生してしまいますね。
.netの性質上、ある程度は仕方ないように思います。
フルGCの発生具合はどうでしょう? → GC.CollectionCount(GC.MaxGeneration)
GC.CollectionCountの返す値は、プログラム開始時からの累積なので
問題は増え方ですね。
一定時間内の発生回数が一定であれば、長時間動作でも問題ないでしょうが、
時間経過とともに頻発してくるようでしたら、
どこかでメモリリークしていないか調査する必要がありますね。
-----
昨日のサンプルプログラム(投稿#13)の訂正
public void Reset()
{
_currentPos = 0 -1;
}
レスありがとうございます。
GCを調べてみました。
1時間~2時間くらいの動作での値になります。
GC.CollectionCount(0): 13000
フルGC:300
というような感じになっております。。
>.netの性質上、ある程度は仕方ないように思います。
>フルGCの発生具合はどうでしょう? → GC.CollectionCount(GC.MaxG>eneration)
GC.CollectionCount(0)、GCフルとも
一定時間の増加しています。
ペースが変わるようなことは、動作中は見当たりませんでした
>問題は増え方ですね。
>一定時間内の発生回数が一定であれば、長時間動作でも問題ないでしょ>うが、時間経過とともに頻発してくるようでしたら、どこかでメモリリークして>いないか調査する必要がありますね
お願いいたします。
ということは、1分間に2~5回程度はフルGCが発生しているということですか。
結構多い様な気がしますね。
今朝、私も自宅PCで提示したサンプルプログラム動かしっぱなしにしてきました。
帰宅したら、うまくいけばGCの統計が取れていると思いますので確認してみます。
ペースが変わっていないのであれば良いのですが、
描画が遅くなってくるというのが、気になりますね。
試しに、
ワークステーションGCからサーバーGCに変えてみては
どうなるでしょう?
GCSettings.IsServerGC プロパティ
以前検証したときには、
サーバーGCのときのほうが、1回のGCでのゴミ回収率が良くなるようで
GC発生がかなり減りました。
リンク先にも記述されているのですが、
GCSettings.IsServerGC プロパティ
.configファイルを作成(すでにあれば追記)して、実行ファイルと同じ場所に配置してください。
実行ファイル名.exe.config
<configuration>
<runtime>
<gcServer enabled="true" />
</runtime>
</configuration>
レスありがとうございます。
すみません、理解できました。
ただ、教え頂いた通りやってみたのですが
値がfalseのままでした。
サイトを読んでみましたところ
『サーバーのガベージ コレクションは、マルチプロセッサ コンピュータ上でのみ使用できます。』
と書いてありますが、CPUの問題なのでしょうか?
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<runtime>
<gcServer enabled="true" />
</runtime>
</configuration>
・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・・
よろしくお願いいたします。
レスありがとうございます。
システムの1つの機能として、送られてきたデータをログとして
ファイルに落としています。
こちらをコメントにしたら
さらにガーベージコレクションの値が落ち着いたように思います。
遅れも、だいぶ改善されたようにも思います。
GC.CollectionCount(0): 10000
フルGC:20
何個かの受信したデータをStaringで貯めて
タイマー処理のときにファイルに書き込みし、貯めたデータを
初期化していますが、こちらはGCを行っているのでしょうか?
GCに影響しないように、ログとして残したいのですが
方法は何でもいいのですが、GCが増えないような方法でファイルに
データを記録する、よい方法は、ありますでしょうか??
/////////////////////////////////////////////////////////
private void serialPort_DataReceived(object sender, System.IO.Ports.SerialDataReceivedEventArgs e)
{
String ReciveDataStr;
String[] ReciveData = new String[5];
String[] SplitData ;
//String
int i = 0;
if (serialPort.BytesToRead > 29)
{
Work += serialPort.ReadExisting();
ReciveDataStr = Work .Substring(0, 6000);
Work = Work .Substring(6000);
SplitData = ReciveDataStr.Split('\n');
for (i = 0; i < (pp.Length - 1); i++)
{
if (SplitData [i] != "")
{
ReciveData = SplitData [i].Split(',');
ReciveData[4] = ReciveData[4].Substring(0, 2);
if (func_check_sum(ReciveData, ReciveData[4]) == false)
{
return;
}
SecondCount++;
if (SecondCount == 17)
{
_data_x.Write(DataCalc(ReciveData[0]) + offsetx);
_data_y.Write(DataCalc(ReciveData[1]) + offsetx);
_data_z.Write(DataCalc(ReciveData[2]) + offsetx);
SecondCount = 0;
}
}
↓ここです。
/* FileData += ReciveData[0] + "," + _
ReciveData[1] + "," + _
ReciveData[2] + "," + _
ReciveData[3] + "," + _
ReciveData[4] + "," + _
String.Format("{0}", t.ToString("MMddyyyy hh:mm:ss.fff tt")) + "\r"; */
}
}
/////////////////////////////////////////////////////////
private void timer_Tick(object sender, EventArgs e)
{
/* if (logenable == TRUE)
{
writer.WriteLine(FileData);
FileData = "";
}*/
this.plotSurface2D_X.Refresh();
this.plotSurface2D_Y.Refresh();
this.plotSurface2D_Z.Refresh();
}
以上ですよろしくお願いいたします。
一応結果報告しておきます。
【動作環境】
CPU:Intel Core2
OS:Windows Vista SP1
コンパイラ:VisualStudio2008
GC:ワークステーションGC
プログラム:投稿#12~14
【結果】
時間後 gen-0 gen-1 gen-2
1 567 50 5
2 1134 101 11
3 1701 150 16
4 2268 201 22
5 2835 250 27
6 3402 301 33
7 3969 350 38
8 4536 401 44
9 5103 451 50
10 5670 501 55
11 6237 551 61
12 6804 601 66
【結果分析】
フルGC(gen-2)は、10分に1回程度でした。
実際は10分ごとに計測したのですが、グラフにしてみると
gen-0~gen-2のいずれも見事な直線となりました。
【考察】
この結果をみると、
kenさんのプログラムのGC発生がやけに多く感じられます。
やはり、グラフ描画以外のことろ(データ受信?)で
メモリを浪費しているように思います。