分析应用商店应用中 CPU 的使用量
在需要调查应用中的性能问题时,最好从了解它是如何使用 CPU 开始。通过 Visual Studio 性能和诊断中心中的 CPU 使用量工具,可显示 CPU 花费时间执行 C++、C#/VB 和 JavaScript 代码的位置。若要专注于特定的方案,可使用 XAML UI 响应能力工具、能量消耗工具或者在单个诊断会话中同时使用这两种工具来运行 CPU 使用量工具。在 Visual Studio 2013 中,CPU 使用量工具将替代 CPU 采样工具。
备注
CPU 使用量工具不能与 Windows Phone Silverlight 8.1 应用一起使用。
本演练指导你收集和分析简单应用的 CPU 使用量。
将使用默认的性能和诊断中心过程来收集数据,但性能和诊断中心也会提供许多其他选项来运行和管理诊断会话。例如,你可以在 Windows Phone 应用或 Windows 应用商店应用上运行 CPU 使用量工具;在 Visual Studio 计算机、Windows Phone 或 Windows 应用商店设备上或者在某个 Visual Studio 仿真程序或模拟器中运行诊断会话。
随后,你将深入了解 CPU 使用量诊断报告。
内容
创建 CpuUseDemo 项目
什么是 CpuUseDemo?
收集 CPU 使用量数据
分析 CPU 使用量报告
后续步骤
MainPage.xaml
MainPage.xaml.cs
创建 CpuUseDemo 项目
使用 BlankApp 模板来创建名为 CpuUseDemo 的新 C# Windows 应用商店应用项目。
用此代码替换 MainPage.xaml。
用此代码替换 MainPage.xaml.cs。
什么是 CpuUseDemo?
CpuUseDemo 是用于演示如何收集和分析 CPU 使用量数据而创建的应用。通过调用方法(该方法从对函数的多次调用中选择最大值),按钮将生成一个数字。被调用的函数将创建大量的随机值,然后将返回最后一个随机值。数据将显示在文本框中。构建该应用并对其进行测试。
该应用和 CpuUseDemo 的方法并非很有趣,但可以很轻松地向你介绍一些有关 CPU 使用量数据分析的常见情况。
收集 CPU 使用量数据
在 Visual Studio 中,将部署目标设置为**“模拟器”并将解决方案配置设置为“零售”**。
通过在模拟器中运行该应用,你可以在该应用和 Visual Studio IDE 之间轻松切换。
通过在**“释放”**模式下运行该应用,可以为你提供应用实际性能的更好的视图。
在**“调试”菜单上,选择“性能和诊断”**。
在性能和诊断中心中,选择**“CPU 使用量”,然后选择“启动”**。
在应用启动后,单击**“获取最大值”。在显示输出后大约等待一秒钟,然后选择“获取最大异步数”**。借助按钮点击之间的等待,可以更轻松地在诊断报告中隔离按钮单击例程。
在第二个输出行显示之后,在性能和诊断中心中选择**“停止收集”**。
CPU 使用量工具可分析数据并显示报告。
分析 CPU 使用量报告
CPU 使用量时间线关系图**|选择时间线段以查看详细信息|CPU 使用量调用关系树|调用关系树结构|外部代码|调用关系树数据列|**CPU 使用量调用关系树中的异步函数
CPU 使用量时间线关系图
CPU 使用量关系图借助设备上的所有处理器内核,以所有 CPU 时间的百分比形式显示应用的 CPU 活动。在双核计算机上收集此报告的数据。两个较大的峰值表示两次按钮单击事件的 CPU 活动。GetMaxNumberButton_Click 将在单核上同步执行,因此方法的关系图高度永远不超过 50% 是合理的。GetMaxNumberAsycButton_Click 跨两个核异步运行,因此其峰值更接近于同时在这两个核上使用的所有 CPU 资源量,这看起来也是合理的。
选择时间线段以查看详细信息。
使用**“诊断会话”**时间线上的选择栏以专注于 GetMaxNumberButton_Click 数据:
**“诊断会话”**时间线现在将显示在选定的时间线段(根据该报告,应稍大于 2 秒)中花费的时间,并且筛选在选定时间段中运行的这些方法的调用关系树。
现在选择 GetMaxNumberAsyncButton_Click 段。
完成此方法的速度比完成 GetMaxNumberButton_Click 的速度快了一秒钟,然而调用关系树项的含义却不太明显。
CPU 使用量调用关系树
若要开始了解调用关系树的信息,请重新选择 GetMaxNumberButton_Click 段并查看调用关系树的详细信息。
调用关系树结构
CPU 使用量调用关系树中的顶级节点是一个伪节点 |
|
在大多数应用中,当禁用“显示外部代码”选项时,二级节点是[外部代码]节点,该节点包含系统和框架代码,它可以启动和停止应用、绘制 UI、控制线程计划以及向应用提供其他低级服务。 |
|
二级节点的子级为用户代码方法和异步例程,它们由二级系统和框架代码进行调用或创建。 |
|
方法的子节点仅包含用于父方法调用的数据。在禁用“显示外部代码”时,应用方法还包含[外部代码]节点。 |
外部代码
外部代码是由编写的代码执行的系统和框架组件中的函数。外部代码包含一些函数,它们可启动和停止应用、绘制 UI、控制线程并向应用提供低级服务。在大多数情况下,你不会对外部代码产生兴趣,因此 CPU 使用量调用关系树将用户方法的外部功能收集到某个**[外部代码]**节点中。
若要查看外部代码的调用路径,请从**“筛选器视图”列表中选择“显示外部代码”,然后选择“应用”**。
请注意,许多外部代码调用链已深度嵌套,因此函数名列的宽度可能超过所有计算机监视器(最大的计算机监视器除外)的显示宽度。发生这种情况时,函数名将显示为**[…]**:
使用搜索框以找到你正在查找的节点,然后使用水平滚动条以使数据在视图中显示:
调用关系树数据列
总 CPU (%) |
在选定时间范围内应用的 CPU 活动的百分比,它由调用的函数以及该函数调用的函数使用。请注意,这与“CPU 使用量”时间线关系图不同,因为它已将某个时间范围内的应用总活动与总可用的 CPU 容量进行比较。 |
自测 CPU (%) |
在选定时间范围内应用的 CPU 活动的百分比,它由调用的函数(排除由该函数调用的函数活动)使用。 |
总 CPU(毫秒) |
在选定时间范围内调用函数以及由该函数调用的函数所需的毫秒数。 |
自测 CPU(毫秒) |
在选定时间范围内调用函数以及由该函数调用的函数所需的毫秒数。 |
模块 |
包含函数的模块名或包含[外部代码]节点中的函数的模块数量。 |
CPU 使用量调用关系树中的异步函数
当编译器遇到异步方法时,它将创建一个隐藏类以控制该方法的执行。从概念上来说,该类为包含编译器生成的函数列表的状态机,其中这些函数可异步调用原始方法的操作并且可正确用于回调、计划程序和迭代器。当由父方法调用原始方法时,运行时将从父方法的执行上下文中移除该原始方法,并且将在控制系统和框架代码的上下文中运行隐藏类的方法,以控制应用的执行。通常在一个或多个不同的线程上执行异步方法,但并非总是如此。此代码作为**[外部代码]**节点(该节点紧接在 CPU 使用量调用关系树的顶级节点的下方)的子级显示在该关系树中。
若要在我们的示例中查看该示例,请在时间线中重新选择 GetMaxNumberAsyncButton_Click 段。
**[外部代码]**下方的前两个节点是状态机类的编译器生成的方法。第三个节点是原始方法的调用。通过展开生成的方法,可显示当前进行的操作。
MainPage::GetMaxNumberAsyncButton_Click 执行非常少的操作;它管理任务值列表、计算结果的最大值以及显示输出。
MainPage+<GetMaxNumberAsyncButton_Click>d__3::MoveNext 显示计划和启动 48 个任务(包含对 GetNumberAsync 的调用)所需的活动。
MainPage::<GetNumberAsync>b__b 显示有关调用 GetNumber 的任务的活动。
后续步骤
CpuUseDemo 应用并非最出色的应用,但你可以在性能和诊断中心中,使用异步操作和其他工具对其进行实验,以扩展它的实用功能。
请注意,MainPage::<GetNumberAsync>b__b 在[外部代码]中花费的时间多于其执行 GetNumber 方法所需的时间。异步操作占用了其中大部分时间。尝试增加任务(在 MainPage.xaml.cs 的 NUM_TASKS 常量中设置的任务)的数量,并尝试减少 GetNumber 中的迭代数(更改 MIN_ITERATIONS 值)。运行收集方案,并将 MainPage::<GetNumberAsync>b__b 的 CPU 活动与原始 CPU 使用量诊断会话中的 CPU 活动进行比较。尝试减少任务并增加迭代。
用户通常不会关心应用的实际性能;他们在意的是应用的感知性能和响应能力。XAML UI 响应能力工具显示影响感知响应能力的 UI 线程上的活动详细信息。
在诊断和性能中心中创建新会话,并同时添加 XAML UI 响应能力工具和 CPU 使用量工具。运行收集方案。如果你已阅读了前面的内容,该报告可能并未显示有关你尚未理解的所有内容,然而前两个方法的**“UI 线程使用率”**时间线图中的差异却令人印象深刻。在复杂且真实的应用中,工具的组合使用将很有帮助。
MainPage.xaml
<Page
x:Class="CpuUseDemo.MainPage"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:CpuUseDemo"
xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">
<Page.Resources>
<Style TargetType="TextBox">
<Setter Property="FontFamily" Value="Lucida Console" />
</Style>
</Page.Resources>
<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,40,0,0">
<Button Name="GetMaxNumberButton" Click="GetMaxNumberButton_Click" Content="Get Max Number" />
<Button Name="GetMaxNumberAsyncButton" Click="GetMaxNumberAsyncButton_Click" Content="Get Max Number Async" />
</StackPanel>
<StackPanel Grid.Row="1">
<TextBox Name="TextBox1" AcceptsReturn="True" />
</StackPanel>
</Grid>
</Page>
MainPage.xaml.cs
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
using Windows.Foundation.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Concurrent;
// The Blank Page item template is documented at https://go.microsoft.com/fwlink/?LinkId=234238
namespace CpuUseDemo
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
const int NUM_TASKS = 48;
const int MIN_ITERATIONS = int.MaxValue / 1000;
const int MAX_ITERATIONS = MIN_ITERATIONS + 10000;
long m_totalIterations = 0;
readonly object m_totalItersLock = new object();
private void GetMaxNumberButton_Click(object sender, RoutedEventArgs e)
{
GetMaxNumberAsyncButton.IsEnabled = false;
lock (m_totalItersLock)
{
m_totalIterations = 0;
}
List<int> tasks = new List<int>();
for (var i = 0; i < NUM_TASKS; i++)
{
var result = 0;
result = GetNumber();
tasks.Add(result);
}
var max = tasks.Max();
var s = GetOutputString("GetMaxNumberButton_Click", NUM_TASKS, max, m_totalIterations);
TextBox1.Text += s;
GetMaxNumberAsyncButton.IsEnabled = true;
}
private async void GetMaxNumberAsyncButton_Click(object sender, RoutedEventArgs e)
{
GetMaxNumberButton.IsEnabled = false;
GetMaxNumberAsyncButton.IsEnabled = false;
lock (m_totalItersLock)
{
m_totalIterations = 0;
}
var tasks = new ConcurrentBag<Task<int>>();
for (var i = 0; i < NUM_TASKS; i++)
{
tasks.Add(GetNumberAsync());
}
await Task.WhenAll(tasks.ToArray());
var max = 0;
foreach (var task in tasks)
{
max = Math.Max(max, task.Result);
}
var func = "GetMaxNumberAsyncButton_Click";
var outputText = GetOutputString(func, NUM_TASKS, max, m_totalIterations);
TextBox1.Text += outputText;
this.GetMaxNumberButton.IsEnabled = true;
GetMaxNumberAsyncButton.IsEnabled = true;
}
private int GetNumber()
{
var rand = new Random();
var iters = rand.Next(MIN_ITERATIONS, MAX_ITERATIONS);
var result = 0;
lock (m_totalItersLock)
{
m_totalIterations += iters;
}
// we're just spinning here
// and using Random to frustrate compiler optimizations
for (var i = 0; i < iters; i++)
{
result = rand.Next();
}
return result;
}
private Task<int> GetNumberAsync()
{
return Task<int>.Run(() =>
{
return GetNumber();
});
}
string GetOutputString(string func, int cycles, int max, long totalIters)
{
var fmt = "{0,-35}Tasks:{1,3} Maximum:{2, 12} Iterations:{3,12}\n";
return String.Format(fmt, func, cycles, max, totalIters);
}
}
}