简单的性能图表






4.92/5 (119投票s)
一个简单的性能图表/监控控件
引言
简单性能图表是一个UserControl
,旨在以视觉、简洁的方式显示各种性能数据,例如磁盘驱动器的每秒读取次数、服务器的带宽或空闲CPU资源。它可以通过内置的Timer
进行控制,从而实现值的同步显示。该控件提供了多种格式设置选项,例如边框样式、线条颜色和样式、宽度、背景渐变等。
本文的目的不仅是提供有关如何使用此简单控件的必要信息,还要展示一些有趣的问题是如何解决的。它既不旨在涵盖所有绘图技术,也不提供生产环境的现成解决方案。
特点
- 具有自动缩放功能的绝对和相对模式
- 内置计时器用于显示同步或实时显示
- 可定制的显示(颜色、线条样式、边框)
背景
我在网上找到了许多图表绘制项目,但它们大多数都是为了以无数种可能的变体显示完整的N记录。我找不到一个好的控件来显示我用PerformanceCounter
组件收集的(或从其他来源获取的)服务器性能的“实时”图表。所以我自己写了一个,并学到了一些技巧和技术,现在我想与大家分享。
Using the Code
设置图表控件
使用该控件非常简单。只需链接所需的程序集引用(“SpPerfChart.dll”),编译您的项目,从工具箱中选择SpPerfChart
控件并将其拖到您的Form
或任何Control
中。您可以在设计器的属性面板中编辑控件的所有属性。
提供性能数据
除了能够Clear()
图表中的任何数据之外,只有一个public
方法可用于提供数据:AddValue(decimal)
。无论何时您从任何地方(由事件发布、从重复例程获取等)获取值,都使用AddValue(decimal)
方法将其添加到PerfChart
控件中。无需担心数据准备或显示同步。请务必为您的场景正确设置PerfChart
控件。
关注点
现在我们知道了如何使用该控件,让我们来看看一些有趣的问题以及它们是如何解决的。
绘图方法
图表的实际绘制是项目中最简单的部分之一。我决定使用两个Point
,分别表示current
和previous
值位置。在遍历value
集合时,Point
实例每次都会被重用。我使用了一个“技巧”来避免在这里增加一个额外的条件:不是在循环内跳过绘制第一行(初始“previous value”为零),而是在控件的边界之外绘制第一行。
所以让我们看看绘图方法的基本部分
/// <summary>
/// Draws the chart (w/o background or grid, but with border) to the
/// Graphics <paramref name="g"/>
/// </summary>
/// <param name="g">Graphics</param>
private void DrawChart(Graphics g) {
visibleValues = Math.Min(this.Width / valueSpacing, drawValues.Count);
if (scaleMode == ScaleMode.Relative)
currentMaxValue = GetHighestValueForRelativeMode();
//"trick": initialize the first previous Point outside the bounds
Point previousPoint = new Point(Width + valueSpacing, Height);
Point currentPoint = new Point();
// Only draw average line when possible (visibleValues)
//and needed (style setting)
// [...]
// Connect all visible values with lines
for (int i = 0; i < visibleValues; i++) {
currentPoint.X = previousPoint.X - valueSpacing;
currentPoint.Y = CalcVerticalPosition(drawValues[i]);
// Actually draw the line
g.DrawLine(perfChartStyle.ChartLinePen.Pen, previousPoint,
currentPoint);
previousPoint = currentPoint;
}
// Draw current relative maximum value string
// [...]
// Draw Border on top
ControlPaint.DrawBorder3D(g, 0, 0, Width, Height, b3dstyle);
}
这看起来相当简单,而且确实如此!查看以下源代码,了解如何绘制背景渐变和网格。我不会对此浪费太多文字。
/// <summary>
/// Draws the background gradient and the grid into Graphics
/// <paramref name="g"/>
/// </summary>
/// <param name="g">Graphic</param>
private void DrawBackgroundAndGrid(Graphics g) {
// Draw the background Gradient rectangle
Rectangle baseRectangle = new Rectangle(0, 0, this.Width, this.Height);
using (Brush gradientBrush = new LinearGradientBrush(
baseRectangle, perfChartStyle.BackgroundColorTop,
perfChartStyle.BackgroundColorBottom, LinearGradientMode.Vertical))
{
g.FillRectangle(gradientBrush, baseRectangle);
}
// Draw all visible, vertical gridlines (if wanted)
if (perfChartStyle.ShowVerticalGridLines) {
for (int i = Width - gridScrollOffset; i >= 0; i -= GRID_SPACING) {
g.DrawLine(perfChartStyle.VerticalGridPen.Pen, i, 0, i, Height);
}
}
// Draw all visible, horizontal gridlines (if wanted)
if (perfChartStyle.ShowHorizontalGridLines) {
for (int i = 0; i < Height; i += GRID_SPACING) {
g.DrawLine(perfChartStyle.HorizontalGridPen.Pen,
0, i, Width, i);
}
}
}
你可能注意到了这里的gridScrollOffset
变量。这是必需的,因为水平网格线始终具有相同的位置,而垂直网格则随图表(线)本身滚动。每次添加新的性能值时都会计算gridScrollOffset
值。
快速无闪烁绘图:双缓冲
所有动画元素(或仅区域)必须在每次更改时重新绘制,这可能会导致难看的闪烁,具体取决于重绘速度和要重绘画布的大小。(您可以限制区域以减少闪烁效果,但由于它是滚动的,因此这对于此图表控件来说不是一个选项。)基本上,我发现了两种不同的选项,在这种情况下有所帮助
您可以使用以下(推荐的)方法为控件启用双缓冲
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
或者,您只需设置控件的DoubleBuffered
属性
this.DoubleBuffered = true;
我测试了这些选项,没有明确的结论 - 两种情况下的结果都令人满意。第一种方法在如何处理绘图方面更具体,因此更优化,这就是我推荐它的原因。您可以在参考部分找到更多关于双缓冲的信息。
相对值缩放
一个重要的问题是能够以适当的关系显示任意值,仅限于实际的查看范围。定义一个固定的Maximum
值(例如,100,000 kbits的网络带宽)会导致在网络报告(通常可能)低带宽使用量为10、100或1,000 kbits时,图表变化不明显。通过自动相对缩放,最高可见值是所有显示计算的度量。因为它是动态的,所以该值显示在图表的左上角。
可见值的数量是根据控件的Width
和(固定)水平值间距(以像素为单位)计算的。显示的最高值是通过一个简单的循环计算出来的,该循环检查所有可见值以找到最高值。最后,执行一些简单的数学“三法则”函数来计算实际的像素位置。这由CalcVerticalPosition()
方法处理
/// <summary>
/// Calculates the vertical Position of a value in relation the chart size,
/// Scale Mode and, if ScaleMode is Relative, to the current maximum value
/// </summary>
/// <param name="value">performance value</param>
/// <returns>vertical Point position in pixels</returns>
private int CalcVerticalPosition(decimal value) {
decimal result = Decimal.Zero;
if (scaleMode == ScaleMode.Absolute)
result = value * this.Height / 100;
else if (scaleMode == ScaleMode.Relative)
result = (currentMaxValue > 0) ?
(value * this.Height / currentMaxValue) : 0;
result = this.Height - result;
return Convert.ToInt32(Math.Round(result));
}
如您所见,该方法非常简单。currentMaxValue > 0
条件阻止该方法生成除以零。它可以与else if
子句中的ScaleMode
比较结合使用,但这种更结构化的方案对于未来的扩展看起来更“安全”。
同步值显示
只要我们能确保我们的“性能提供者”(实际上是一个提供值的类)以固定的时间间隔提供所有值,我们就不必关心显示同步。例如,这非常适合显示CPU使用率图表,其中我们每秒测量一次数据,由我们的自定义Timer
组件控制。
但有时,我们无法确定间隔的规律性,或者我们每秒收到多少值。简单性能图表提供了三种同步选项来解决此问题:Simple
、SynchronizedAverage
和SynchronizedSum
。让我们看看每种选项的典型示例
TimerMode: Simple
案例示例:测试环境中的Web服务器请求持续时间
我们希望在图表上显示每个请求的持续时间,以便我们可以跟踪异常长时间的请求。我们更喜欢Simple TimerMode
而不是手动方法,因为值是实时报告的,并且每秒可能有数十个请求。Disabled TimerMode
会导致每个值都重绘,从而降低整个控件的性能。我们使用Simple TimerMode
每秒只刷新图表一次。在每个间隔,所有收集到的值(在间隔期间)都会被“刷新”并显示在图表中。
TimerMode: SynchronizedAverage
案例示例:Web服务器下载文件大小统计
与上一个案例一样,这些值是实时报告的。每个开始的请求都会触发AddValue()
方法,提供请求文件的大小(以字节为单位)。因为我们只需要一个简单的文件吞吐量统计概述,所以我们启用了SynchronizedAverage TimerMode
。在每个间隔,它将报告一个值:该间隔期间所有收集值的计算平均值。示例:提供者在一秒内报告了三次文件下载:100 KB、200 KB和900 KB。图表将显示400 KB的平均值。
TimerMode: SynchronizedSum
案例示例:Web服务器并发用户负载
让我们假设大多数条件都可以从上一个案例中得出。但这次,我们对每秒请求我们Web服务器的实际用户数量感兴趣。(实际的方法参数甚至可以是硬编码的1.0
)。SynchronizedSum TimerMode
将添加在间隔期间提供的所有值。
演示应用程序提供动态值生成器,可以在其中测试和理解所有行为。
视觉样式
现在让我们介绍一下我们的性能图表库的样式设置可能性。
为每个单独绘制的元素提供广泛的格式设置可能性是一件相当容易的事情。您可以修改每个单行的数十个属性(例如Color
、Width
或DashPattern
,仅举几例)。我不想在本文中涵盖所有可能性。
样式属性对象位于子属性中
我决定允许每个元素有不同的格式选项:线条(主图线、可选的平均水平线、可选的网格线)可以有自定义的Color
、Width
和DashStyle
。除此之外,还有一些其他设置,如背景渐变颜色和网格线的可见性。
将所有这些可能的属性直接放入控件类本身并不是一个好方法,所以我创建了一个名为PerfChartStyle
的简单类,只包含格式属性。创建了这个类的一个全局实例,并以相同的名称(PerfChartStyle
)作为属性公开。
现在,这是最终的设计器属性栏
那是什么?属性没有声明为“read-only
”。然而,表单设计器不允许更深入地查看style
类。或者更确切地说,它还不知道。这就是我们需要告诉设计器的地方。
第一次尝试
[TypeConverterAttribute(typeof(ExpandableObjectConverter))]
public class PerfChartStyle
{
private ChartPen verticalGridPen;
private ChartPen horizontalGridPen;
// [...]
TypeConverter
“...提供了一种统一的方式来将值类型转换为其他类型,以及访问标准值和子属性。”(来自MSDN)。通过TypeConverter
属性,我们告诉组件设计器我们希望使用哪个TypeConverter
。ExpandableObjectConverter
是一个转换器,它将其公共和可见属性公开给PropertyGrid
。我不会在这里详细介绍;有关此主题的更多信息,请继续阅读参考部分。
现在我们已经设置了正确的TypeConverter
,但我们还没有准备好:我们的更改只在设计模式下有效!UserControl
无法将更改保留在子属性中。这与ComponentModel
架构有关。哦,好吧,它还不能:让我们从类上的属性转移到SPPerfChart
控件的实际PerfChartStyle
属性及其属性
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
[Category("Appearance"), Description("Appearance and Style")]
public PerfChartStyle PerfChartStyle {
get { return perfChartStyle; }
set { perfChartStyle = value; }
}
第一行完成了整个技巧。使用DesignerSerializationVisibility.Content
,我们指示设计器的代码生成器为实际的子属性生成代码,而不是类本身。
这是最终结果
我们做到了!可扩展的样式属性,带有持久化的更改。
参考文献与延伸阅读
结论
这个性能图表项目是创建具有自定义绘图的基本UserControl
的一个很好的例子。但我们还没有完成:许多可能的改进想法正在等待!我们可以改进设计时集成,更仔细地研究可能的线程问题,并集成一百个新功能。但是,如果这个简单的UserControl
能够满足我们的需求,那么由于.NET及其直接而强大的绘图功能,我们从头开始实现这个解决方案的全部努力是低成本的。
可能的改进想法
有无数种方法可以改进此控件。如果您打算扩展SPPerfChart
,这里有一些想法。
- 在
ChartStyle
中允许更多设置,如网格大小或值间距 - 正确显示负值(支持零线)
- 允许多个叠加图线
- 允许从左到右滚动
- 提供ASP.NET支持
...以及更多功能。
历史
- 2007年1月17日:版本0.1:项目启动(本地版本)
- 2007年2月9日:版本1.0:首次公开发布(程序集版本:1.0.2595.43101)
- 2007年2月9日:版本1.01:文章 minor 修改;添加了许可证和免责声明
- 2013年9月25日:版本1.02:删除了LCPL许可证,应用了标准CPOL许可证