Page 1 of 3

ColorGrid unacceptable performance

Posted: Wed Jan 11, 2006 6:11 pm
by 9639223
Hi,

I'm using ColorGrid to output a large amount of data (about 1000x100 values) and it repaints very very very slowly. Profiler shown that the method which takes most time in painting process is ColorGrid.FillBitmap(). This method uses Bitmap.SetPixel(), which is very slow and subsequent calls to it may be replaced with bitmap.LockBits and direct writing to the bitmap:

Code: Select all

	int w = bitmap.Width;
	int h = bitmap.Height;
	BitmapData bmData = bitmap.LockBits(new Rectangle(0, 0, w, h), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);

	unsafe
	{
		int *ptr = (int*)bmData.Scan0.ToPointer();

		for(int y = 0; y < h; ++y)
		{
			for(int x = 0; x < w; ++x)
			{
				*ptr++ = (int)colors[x,y].ToArgb();
			}
		}
	}

	bitmap.UnlockBits(bmData);
Also, I would like to have an option (say, FastDraw property) to use simple and fast drawing without values-to-colors mapping, without grid borders and with regular grid. I would like to be able to specify color values in RGB as Y values in ColorGrid. In this case RGB value could be retrieved as simple and fast as one indexing, one addition and one multiplication (times faster than in the current implementation).

Could you, please, consider adding these changes to ColorGrid?

Posted: Thu Jan 12, 2006 11:28 am
by narcis
Hi rvs,

This is not possible because TeeChart for .NET is 100% C# managed code.

We could use something like the code below but we thing it doesn't improve the performance substantially. However, we will do some testing to see what can be done.

Code: Select all

lockRect = new Rectangle(new Point(0,0), fBuffer.Size);
fBufferData = fBuffer.LockBits(lockRect,System.Drawing.Imaging.ImageLockMode.ReadWrite,System.Drawing.Imaging.PixelFormat.Format32bppPArgb);

System.IntPtr b2line0 = resultData.Scan0;
System.IntPtr b1line0 = fBufferData.Scan0;

int Dif=fBufferData.Stride; 

for(y=0; y<h; ++y) 
{
tmpy = Utils.Round(Utils.Sqr(y-yy));

for(x=0; x<w; ++x) 
{
dist = Math.Sqrt(Utils.Sqr(x-xx)+tmpy);
offset=(y*Dif)+(BytesPerPixel*x);

Marshal.WriteByte(b2line0,offset+2,Marshal.ReadByte(b1line0, offset+2));
Marshal.WriteByte(b2line0,offset+1,Marshal.ReadByte(b1line0, offset+1));
Marshal.WriteByte(b2line0,offset,Marshal.ReadByte(b1line0, offset));
Marshal.WriteByte(b2line0,offset+3,Marshal.ReadByte(b1line0, offset+3));
}
}
fBuffer.UnlockBits(fBufferData);
result.UnlockBits(resultData);
BTW: Would you mind testing at your end the 3 possible methods (Existing one, your suggestion and the code above) to give us some additional information about the existing possibilities?

Thanks in advance.

Posted: Thu Jan 12, 2006 6:06 pm
by 9639223
Hi, NarcĂ­s

Thank you for quick answer.

I've made a simple test of different ways to fill Bitmap:

Code: Select all

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

public class ConsoleApp
{
	private static void SetPixelFill(Bitmap bitmap)
	{
		int width = bitmap.Width;
		int height = bitmap.Height;
		Color color = Color.Purple;

		for(int j = 0; j < height; ++j)
			for(int i = 0; i < width; ++i)
				bitmap.SetPixel(i, j, color);
	}

	private static void MarshalFill(Bitmap bitmap)
	{
		int width = bitmap.Width;
		int height = bitmap.Height;
		Color color = Color.Purple;

		BitmapData bmData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);

		IntPtr ptr = bmData.Scan0;

		for(int j = 0; j < height; ++j)
			for(int i = 0; i < width; ++i)
				Marshal.WriteInt32(ptr, (i + j * width) * 4, color.ToArgb());

		bitmap.UnlockBits(bmData);
	}

	private static void DirectFill(Bitmap bitmap)
	{
		int width = bitmap.Width;
		int height = bitmap.Height;
		Color color = Color.Purple;

		BitmapData bmData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);

		unsafe
		{
			int *ptr = (int*)bmData.Scan0.ToPointer();

			for(int j = 0; j < height; ++j)
				for(int i = 0; i < width; ++i)
					*(ptr + i + j * width) = color.ToArgb();
		}

		bitmap.UnlockBits(bmData);
	}

	private static void DirectFillOptimized(Bitmap bitmap)
	{
		int width = bitmap.Width;
		int height = bitmap.Height;
		Color color = Color.Purple;

		int argb = color.ToArgb();

		BitmapData bmData = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);

		unsafe
		{
			int *ptr = (int*)bmData.Scan0.ToPointer();

			for(int j = 0; j < height; ++j)
				for(int i = 0; i < width; ++i)
					*ptr++ = argb;
		}

		bitmap.UnlockBits(bmData);
	}



	private delegate void TestInvoker(Bitmap bitmap);

	private static void RunTest(string testName, TestInvoker test, Bitmap originalBitmap)
	{
		Bitmap bitmap = (Bitmap)originalBitmap.Clone();

		System.Console.Write("\nRunning \"{0}\" test...   ", testName);

		DateTime start = DateTime.Now;

		test.Invoke(bitmap);

		System.Console.WriteLine("{0} ms", (DateTime.Now - start).TotalMilliseconds);
		bitmap.Save(testName + ".png");
	}

	public static void Main(string[] args)
	{
		Bitmap originalBitmap = new Bitmap(1600, 1200, PixelFormat.Format32bppArgb);

		RunTest("SetPixel fill", new TestInvoker(SetPixelFill), originalBitmap);
		RunTest("marshal fill", new TestInvoker(MarshalFill), originalBitmap);
		RunTest("direct fill", new TestInvoker(DirectFill), originalBitmap);
		RunTest("direct fill optimized", new TestInvoker(DirectFillOptimized), originalBitmap);
	}
}
You can compile it with 'csc /unsafe /o+ test.cs'.

This test outputs the following results on my pc:

Code: Select all

Running "SetPixel fill" test...   4453.125 ms
Running "marshal fill" test...   93.75 ms
Running "direct fill" test...   78.125 ms
Running "direct fill optimized" test...   31.25 ms
You see, that the method you've suggested (with Marshal.WriteInt32) makes bitmap filling about 50 times faster. But execution time can be made 3 more times shorter by using unsafe code properly.

I don't know your reasons of being afraid to add unsafe code to TeeChart. Probably, you want to keep your library at the highest level of compatibility and safety - that's fine. As for me, I don't need TeeChart.dll to be CLSCompliant or to be 100% verified code. I would be glad if you made a separate TeeChart.dll version with maximum optimizations, which probably will involve unsafe code. And ColorGrid is not the only place which you could optimize this way. I think this could be useful to many of your customers.

And what about FastDraw option, which should disable value-to-color mapping and IrregularGrid? As I wrote earlier, this option can be implemented very fast in case of regular grids. You could bypass indexing by x- and z- axis and use Series.YValues.Value to retrieve values and use this values as RGB colors. This should increase speed seriously and wouldn't require any unsafe code.

Posted: Fri Jan 13, 2006 11:23 am
by Chris
Hello,

Many thanks for your input on this issue.

Within the ColorGrid.FillBitmap() method I have now replaced the Bitmap.SetPixel method for the Interop marshalling technique. This means that the following code (Release mode under .NET v2.0):

Code: Select all

   private DateTime start; 

    private void Form1_Load(object sender, EventArgs e)
    {
      tChart1.Aspect.View3D = false;
      start = DateTime.Now;
      Steema.TeeChart.Styles.ColorGrid grid = new Steema.TeeChart.Styles.ColorGrid(tChart1.Chart);
      grid.FillSampleValues(100); 
    }

    private void tChart1_AfterDraw(object sender, Steema.TeeChart.Drawing.Graphics3D g)
    {
      label1.Text = (DateTime.Now - start).TotalMilliseconds.ToString();
    }
Now runs 1.05 times faster (781.1232 ms vs. 821.1808). This code change will be available in the debug build due for release within the next couple of working days.
I don't know your reasons of being afraid to add unsafe code to TeeChart. Probably, you want to keep your library at the highest level of compatibility and safety - that's fine. As for me, I don't need TeeChart.dll to be CLSCompliant or to be 100% verified code. I would be glad if you made a separate TeeChart.dll version with maximum optimizations, which probably will involve unsafe code. And ColorGrid is not the only place which you could optimize this way. I think this could be useful to many of your customers.
I agree. We have had plans for a while to create a GDI (unmanaged) canvas and maybe we could also think of creating an umanaged teechart.dll with a number of unsafe optimizations; I will mention this idea to the development team during our next meeting.
nd what about FastDraw option, which should disable value-to-color mapping and IrregularGrid? As I wrote earlier, this option can be implemented very fast in case of regular grids. You could bypass indexing by x- and z- axis and use Series.YValues.Value to retrieve values and use this values as RGB colors. This should increase speed seriously and wouldn't require any unsafe code.
That is also a good suggestion, as the value-to-color mapping is another bottleneck for regular grid painting. I have added this idea to our wish list and will look into it myself when I have the time.

Posted: Wed Jan 18, 2006 5:45 pm
by 9639223
Hi, Christopher

Thanks for dealing with this issue so eagerly. When and where can I get the version with optimized FillBitmap?

Regarding performance change you've mentioned (Now runs 1.05 times faster (781.1232 ms vs. 821.1808)). I think that the difference should increase if you use more values (grid.FillSampleValues(499);) and turn off grid borders (grid.Pen.Visible = false;). And it's more appropriate to move start = DateTime.Now; to the tChart1_BeforeDraw method.

Posted: Thu Jan 19, 2006 3:37 pm
by narcis
Hi rvs,
Thanks for dealing with this issue so eagerly. When and where can I get the version with optimized FillBitmap?
This version is available since last monday on our customer download area as you can see announced here.

Posted: Wed Jan 25, 2006 8:40 am
by 9639223
Hi, Christopher

I've downloaded the new debug version and it works fine. The ColorGrid filled with FillSampleValues(499) is drawn in 500ms instead of 1750ms as it was in the previous version.

Thanks a lot for this improvement. Our app now seems to work rather smooth when drawing ColorGrid. And I think that an option to disable value -> color mapping could make it even faster. Could you, please, notify me when you are done with this functionality?

Best regards,
Alexander

Posted: Wed Jan 25, 2006 9:47 am
by 9639223
Hi

I've just analyzed the performance of my app which uses ColorGrid and found that it allocates and frees a large memory block each time the series is repainted. Further analysys has shown that the DrawCellUsingBitmap method which is called on each repaint, creates one bitmap, fills it, paints, clones it and disposes. Though garbage collector manages these allocations quite well, I think that this behavior is very strange and not optimal. Why not store the bitmap and re-allocate it only if it's size should change? This would spare two bitmap allocations on each ColorGrid repaint in the case the bounds are unchanged (and this is a frequent case).

You could use, for instance, the following code to achieve this:

Code: Select all

private void DrawCellUsingBitmap(ref Rectangle tmpBounds, ref Rectangle R)
{
	try
	{
		int newWidth = Math.Abs((int) (((1 + tmpBounds.Right) - tmpBounds.Left) - this.tmpDec));
		int newHeight = Math.Abs((int) (((1 + tmpBounds.Bottom) - tmpBounds.Top) - this.tmpDec));
		if(newWidth == 0 || newHeight == 0)
			throw new Exception();

		if(bbitmap == null || bbitmap.Width != newWidth || bbitmap.Height != newHeight)
		{
			if(bbitmap != null)
				bbitmap.Dispose();

			bbitmap = new Bitmap(newWidth, newHeight);
		}
	}
	catch
	{
		throw new TeeChartException(Texts.ColorGridSeriesRangeExceed);
	}

	this.FillBitmap(ref tmpBounds, bbitmap);
	if (this.MinXValue() > 1)
	{
		  tmpBounds.X += Utils.Round(this.MinXValue());
		  tmpBounds.Width += Utils.Round(this.MinXValue());
	}
	if (this.MinZValue() > 1)
	{
		  tmpBounds.Y += Utils.Round(this.MinZValue());
		  tmpBounds.Height += Utils.Round(this.MinZValue());
	}
	R = this.CalcDestRectangle(tmpBounds);
	this.DrawBitmap(bbitmap, ref R);
}
Hope my small investigation will help you improve TeeChart.

Best regards,
Alexander

Posted: Wed Jan 25, 2006 4:31 pm
by Chris
Hello,
You could use, for instance, the following code to achieve this:
<snip>
Hope my small investigation will help you improve TeeChart.
Yes, thank you, I've incorporated similar code into TeeChart, a debug build of which will be available very soon.
Thanks a lot for this improvement. Our app now seems to work rather smooth when drawing ColorGrid. And I think that an option to disable value -> color mapping could make it even faster. Could you, please, notify me when you are done with this functionality?
You can avoid calling that code by setting either UseColorRange and UsePalette to false or by setting ColorEach to true. Alternatively, you could create your own ColorGrid series overriding the ValueColor property, e.g.

Code: Select all

	public class MyGrid : Steema.TeeChart.Styles.ColorGrid 
	{
		public MyGrid(Steema.TeeChart.Chart c) : base(c) 
		{
			ypalette = false;
		}

		private bool ypalette;

		/// <summary>
		/// Setting to true will create a Grid Palette based only on the Series YValues.
		/// </summary>
		[DefaultValue(false)]
		public bool YPalette 
		{
			get{return ypalette;}
			set{ypalette = value;}
		}

		public override Color ValueColor(int valueIndex)
		{
			if(ypalette) 
			{
				double argb = (-16777215 / YValues.Range) * YValues[valueIndex];
				return Color.FromArgb(Steema.TeeChart.Utils.Round(argb));
			}
			else 
				return base.ValueColor (valueIndex);
		}
	}

Posted: Wed Jan 25, 2006 7:26 pm
by 9639223
Hi, Christopher

You gave me a fine idea to inherit ColorGrid. I'll try to implement fast ColorGrid painting and will inform you about the results.

Best regards,
Alexander

Posted: Wed Jan 25, 2006 7:53 pm
by 9639223
Hi, Christopher

I've made the FastColorGrid class with several optimizations (including unsafe techniques) and achieved paint time of about 60ms on the ColorGrid with 500x500 values. The last debug build of TeeChart does the same in 500ms, and the original version - in 1750ms.

If you are interested in this I could send you the code.

Thanks for your help.

Best regards,
Alexander

Posted: Wed Jan 25, 2006 8:54 pm
by Chris
Hello Alexander,

Yes, please send me any TeeChart code that you have that you feel could be useful to its development.

Many thanks again,

Posted: Thu Jan 26, 2006 1:27 pm
by 9639223
Hi, Christopher

I've noticed one strange bug with TChart and ColorGrid.
I resize the form with the TChart anchored to it's borders and when the height of the chart becomes one of the magic values (say, 358, 362, 367, 151, 149...), ColorGrid doesn't paint. I didn't check the issue with other series.

I'm using TeeChart v2.0.2207.17144. I've made 3 screenshots showing the problem:
Image
Image
Image

PS I'll post my FastColorGrid code soon.

Regards,
Alexander

Posted: Thu Jan 26, 2006 5:56 pm
by 9639223
Hi,

This is my FastColorGrid class. Though I didn't run it through exhaustive testing, it seems to work correctly in my app. SimpleColoring property enables unconfigurable mapping of the y values to the color range (white; navy). You could add something like this to the TChart replacing the unsafe code with Marshal.Write... calls.

I also had to take some painting code from the TChart, 'cause those methods are private.

Code: Select all

using System;
using System.Collections.Generic;
using System.Text;
using Steema.TeeChart;
using System.Drawing;
using Steema.TeeChart.Styles;
using System.Reflection;
using System.Drawing.Imaging;
using Steema.TeeChart.Drawing;
using System.ComponentModel;

namespace WindowsApplication1
{
	public class FastColorGrid : Steema.TeeChart.Styles.ColorGrid
	{

		#region Private fields
		
		private bool simpleColoring = false;
		private Bitmap bbitmap = null;

		#endregion

		#region Construction/destruction
		
		public FastColorGrid()
		{
			base.IrregularGrid = false;
		}

		public FastColorGrid(Chart c)
			: base(c)
		{
			base.IrregularGrid = false;
		}
		
		#endregion

		#region Public properties

		[DefaultValue(false)]
		public bool SimpleColoring
		{
			get { return simpleColoring; }
			set { simpleColoring = value; }
		}

		#endregion

		#region Overrides

		protected override void Draw()
		{
			if (base.Count <= 0)
				return;

			int horzStartPos = (int)GetHorizAxis.CalcPosPoint(GetHorizAxis.IStartPos);
			int horzEndPos = (int)GetHorizAxis.CalcPosPoint(GetHorizAxis.IEndPos);
			if (GetHorizAxis.Inverted)
				horzStartPos++;
			else
				horzEndPos++;
			int vertStartPos = (int)GetVertAxis.CalcPosPoint(GetVertAxis.IStartPos);
			int vertEndPos = (int)GetVertAxis.CalcPosPoint(GetVertAxis.IEndPos);
			if (GetVertAxis.Inverted)
				vertEndPos++;
			else
				vertStartPos++;

			Rectangle rectangle1 = Rectangle.FromLTRB(horzStartPos, vertStartPos, horzEndPos, vertEndPos);
			Chart.Graphics3D.OrientRectangle(ref rectangle1);
			rectangle1.X -= Utils.Round(this.MinXValue());
			rectangle1.Y -= Utils.Round(this.MinZValue());
			rectangle1.Width -= Utils.Round(this.MinXValue());
			rectangle1.Height -= Utils.Round(this.MinZValue());
			if (rectangle1.X < 1)
			{
				rectangle1.X = 1;
			}
			if (rectangle1.Y < 1)
			{
				rectangle1.Y = 1;
			}
			if (rectangle1.Right > NumXValues)
			{
				rectangle1.Width = NumXValues - rectangle1.X;
			}
			if (rectangle1.Bottom > NumZValues)
			{
				rectangle1.Height = NumZValues - rectangle1.Y;
			}

			Rectangle rectangle2 = new Rectangle(0, 0, 0, 0);
			DrawCellUsingBitmap(ref rectangle1, ref rectangle2);
		}

		#endregion

		#region Private methods

		private void DrawCellUsingBitmap(ref Rectangle tmpBounds, ref Rectangle R)
		{
			int newWidth = Math.Abs((int)((1 + tmpBounds.Right) - tmpBounds.Left));
			int newHeight = Math.Abs((int)((1 + tmpBounds.Bottom) - tmpBounds.Top));
			if (newWidth == 0 || newHeight == 0 || newWidth > 3000 || newHeight > 3000)
				throw new TeeChartException(Texts.ColorGridSeriesRangeExceed);

			if (bbitmap == null || bbitmap.Width != newWidth || bbitmap.Height != newHeight)
			{
				if (bbitmap != null)
					bbitmap.Dispose();

				bbitmap = new Bitmap(newWidth, newHeight);
			}

			FillBitmap(ref tmpBounds, bbitmap);

			if (MinXValue() > 1)
			{
				tmpBounds.X += Utils.Round(MinXValue());
				tmpBounds.Width += Utils.Round(MinXValue());
			}
			if (MinZValue() > 1)
			{
				tmpBounds.Y += Utils.Round(MinZValue());
				tmpBounds.Height += Utils.Round(MinZValue());
			}
			R = CalcDestRectangle(tmpBounds);
			DrawBitmap(bbitmap, ref R);
		}

		private void FillBitmap(ref Rectangle tmpBounds, Bitmap bitmap)
		{
			int width = bitmap.Width;
			int height = bitmap.Height;
			int top = tmpBounds.Top;
			int bottom = tmpBounds.Bottom;
			int left = tmpBounds.Left;
			int right = tmpBounds.Right;

			BitmapData data1 = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppRgb);
			IntPtr ptr1 = data1.Scan0;

			unsafe
			{
				int* imgBase = (int*)ptr1.ToPointer();
				if (simpleColoring)
				{
					double[] v = YValues.Value;
					int xValues = NumXValues;
					int zValues = NumZValues;

					double minYValue = YValues.Minimum;
					double range = 255 / (YValues.Maximum - YValues.Minimum);
					int left1 = left - 1;
					int xcnt = right - left + 1;

					for (int z = top; z <= bottom; z++)
					{
						int zz = z - top;
						int* p = imgBase + zz * width;
						int t = (z - 1) + zValues * left1;
						for (int x = xcnt; x > 0; --x)
						{
							int c = 255 - (int)((v[t] - minYValue) * range);
							t += zValues;
							*p++ = c << 16 | c << 8 | c >> 1 | 0x80;
						}
					}
				}
				else
				{
					for (int z = top; z <= bottom; z++)
					{
						int zz = z - top;
						int* p = imgBase + zz * width;
						for (int x = left; x <= right; x++)
						{
							Color color = Color.White;
							int idx = base[x, z];
							if (idx != -1)
							{
								color = this.ValueColor(idx);
								if (color == Color.Empty)
									color = Color.White;
							}
							*p++ = color.ToArgb();
						}
					}
				}
			}
			bitmap.UnlockBits(data1);
		}

		private void DrawBitmap(Bitmap bitmap, ref Rectangle r)
		{
			Graphics3D graphicsd1 = Chart.Graphics3D;
			graphicsd1.PrepareDrawImage();
			graphicsd1.Draw(r, bitmap, false);
		}

		private Rectangle CalcDestRectangle(Rectangle tmpBounds)
		{
			int num1 = GetHorizAxis.CalcPosValue(Math.Max(MinXValue(), CalcMinValue((double)(tmpBounds.Left - 1))));
			int num2 = GetHorizAxis.CalcPosValue(Math.Min(MaxXValue(), CalcMaxValue((double)tmpBounds.Right)));
			int num3 = GetVertAxis.CalcPosValue(Math.Max(MinZValue(), CalcMinValue((double)(tmpBounds.Top - 1))));
			int num4 = GetVertAxis.CalcPosValue(Math.Min(MaxZValue(), CalcMaxValue((double)tmpBounds.Bottom)));
			return Rectangle.FromLTRB(num1, num3, num2, num4);
		}

		private double CalcMinValue(double Value)
		{
			if (!CenteredPoints)
			{
				return Value;
			}
			return (Value - 0.5);
		}

		private double CalcMaxValue(double Value)
		{
			if (CenteredPoints)
			{
				return (Value + 0.5);
			}

			return (Value + 1);
		}

		#endregion
	}
}
Best regards,
Alexander

Posted: Thu Jan 26, 2006 6:00 pm
by 9639223
One more thing I forgot to ask: why does the ColorGrid series disappear when I move it (with right mouse button) right or up? This doesn't happen when the grid is moved left or down. I guess this is the behavior of the chart itself, not the ColorGrid. But I had not time to check this thoroughly.