Introduction

The Simple Performance Chart is a UserControl that is designed and developed to display varying
performance data like reads per second on a disk drive, the bandwidth for a server, or free CPU resources,
in a visual, clean manner. It can be controlled by a built-in Timer, which makes synchronized display of
values possible. The control offers several formatting options like border style, line colors and
styles, widths, a background gradient, and so on.

The purpose of this article is not only to provide necessary information on how to use
this simple control, but also to show how a few interesting issues
have been solved. It's neither intended for covering all drawing techniques, nor to provide a
ready solution for a production environment.

Features

Absolute and Relative mode with automatic scaling

Built-in Timer for display synchronization or
Real-time display

Customizable display (colors, line style, borders)

Background

I found many chart drawing projects on the web, but most of them were intended for displaying complete data
records in countless possible variations. I could not find a good control
that could display a "real-time"
chart of my server's performance, which I collected with the PerformanceCounter
component (or fetched
from other sources). So I wrote my own, and learned a few tricks and techniques, which I'd like to share with you now.

Using the code

Set up the Chart Control

Using the control is pretty easy. Simply link the required assembly reference ("SpPerfChart.dll"),
compile your project, choose the SpPerfChart control from the
Toolbox and drag it into your Form or
any Control. You can edit all of the control's properties in the designer's Properties
panel.

Provide performance data

Other than the ability to Clear() the chart from any data, there
is only one public method
available for providing the data: AddValue(decimal). Any
time you obtain a value from anywhere (released by an event, obtained from
recurring routines, and so on), add it to the PerfChart control using
the AddValue(decimal) method. No need to worry about data preparation or display synchronization.
Be sure to set up the PerfChart control properly for your scenario.

Now that we know how to use the control, let's take a look at some interesting issues and how
they have been solved.

Drawing Methods

The actual drawing of the graph is one of the easier parts of the project. I decided to work with two Points,
representing the current and the previous value position. While
iterating through the value collection,
the Point instances are being reused every time. I used a "trick" to avoid an additional condition here:
Instead of skipping the drawing of the first line inside the loop (initial "previous value" is zero), the first line is
drawn outside the control's bounds.

You probably noticed the gridScrollOffset variable here. It's required because while the horizontal
gridlines always have the same positions, the vertical grid is scrolling with the
chart (line) itself. The
gridScrollOffset value is calculated every time when a new performance value is added.

Quick and flicker-free drawing: Double Buffering

All animated elements (or just regions) must redraw on each change, which can cause ugly flickering, depending on
the redraw speed and size of the canvas to be redrawn. (You could limit the region to reduce the flickering effect,
but since it's scrolling, this is not an option for this chart control.) Basically I found two different
options which helped in this case:

You can enable double buffering for a control using the following (recommended) method:

I tested the options without a clear conclusion - the results were satisfying in both cases.
The first method is
more specific on how to handle drawing and thus more optimized, which is why I recommend
it. You can find more
information on double-buffering in the References section.

Relative Value Scaling

An essential issue was the capability to display arbitrary values in a proper relationship, limited
to the actual viewing range. Defining a fixed Maximum value (for example, a network bandwidth of 100,000 kbits)
would result in unrecognizably flat graph changes when the network reports (as
it usually might) a low bandwidth usage of maybe
10, 100 or 1,000 kbits. With automatic relative scaling, the highest visible value is the measure for all
display calculations. Because it's dynamic, this value is displayed in the upper-left corner of the chart.

The number of visible values is calculated from the control's Width and the (fixed) horizontal value
spacing (in pixels). The highest displayed value results from a simple loop that checks all visible values to find the highest one. Finally,
a few simple mathematical "rule of three" functions are performed to calculate the actual pixel positions.
This is handled by the CalcVerticalPosition() method:

As you can see, the method is quite simple. The currentMaxValue > 0 condition prevents the method
from generating a division by zero. It could have been combined with the ScaleMode comparison in the
else if clause,
but this more structured scheme looked "safer" for future extensions.

We don't have to care about the display synchronization as long we can be sure that our "performance provider" (actually a class
supplying the values) is providing all values at regular intervals. For example, this is very suitable for displaying a
CPU usage chart, where we measure the data once a second, controlled by our custom Timer
component.

But sometimes, we can't tell how regular the intervals can be, or how many values we receive each second. The Simple Performance
Chart offers three synchronization options to address this problem: Simple,
SynchronizedAverage and SynchronizedSum.
Let's take a look at typical examples for each option:

TimerMode: Simple

Case example: Web server request duration in a testing environment

We want each single request duration to be visible on the chart, so that we can track down conspicuously long-lasting
requests. We prefer the Simple TimerMode instead of the manual method because the values are reported in real-time and
there could be dozens of requests in a second. Disabled TimerMode would cause a redraw for each value, slowing down
the performance of the whole control. We use the Simple TimerMode to refresh the chart only once a second. On every interval, all
collected values (during the interval) are "flushed" and displayed in the chart.

TimerMode: SynchronizedAverage

Case example: Web server download file size statistics

Like in the previous case, the values are reported in real-time. Every starting request triggers the AddValue()
method, providing the size of the requested file (in bytes). Because we only need a simple, statistical overview of the file throughput,
we enable the SynchronizedAverage TimerMode. On each interval it will report exactly one value: the calculated
average of all collected values during the interval time span. Example: The provider reported three file downloads during a second:
100 kB, 200 kB and 900 kB. The chart will display the average of 400 kB.

TimerMode: SynchronizedSum

Case example: Web server simultaneous users load

Let's assume that most conditions can be derived from the previous case. But this time, we are interested in the actual
amount of users requesting our web server per second. (The actual method parameter can even be a hard-coded
1.0).
The SynchronizedSum TimerMode will add all values provided during the interval period.

The Demo Application offers dynamic value generators where all behaviors can be tested and comprehended.

Visual Styling

It's quite an easy matter to allow extensive formatting possibilities for each single drawn element. You can
modify dozens of properties for each single line (like Color, Width or DashPattern, just to name few). I don't want to cover all the possibilities in
this article.

Style Properties Object Located in a Sub-Property

I decided to allow different formatting options per element: The lines (main chart line, optional average-level line, optional grid lines)
can have custom Colors, Widths and DashStyles. Besides that, there
are a few other
settings like the background gradient colors and visibility of gridlines.

It wouldn't be a good approach to put all these possible properties directly into the
control class itself, so I
created a simple class called PerfChartStyle, containing just the formatting properties.
A global instance of this class was created and exposed in a property with the same
name (PerfChartStyle).

Now, this was the resulting Designer Property Bar:

So what's that? The property was not declared as "read-only". Nevertheless, the
forms designer doesn't allow
a closer look into the style class. Or rather, it doesn't know about it yet.
That is what we need to tell the designer.

A TypeConverter "...provides a unified way of converting types of values to other types, as well as for accessing standard values and subproperties." (from
MSDN). With the TypeConverter attribute we tell the component designer
which TypeConverter we'd like to use. The ExpandableObjectConverter is a converter which discloses its public and visible
properties to a PropertyGrid.
I won't cover the details here; for more information on this topic, continue to the references section.

Now that we set up the right TypeConverter, we're not ready yet: Our changes are only
effective while in design mode!
The UserControl isn't able to keep the changes inside the child properties. That has something to to with
the ComponentModel architecture. Oh well, it's not able yet: Let's move from
attributes
on the class to the actual PerfChartStyle property of the SPPerfChart
control and its attributes:

Conclusion

This Performance Chart project is a good example of how to create a basic UserControl with custom drawing.
But we are not finished yet: Many ideas for possible improvements are waiting! We can improve the
design-time integration, have a closer look at possible threading issues, and integrate a hundred new features. But if
this simple UserControl can satisfy our needs, our whole effort to implement this solution from scratch is
low, thanks to .NET and its straightforward but powerful drawing capabilities.

Share

About the Author

Thomas' (professional) software development career started in 1998, where he made his first experience with Java, with main focus on web/internet development with JSP and later, ColdFusion, until he finally was confronted with the Microsoft .NET technology.

Comments and Discussions

Thanks for your compliments! There is no option like this, but with a little source code manipulation you should be able to get the results you need. Try to understand what Method CalcVerticalPosition() does. Sorry that I cannot help you further

The software was originally released under the LGPL license as stated in the document. If the restrictions aren't clear (i might choose a different license in future, since i found the LGPL beeing not that suitable) I hereby grant you the right to use it in any commercial software without further attribution.

This control is very useful.You have listed both LGPL v2.1 and LGPL v3 licenses. Which one is applicable?As per your earlier reply can this still be used in a commercial software without further attribution?

Is there any way to adjust the scale of the Y-axis when set to ScaleMode = Absolute? It defaults to work for values from 0 to 100, but I'd like to be able to scale out to accept values >100, and to scale down to handle data that scales from 0 to 1.

Not bad. It does want I want. However, allow me to suggest some performance tweaks.

1. Create a fixed-size circular list class such that item[0] is always the oldest data item. Implement as an array with head and tail indices. Why? When your List<> gets full, inserting at the head copies the entire underlying array 1 element (i.e. 0->1, 1->2, etc). The circular list performs 0 copies. Nice.

2. Create a PointF[] array with the max number of points. In DrawChart, loop over all data items putting each value in successive PointFs. Then you can call Graphics.DrawLines passing ALL points at once. This reduces your calls into GDI+ from MAX_VALUE_COUNT per draw to 1 per draw. Nice.

3. Investigate Matrix. Create a Matrix that shifts the origin to bottom of the window and right to where you want to start drawing (translate operation). Also invert the y-axis (scale operation). Finally, scale x and y so that your real-world values are mapped to the available client width/height. Then, you can just use the real point indices (i.e. 0, 1, 2, ...) for the X value of your PointFs and the actual real-world values for the Y of your PointFs. Just before you call DrawLines, call Matrix.TransformPoints to map all real-world PointFs into their pixel counterparts. Again, reduces the number of calls to your CalcVerticalPosition. Microsoft is motivated to make the TransformPoints method fast. Nice. [I can recommend Chris Sells' "Windows Forms Programming in C#" as a reference to transforms that is FAR more helpful than MSDN.] [You already know the math involved since you did it yourself in CalcVerticalPosition. Breaking it down into translate, scale, scale should make it obvious.]

(Note: you could set the resulting transform matrix into the Graphics object and then DrawLines will transform your points as it draws. I recommend against this. Why? Because it applies the same transform to your *pen*! So your 1 pixel width pen gets scaled. It stinks. By doing the transforms yourself, you can create a pen of width 2 pixels and actually *get* 2 pixels.)

After all that, your ChartDraw loops from 0 to number of data points. At each point, you just slap the index in as X and the real value as Y. Then you DrawLines. Boom.

These changes, in my mind, simplify the drawing code. They do speed it up. I'm using it at higher rates and needed the lower processor utilization. I'd post my changes but I did them on my company's nickel and I can't do it.

wow, that looks really helpful! I'm really impressed! Thanks, I really appreciate your suggestions, and the explanations sound very convincing. Nice to see someone finally analyzing the project and getting deeper into the details. I hope that it was no pain doing this, I really tried hard to write clean code

I must confess that I didn't spend much time optimizing the drawing methods. I was aware of some possibilities (like the drawing of multiple lines using DrawLines), but the possibilities of the Matrix sound very interesting.

After using the built-in graphic transforms with Matrix, my performance was abysmal. I did not do any timing beforehand, but the graphics transforms resulted in an average update time of 8.8 ms. Pretty bad.

Timing separately, DrawLines took 1.3 ms and TransformPoints took 6.4 ms. The rest of the time went to drawing the border and the text. Well, nothing I can do about DrawLines but the TransformPoints is puzzling. Why should it take almost 5 times as long to transform the points as it does to draw pixels? Until I thought about it.

Consider that the transform matrix is 3x3. The point matrix is 1x3. To do the matrix multiply and get the result costs 3 multiplies and 2 additions per dimension (3). So 9 multiplies and 6 additions per point. I'm doing 1024 points per update, so that is 15k floating point ops (flops). Ten times per second yields 150k flops. That's a few.

Now, to do 2-D graphing like this is nothing but an origin shift, flipping the y-axis, and scaling to fit the window. I can do that using a linear point transformation, and you all know the form: y = mx + b. Calculate m and b *once* outside the transform loop, and you get 1 multiply and 1 add for 2k flops per update, a savings of 13k flops. Fairly significant.

eclipse2k1's original implementation (in that regard) was optimal!

Implementing this transform speeds the whole DrawChart method up to 2.4 ms per update, nearly a 4x improvement. So forget about Matrix and transforms for 2D stuff. Just implement your point transform (i.e. from real-world value to pixel) in-line in DrawChart to avoid the overhead of a function call.

Summary. I have a circular list to reduce my copy operations. I loop thru each point in the list and transform them into a PointF structure. Then call DrawLines. That's about as fast as I can get. The slowest part now is the Draw3DBorder, which is terrible!

Oh, switching from double-buffered, user-drawn control to an internal bitmap which I then blit to the window saves 0.1 ms.

wow you did a heavy study on this one, i'm very impressed. Really interesting to hear that the original basic implementation had also best performing parts - I wouldn't believe that (not at all that a Matrix transform has those drawbacks). But oh well, we all learn, and in this case, we seem to have learned both a lot of things. Thanks for your outstanding commitment!

I don't have much ASP.NET experience, but i guess there are two issues that need to be solved; First, The Control has to render as an image. There are no such extensive drawing possiblilties on client side. And second, it has to refresh every time the chart has changed, which means that all bitmap data has to be transfered to the client, therefore it makes this Performance Chart Engine quite unreliable for Web client use.

I would recommend a solution with better client-side integration. This could be flash-based, probably XAML or a Java Applet. The Server then only needs to transmit single values, the chart is actually drawn and displayed client-side.