65.9K
CodeProject 正在变化。 阅读更多。
Home

支持 AJAX 的性能计数器 Web 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.66/5 (14投票s)

2007 年 3 月 4 日

BSD

13分钟阅读

viewsIcon

107278

downloadIcon

1245

一个 ASP.NET Web 控件,可将性能计数器数据以文本标签、进度条、直方图或折线图的形式呈现到屏幕上,并通过 AJAX 调用自动更新自身。

Screenshot - performancecountercontrol.gif

引言

在拜访您办公室的潜在客户眼中,什么比任何东西都更能给他们留下深刻印象?一个显示关键服务器实时性能数据的大屏幕。我不是开玩笑,只是部分地说:它能让公司看起来很专业,而且对于网络运营员工来说也是一个巨大的福音,他们可以立即发现服务器是否存在问题。显然,网页是这种性能数据展示的理想媒介,因为您可以为网络运营人员实现多个版本的性能数据显示,甚至可以供希望通过手机或 PDA 查看服务器状态的人员使用,并且还可以驱动数据自动更新。关于这个主题有一些基础的 Web 控件和教程(例如 4GuysFromRolla 上的这篇教程),但没有一个功能非常丰富或可以自动更新,而我认为自动更新是性能计数器数据显示最引人注目的特性。这让我感到有些惊讶,所以我决定自己动手开发您今天看到的这个 ASP.NET Web 控件。

背景

首先,有必要概述一下 Windows 环境中的性能指标数据。Windows 机器提供了一种理想的机制,可以通过性能计数器捕获这些数据:它们监控处理器、内存、磁盘 I/O、重要服务实例等系统组件,并发布各种与性能相关的数据。特定的性能计数器由四部分数据定义,第一部分是计数器的类别名称。顾名思义,这是性能计数器的通用类别,可以是内存、处理器、网络接口等。第二个数据点是计数器名称,即此监视器正在读取数据的该类别中的性能组件:例如,在内存类别中,您可以监视可用兆字节、每秒页数、系统代码总字节数等各种数据。第三个是实例名称,即我们正在监视的计数器实例。许多性能计数器没有实例,但有些计数器会收集特定组件的多个成员的性能指标。例如,在“进程”类别和“进程时间百分比”计数器下,我们有一个总 CPU 时间使用百分比的实例,以及系统上当前运行的每个进程的实例,这意味着我们可以收集整体 CPU 使用情况数据,也可以收集各个进程的 CPU 使用情况数据。性能计数器的最后一个数据点是计算机名称,它指示我们正在检查的性能计数器所在的网络上的计算机。这意味着您可以从一台中央计算机监视网络上多台计算机的性能计数器,然后由该中央计算机负责将这些指标以网页的形式发布。如果您想获取您机器上可用的性能计数器列表(您可以收集到大量令人惊叹的数据),只需转到“开始”->“运行”,然后键入 perfmon.exe。然后,您可以右键单击图表并选择“添加计数器”以获取可用计数器列表。.NET 通过 System.Diagnostics 命名空间中的 PerformanceCounter 类提供了对性能计数器的出色编程访问,我们将在本项目中大量使用该类(您可以在 此处找到 MSDN 对该类的介绍)。

实现

首先,本项目有一个先决条件:您需要安装 Microsoft 的 ASP.NET AJAX 框架,您可以在 此处找到。这并非因为其 AJAX 回调功能(ASP.NET 2.0 已提供非常基础的版本,足以满足本项目需求),而是因为其对客户端 JavaScript 代码的增强,我现在的大部分项目都使用这些增强功能。它们赋予客户端代码更具组织性、类似于 C# 的感觉,并提供继承、命名空间和枚举等增强功能。虽然对本项目而言,对这些功能的详尽概述并非必需,但感兴趣的读者可以 在此处了解更多信息。

基本功能

对于本项目,90% 的功能都出奇地容易实现,但最后 10% 包含了一些有趣的问题,需要对 ASP.NET 渲染过程进行一些创造性的使用,这将在后面介绍。首先,我们先回顾一下此控件提供的功能。最基本的功能是,它将性能计数器数据呈现到屏幕上,并通过 AJAX 调用在预定的时间间隔后自动更新自身。性能计数器数据在屏幕上的呈现形式由 PerformanceCounterDisplayType 枚举定义。

public enum PerformanceCounterDisplayType
{
  /// <summary>
  /// Display the current value of the counter.
  /// </summary>
  Text,
  /// <summary>
  /// Display a single, updating bar representing the percentage value of the 
  /// counter between its floor and ceiling.
  /// </summary>
  ProgressBar,
  /// <summary>
  /// Display a line graph of the counter's historical data.
  /// </summary>
  LineGraph,
  /// <summary>
  /// Display a histogram (bar graph) of the counter's historical data.
  /// </summary>
  Histogram
}

正如您所见,我们可以将数据呈现为简单的文本标签、进度条、历史折线图或历史直方图(条形图)。该控件的属性包含您期望的 Web 控件的许多属性,以及定义其正在渲染的性能计数器的属性。CategoryNameCounterNameInstanceNameMachineName 完成了后者的功能,分别定义了背景部分讨论的性能计数器的数据点。RefreshInterval 属性定义了此控件数据刷新尝试之间的间隔时间(以秒为单位)。FloorCeiling 属性用于渲染进度条、直方图和折线图,以便我们可以确定性能计数器值可能落入的范围。HistoryCounter 属性由直方图和折线图使用,用于确定屏幕上显示的历史数据点的数量。Modifier 属性是将实际性能计数器值乘以某个值,然后再返回,这对于在不同测量单位(如字节和千字节)之间进行转换非常有用。Invert 属性指示是否应在返回值之前反转性能计数器的值(从 Ceiling 属性中减去)。WidthHeightCssClass 属性控制控件数据在屏幕上的外观。FormatString 属性用于文本显示,以在屏幕上显示性能计数器值之前对其进行格式化。Orientation 属性用于进度条,以确定进度条“增长”的方向(水平或垂直)。最后,OnChange 属性表示在刷新性能计数器数据时应调用的客户端 JavaScript 函数。此函数应接受一个客户端 PerformanceCounter 对象(稍后将介绍)作为参数,并可用于向用户显示警告消息、更改控件的 CSS 类等。

渲染和值计算

现在我们来看看有趣的实现细节,首先是实际构建控件层次结构以在屏幕上显示性能计数器数据。因此,我们必须查看 CreateChildControls() 方法。

protected override void CreateChildControls()
{
  string hashKey = categoryName + ":" + counterName + ":" + instanceName + 
                   ":" + machineName;

  switch (displayType)
  {
    // In the case of a text display type, simply create a Label child object
    case PerformanceCounterDisplayType.Text:
      Label textLabel = new Label();
      textLabel.CssClass = cssClass;

      Controls.Add(textLabel);
      break;

    // In the case of a progress bar, create a label whose width and height 
    // represent the maximum width and height of the progress bar and 
    // another label that will represent the current value of the counter
    case PerformanceCounterDisplayType.ProgressBar:
      Label containerLabel = new Label();
      Label progressBarLabel = new Label();

      containerLabel.Width = width;
      containerLabel.Height = height;

      progressBarLabel.CssClass = cssClass;
      progressBarLabel.Style["position"] = "absolute";
      progressBarLabel.Style["overflow"] = "hidden";

      // Set the actual progress bar's style attributes according to 
      // whether it grows horizontally or vertically
      if (orientation == RepeatDirection.Vertical)
      {
        progressBarLabel.Style["bottom"] = "0px";
        progressBarLabel.Width = width;
        progressBarLabel.Height = 1;
      }

      else
      {
        progressBarLabel.Style["left"] = "0px";
        progressBarLabel.Height = height;
        progressBarLabel.Width = 1;
      }

      containerLabel.Controls.Add(progressBarLabel);
      Controls.Add(containerLabel);

      break;

    // In the case of a histogram, create a container panel and a 
    // sub-container panel, the latter of which will hold the histogram 
    // bars
    case PerformanceCounterDisplayType.Histogram:
      int sampleWidth = Convert.ToInt32(Math.Floor(
                                        (double)(width / historyCount)));
      Panel containerPanel = new Panel();
      Panel subContainerPanel = new Panel();

      containerPanel.Width = width;
      containerPanel.Height = height;
      containerPanel.Style["position"] = "relative";

      // Two panels are necessary so that the histogram bars are rendered 
      // properly
      subContainerPanel.Width = width;
      subContainerPanel.Style["position"] = "absolute";
      subContainerPanel.Style["bottom"] = "0px";

      Label[] histogramEntries = new Label[historyCount];

      for (int i = 0; i < historyCount; i++)
      {
        histogramEntries[i] = new Label();
        histogramEntries[i].CssClass = cssClass;
        histogramEntries[i].Width = sampleWidth;
        histogramEntries[i].Height = 1;
        histogramEntries[i].Style["position"] = "absolute";
        histogramEntries[i].Style["left"] = Convert.ToString(i * 
                                                             sampleWidth) 
                                            + "px";
        histogramEntries[i].Style["bottom"] = "0px";
        histogramEntries[i].Style["overflow"] = "hidden";

        subContainerPanel.Controls.Add(histogramEntries[i]);
      }

      containerPanel.Controls.Add(subContainerPanel);
      Controls.Add(containerPanel);

      break;

    // In the case of a line graph, simply create a container panel:  the 
    // vector graphics JavaScript library will take care of actually 
    // drawing the graph
    case PerformanceCounterDisplayType.LineGraph:
      Panel lineContainerPanel = new Panel();

      lineContainerPanel.Width = width;
      lineContainerPanel.Height = height;
      lineContainerPanel.Style["position"] = "relative";

      Controls.Add(lineContainerPanel);
      break;
  }

  // Create the performance counter object if it doesn't already exist and, 
  // if it's a non-instantaneous counter, add a sample to the history for it; 
  // we add a sample now so that when we go to actually calculate the initial 
  // value in the PreRenderComplete event handler, sufficient time will have 
  // passed:
  //  1. All controls call CreateChildControls(), adding the initial counter 
  //     sample to the history
  //  2. PreRenderComplete event handler is called for the first control:  it 
  //     gets the counter value from the Value property
  //  3. If the Value property detects that less than 100 milliseconds have 
  //     passed since the history entry was added in CreateChildControls, it 
  //     sleeps for 100 milliseconds and then gets another sample
  //  4. At that point, the calls to the Value property for all other 
  //     controls will occur after the necessary 100 milliseconds and no more 
  //     pausing will be necessary
  if (performanceCounter == null && 
      !performanceCounters.ContainsKey(hashKey))
  {
    performanceCounter = new PerformanceCounter(categoryName, counterName, 
                                                instanceName, machineName);
    performanceCounters[hashKey] = performanceCounter;

    if (!IsPerformanceCounterInstantaneous(performanceCounter))
    {
      performanceCounterSamples[hashKey] = new List<CounterSample>();
      performanceCounterSamples[hashKey].Add(
         performanceCounter.NextSample());
    }
  }

  // If the counter object already exists, just use the copy from the cache
  else if (performanceCounter == null && 
           performanceCounters.ContainsKey(hashKey))
    performanceCounter = performanceCounters[hashKey];

  // Add an event handler to the page's pre-render complete event
  Page.PreRenderComplete += new EventHandler(Page_PreRenderComplete);

  base.CreateChildControls();
}

控件层次结构的创建相对简单:对于文本显示,我们只创建一个 Label 对象;对于进度条,我们创建一个表示进度条最大可能大小的 Label 对象,以及一个表示当前值的 Label 对象;对于直方图,我们创建一个容器 Panel 对象,然后创建表示图中每个条形的 Label 对象;对于折线图,我们创建一个容器 Panel 对象,我们将在稍后对其进行绘图。有趣的代码位于函数最后的几行。在这里,如果 PerformanceCounter 对象尚不存在,我们则创建它。如果它已经存在,我们则使用存储在缓存中的对象;这是为了防止在有多个控件监视同一个性能计数器但以不同方式显示数据的情况下创建重复对象。我们还捕获那些需要初始计数器样本的计数器的初始样本。这是因为某些计数器需要多个样本才能计算其值。网络流量就是一个很好的例子:我们需要捕获两个样本,因为我们需要记录两个样本之间经过的时间以及网卡传输的总字节数的变化。一旦我们获得了这两个值,我们就可以计算该时间段内的每秒传输字节数。但是,我们需要分两个阶段进行此操作,并确保样本之间已经过了最短时间:如果我们连续捕获两个样本,但两个样本之间经过的时间非常短,我们就无法计算出统计上相关的数值。因此,我们在 ASP.NET 渲染过程中分两个阶段进行此操作,以确保我们能够计算出准确的结果,同时花费最短的必要时间。注释中概述了此过程,但我将在此重新说明:

  1. 页面中的每个 PerformanceCounterControl 都会调用 CreateChildControls() 方法。
  2. 对于非即时计数器,我们将计数器的初始样本添加到历史数据中。
  3. PreRenderComplete 事件发生在页面生命周期的 PreRenderComplete 阶段,此时每个控件都会调用其 Page_PreRenderComplete() 处理程序方法。
  4. 第一个非即时计数器控件将调用 Value 属性来获取计数器的当前值。
  5. Value 属性内部,我们检查自步骤 2 中添加第一个历史条目以来是否已至少经过 100 毫秒。
  6. 如果未经过,则我们将线程休眠 100 毫秒,然后获取计数器样本,并根据两个历史样本计算值。
  7. 此时,所有后续的非即时计数器控件都会看到自步骤 2 以来已超过 100 毫秒,因此在获取新的计数器样本和计算值之前不需要线程休眠。

前面提到的 Page_PreRenderComplete() 方法如下所示:

protected void Page_PreRenderComplete(object sender, EventArgs e)
{
  // Add the client-side performance counter initialization snippet
  if (!Page.ClientScript.IsStartupScriptRegistered(GetType(), 
         "PerformanceCounterInitialize"))
    Page.ClientScript.RegisterStartupScript(GetType(), 
       "PerformanceCounterInitialize", 
       "var performanceCounters = new Array();\n", true);

  string childIDs = "";

  // Get the list of client IDs for all child elements for client-side 
  // registration
  switch (displayType)
  {
    case PerformanceCounterDisplayType.Text:
    case PerformanceCounterDisplayType.LineGraph:
      childIDs = "'" + Controls[0].ClientID + "'";
      break;

    // For progress bars and histograms, get the value of performance 
    // counters and render the data appropriately
    case PerformanceCounterDisplayType.ProgressBar:
      if (orientation == RepeatDirection.Horizontal)
        ((Label)Controls[0].Controls[0]).Width = 
           Convert.ToInt32(Math.Max(Math.Min(Math.Floor(
              (Value - floor) / ceiling * width), width), 1));

      else
        ((Label)Controls[0].Controls[0]).Height = 
           Convert.ToInt32(Math.Max(Math.Min(Math.Floor(
              (Value - floor) / ceiling * height), height), 1));

      childIDs = "'" + Controls[0].Controls[0].ClientID + "'";
      break;

    case PerformanceCounterDisplayType.Histogram:
      foreach (Control control in Controls[0].Controls[0].Controls)
        childIDs += "'" + control.ClientID + "', ";

      ((Label)Controls[0].Controls[0].Controls[historyCount - 1]).Height = 
         Convert.ToInt32(Math.Max(Math.Min(Math.Floor(
            (Value - floor) / ceiling * height), height), 1));

      childIDs = childIDs.Substring(0, childIDs.Length - 2);
      break;
  }

  // Register the necessary client script resources and emit the registration 
  // snippet for this control
  Page.ClientScript.GetCallbackEventReference(this, null, 
     "RenderPerformanceCounter", "'" + ClientID + "'");
  Page.ClientScript.RegisterClientScriptResource(typeof(ScriptManager), 
     "MicrosoftAjax.js");
  Page.ClientScript.RegisterClientScriptResource(typeof(ScriptManager), 
     "MicrosoftAjaxWebForms.js");
  Page.ClientScript.RegisterClientScriptResource(GetType(), 
     "Stratman.Web.UI.Resources.PerformanceCounter.js");
  Page.ClientScript.RegisterStartupScript(GetType(), ClientID, 
     String.Format("performanceCounters['{0}'] = new " +
                   "Stratman.Web.UI.PerformanceCounter('{0}', " + 
                   "Stratman.Web.UI.PerformanceCounterDisplayType.{1}, " +
                   "{2}, {3}, {4}, {5}, {6}, {7}, '{8}', " + 
                   "Sys.UI.RepeatDirection.{9}, {10}, '{11}', '{12}', " + 
                   "'{13}', '{14}', '{15}', {16}, {17});\n", ClientID, 
                   displayType.ToString(), Value, refreshInterval, width, 
                   height, ceiling, floor, formatString, orientation, 
                   historyCount, cssClass, categoryName, counterName, 
                   instanceName, machineName, 
                   (onChange == "" ? "null" : onChange), childIDs), true);

  // Only include the vector graphics library if the control is a line graph
  if (displayType == PerformanceCounterDisplayType.LineGraph)
    Page.ClientScript.RegisterClientScriptResource(GetType(), 
       "Stratman.Web.UI.Resources.VectorGraphics.js");
}

如前所述,它通过设置文本显示的值内容或调整进度条或直方图的子元素大小来设置控件的值。它还注册了必要的客户端脚本包含,并发出实例化控件客户端实例的 JavaScript 代码段。最后,Value 属性如下所示:

public float Value
{
  get
  {
    // Make sure that child controls have been created first
    EnsureChildControls();

    string hashKey = categoryName + ":" + counterName + ":" + instanceName + 
                     ":" + machineName;

    // If the performance counter is instantaneous (i.e. requires no 
    // historical data) then just get the current value for it, modify/invert 
    // it if necessary, and return it
    if (IsPerformanceCounterInstantaneous(performanceCounter))
    {
      float calculatedValue = (invert ? 
         ceiling - performanceCounter.NextValue() : 
         performanceCounter.NextValue());
    
      return (modifier != 0 ? calculatedValue * modifier : calculatedValue);
    }

    else
    {
      List<CounterSample> samples = performanceCounterSamples[hashKey];

      // Get the previous sample for this counter
      CounterSample previousSample = 
         samples[performanceCounterSamples[hashKey].Count - 1];

      // If less than 100 milliseconds have passed since the previous sample 
      // was obtained and we have only one historical sample then sleep the 
      // thread; this is to ensure that enough time has passed between 
      // samples for a statistically relevant value to be calculated
      if (performanceCounterSamples[hashKey].Count == 1 && 
          DateTime.Now.ToFileTimeUtc() - previousSample.TimeStamp100nSec < 
          1000000)
        Thread.Sleep(100);

      // If more than 100 milliseconds have passed, then obtain a new sample 
      // and record it in this history data; otherwise we just use the 
      // previous two samples to calculate the value
      if (DateTime.Now.ToFileTimeUtc() - previousSample.TimeStamp100nSec >= 
          1000000)
      {
        if (performanceCounterSamples[hashKey].Count > 1)
          performanceCounterSamples[hashKey].RemoveAt(0);

        samples.Add(performanceCounter.NextSample());
      }

      // Calculate the value, modify/invert it if necessary, and then return 
      // it
      float calculatedValue = CounterSample.Calculate(samples[0], 
                                                      samples[1]);
      calculatedValue = (invert ? ceiling - calculatedValue : 
                                  calculatedValue);

      return (modifier != 0 ? calculatedValue * modifier : calculatedValue);
    }
  }
}

因此,我们看到,如果这是一个即时计数器,我们只需调用 PerformanceCounterGetNextValue() 方法,否则我们必须在两个计数器样本之间进行计算。如果是这种情况,并且我们只有一个历史样本(即,这是该计数器第一次实例化,并且我们尚未将任何内容渲染到屏幕上),那么我们检查自收集第一个样本以来经过了多少时间:如果少于 100 毫秒,则我们将线程休眠那么长时间。之后,我们收集另一个样本,使用这两个样本计算值,根据需要进行修改/反转,然后返回。但是,如果我们的历史样本超过一个,那么我们就检查最后一个样本的时间戳。如果它是在 100 毫秒之前收集的,我们就收集另一个样本,并根据上一个样本和我们刚刚收集的样本计算值。否则,我们就仅使用前两个样本来计算值(而不收集新的样本)。

客户端脚本和回调

驱动控件脚本和客户端更新的 JavaScript 资源文件是 Resources\PerformanceCounter.js。该控件的客户端类是 Stratman.Web.UI.PerformanceCounter(请注意命名空间,这可以通过 ASP.NET AJAX 扩展实现)。它具有您期望的所有常规方法,包括 Render()(在屏幕上渲染计数器数据)和 SetCssClass()(更新正在使用的控件的 CssClass)。Render() 方法的代码如下所示:

Stratman.Web.UI.PerformanceCounter.prototype.Render = function()
{
  // For text displays, simply call String.format()
  if (this.Type == Stratman.Web.UI.PerformanceCounterDisplayType.Text)
    document.getElementById(this.ChildElementIDs[0]).innerHTML = 
       String.format(this.FormatString, this.Value);
    
  // For progress bars, just set the width or height (depending on the 
  // orientation) of the progress bar
  else if (this.Type == 
           Stratman.Web.UI.PerformanceCounterDisplayType.ProgressBar)
  {
    if (this.Orientation == Sys.UI.RepeatDirection.Vertical)
      document.getElementById(this.ChildElementIDs[0]).style.height = 
         Math.round(Math.max(Math.min(
            (this.Value - this.Floor) / this.Ceiling, 1) * this.Height, 1)) + 
         "px";
        
    else
      document.getElementById(this.ChildElementIDs[0]).style.width = 
         Math.round(Math.max(Math.min(
            (this.Value - this.Floor) / this.Ceiling, 1) * this.Width, 1)) + 
         "px";
  }
        
  // For histograms, set the height of each bar to the value of the 
  // corresponding entry in the history data
  else if (this.Type == 
           Stratman.Web.UI.PerformanceCounterDisplayType.Histogram)
  {
    for (var i = 0; i < this.HistoryCount; i++)
      document.getElementById(this.ChildElementIDs[i]).style.height = 
         Math.max(Math.min(
            (this.HistorySamples[i] - this.Floor) / this.Ceiling, 1) * 
             this.Height, 1) + 
         "px";
  }
    
  // For line graphs, call the drawLine() function in the vector graphics 
  // library for each entry in the history data
  else if (this.Type == 
     Stratman.Web.UI.PerformanceCounterDisplayType.LineGraph)
  {
    var sampleWidth = Math.round(this.Width / this.HistoryCount);
    var lineHTML = "";
        
    this.VectorGraphics.setCssClass(this.CssClass);
    this.VectorGraphics.clear();
        
    for (var i = 0; i < this.HistoryCount - 1; i++)
      this.VectorGraphics.drawLine((i * sampleWidth), 
         this.Height - Math.round(Math.min((this.HistorySamples[i] - 
         this.Floor) / this.Ceiling, 1) * (this.Height - 1)) - 1, 
         ((i + 1) * sampleWidth), 
         this.Height - Math.round(Math.min((this.HistorySamples[i + 1] - 
         this.Floor) / this.Ceiling, 1) * (this.Height - 1)) - 1);
            
    this.VectorGraphics.paint();
  }
}

控件的渲染功能与实际 C# 代码中的功能完全相同,但折线图除外。在这里,我们使用 Walter Zorn 出色的 JavaScript 矢量图形库 来负责绘制折线图。在客户端 PerformanceCounter 类的构造函数中,我们创建了一个 jsGraphics 对象,并传入了表示我们在控件层次结构中创建的容器面板的 DOM 对象。要绘制线条,我们只需为历史数据中的每个条目调用 drawLine() 将它们连接起来,然后调用 paint() 来实际将线条渲染到容器控件。

为了更新性能计数器数据,我们使用了批量的 AJAX 调用到服务器。为了实现这一点,在客户端 PerformanceCounter 对象的构造函数结束时,会调用 RegisterForRefresh 方法。此方法如下所示:

function RegisterForRefresh(id, refreshInterval)
{
  // Create the refresh hash entry for this interval if it doesn't already 
  // exist and make a call to setTimeout() to initialize the callback
  if (refreshHash[refreshInterval] == null)
  {
      refreshHash[refreshInterval] = [];
      window.setTimeout("UpdatePerformanceCounters(" + refreshInterval + 
                        ")", refreshInterval * 1000);
  }
    
  Array.add(refreshHash[refreshInterval], id);
}

此函数所做的就是为该刷新间隔创建一个数组条目(如果尚不存在),并调用 setTimeout() 在指定的间隔后启动回调过程。然后,它将此性能计数器控件的客户端 ID 添加到该刷新间隔的数组中。为什么我们这样做?这是为了避免大量不必要的服务器调用:我们将所有应该在给定间隔内刷新的控件的 ID 批量处理,然后一次性调用服务器以获取它们的所有值。这样,我们就可以避免不必要的客户端调用和不必要的服务器负载。总之,UpdatePerformanceCounters() 方法的代码如下所示:

function UpdatePerformanceCounters(refreshInterval)
{
  // Assemble the list of control IDs to update into a comma-delimited 
  // string
  var performanceCounterIDs = refreshHash[refreshInterval][0];

  for (var i = 1; i < refreshHash[refreshInterval].length; i++)
    performanceCounterIDs += "," + refreshHash[refreshInterval][i];

  // Make the callback to the web server and call setTimeout() again to 
  // re-register this callback
  WebForm_DoCallback(refreshHash[refreshInterval][0], performanceCounterIDs, 
                     RenderPerformanceCounters, refreshInterval, null, 
                     false);
  window.setTimeout("UpdatePerformanceCounters(" + refreshInterval + ")", 
                    refreshInterval * 1000);
}

它所做的只是批量处理该间隔的控件 ID,通过 WebForm_DoCallback() 为控件执行回调,并通过另一个 setTimeout() 调用重新注册自身。此时,我们回到了服务器:为了使控件类能够通过 WebForm_DoCallback() 进行回调,它必须实现 ICallbackEventHandler 接口,我们已经这样做了。它要求实现两个方法,第一个是 RaiseCallbackEvent(),它负责进行任何必要的参数解析(在本例中为控件 ID 字符串)。此方法如下所示:

public void RaiseCallbackEvent(string eventArgument)
{
  performanceCounterIDs = eventArgument.Split(',');
}

所以,它只是将控件 ID 字符串拆分为单个 ID。下一个方法是 GetCallbackResult(),它负责实际处理回调。

public string GetCallbackResult()
{
  StringBuilder performanceCounterValues = new StringBuilder();

  // Find each control and get its updated value
  foreach (string performanceCounterID in performanceCounterIDs)
  {
    PerformanceCounterControl performanceCounterControl = 
       (PerformanceCounterControl)FindControlByClientID(
           performanceCounterID, Page);
    
    performanceCounterValues.Append(performanceCounterControl.Value + ",");
  }

  string returnValue = performanceCounterValues.ToString();

  return returnValue.Substring(0, returnValue.Length - 1);
}

这个也很简单:它只需在页面的控件层次结构中搜索每个我们应该更新的控件,获取其最新值,并将其连接到一个不断增长的字符串中。然后,它将该值列表返回到客户端,此时我们的 AJAX 回调的成功事件处理程序 RenderPerformanceCounters() 将被调用。

function RenderPerformanceCounters(response, context)
{
  var performanceCounterValues = response.split(",");
    
  // Loop through each control that we're to update
  for (var i = 0; i < performanceCounterValues.length; i++)
  {
    var performanceCounter = performanceCounters[refreshHash[context][i]];
    performanceCounter.Value = 
       Number.parseInvariant(performanceCounterValues[i]);
        
    // Update the history data for line graphs and histograms
    if (performanceCounter.Type == 
        Stratman.Web.UI.PerformanceCounterDisplayType.LineGraph || 
        performanceCounter.Type == 
        Stratman.Web.UI.PerformanceCounterDisplayType.Histogram)
    {
      var samples = performanceCounter.HistorySamples;

      for (var x = 0; x < performanceCounter.HistoryCount - 1; x++)
        performanceCounter.HistorySamples[x] = samples[x + 1];
        
      samples[performanceCounter.HistoryCount - 1] = 
         performanceCounter.Value;
    }
        
    // Render the control
    performanceCounter.Render();
        
    // Invoke the value changed event handler, if it's set
    if (performanceCounter.OnChange)
      performanceCounter.OnChange(performanceCounter);
  }
}

我们现在进入更新过程的最后阶段。我们将值字符串拆分为各个值,然后更新控件的值,包括直方图和折线图的历史数据。然后,我们为每个控件调用 Render() 方法,如果设置了 OnChange 属性,则调用 OnChange 事件处理程序函数。

用法

为了演示的目的,我在源代码下载中包含了一个名为 PerformanceCountersTest 的测试网站,其屏幕截图可以在本文的顶部看到。实际上没有什么可以深入探讨的:它重新实现了 Windows 任务管理器性能监视器的大部分功能,它基本上由一些布局 CSS 和一堆 PerformanceCounterControl 实例组成。它很好地表明了使用此控件开发丰富的性能监视网页有多么容易。

历史

2007-03-04 - 首次发布。

© . All rights reserved.