ストア アプリにおける CPU 使用率の分析
アプリのパフォーマンスの問題を調査する必要がある場合、まず CPU の使用方法の理解から始めることができます。Visual Studio パフォーマンスと診断ハブの CPU 使用率ツールを使うと、CPU が C++、C#/VB、および JavaScript のコードを実行するためにどこで時間を使用しているかがわかります。特定のシナリオに限定する場合、単一の診断セッションで、CPU 使用率を XAML UI の応答性ツール、エネルギー消費量ツール、またはその両方と一緒に実行できます。Visual Studio 2013 では、CPU サンプリング ツールの代わりに CPU 使用率ツールが使用されます。
注意
CPU 使用率ツールは、Windows Phone Silverlight 8.1 アプリには使用できません。
このチュートリアルでは、簡単なアプリの CPU 使用率の収集と分析について説明します。
既定のパフォーマンスと診断ハブの手順を使ってデータを収集しますが、パフォーマンスと診断ハブでは診断セッションの実行と管理のために他の多くのオプションを提供しています。たとえば、CPU 使用率ツールを Windows Phone または Windows ストア アプリで実行できます。また、診断セッションを Visual Studio のコンピューター、Windows Phone や Windows ストア デバイス、または Visual Studio エミュレーターやシミュレーターの 1 つで実行することもできます。
その後、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 使用率] をクリックしてから、[開始] をクリックします。
アプリが開始すると、[最大数を取得] をクリックします。出力が表示されてから約 1 秒たった後、[非同期で最大数を取得] をクリックします。ボタン クリックごとに合間を開けることで、診断レポートの中でボタン クリックのルーチンを分離しやすくなります。
2 番目の出力行が表示された後、パフォーマンスと診断ハブで [コレクションの停止] をクリックします。
CPU 使用率ツールがデータを分析してレポートを表示します。
CPU 使用率レポートの分析
CPU 利用タイムラインのグラフ**|タイムラインのセグメントを選択して詳細を表示する|CPU 使用率のコール ツリー|コール ツリーの構造|外部コード|コール ツリーのデータ列|**CPU 使用率コール ツリーの非同期機能
CPU 使用状況タイムライン グラフ
CPU 使用状況グラフは、デバイスにあるすべてのプロセッサー コアのすべての CPU 時間に対するアプリの CPU アクティビティの割合を示します。このレポートのデータはデュアル コアのコンピューターで収集されました。2 つの急激な増加は、2 つのボタン クリックの CPU アクティビティを表します。GetMaxNumberButton_Click は、シングル コアで同期的に実行されます。こうすることで、メソッドのグラフの高さが 50% を超えないことが意味をなします。GetMaxNumberAsycButton_Click は、両方のコアで非同期的に実行されます。そのため、そのスパイクが両方のコアですべての CPU リソースを利用する状態に近づくことが正しく観察されます。
詳細を確認するためにタイムライン セグメントを選択する
[診断セッション] タイムラインの選択バーを使用して、GetMaxNumberButton_Click のデータに注目します。
[診断セッション] タイムラインでは、選択したセグメントで使用した時間 (このレポートでは 2 秒余り) が表示され、選択で実行されたメソッドを対象にコール ツリーをフィルターします。
次に GetMaxNumberAsyncButton_Click セグメントを選択します。
このメソッドは GetMaxNumberButton_Click よりも 1 秒早く完了しますが、コール ツリー エントリの意味はそれほど明確ではありません。
CPU 使用率コール ツリー
コール ツリーの情報を理解するには、GetMaxNumberButton_Click セグメントを再度選択して、コール ツリーの詳細を見ます。
コール ツリーの構造
CPU 使用率コール ツリーのトップ レベルのノードは擬似ノードです。 |
|
ほとんどのアプリでは、[外部コードの表示] オプションをオフにすると、セカンド レベルのノードは [外部コード] ノードとなります。このノードに含まれるシステムおよびフレームワーク コードは、アプリの開始と停止、UI の描画、スレッド スケジュールの制御、およびアプリへの他の低レベル サービスの提供を行います。 |
|
セカンド レベル ノードの子はユーザー コード メソッドおよび非同期ルーチンで、セカンド レベル システムとフレームワーク コードによって呼び出される、または作成されます。 |
|
メソッドの子ノードには、親メソッドの呼び出しのみのデータが含まれます。[外部コードの表示] がオフのとき、アプリ メソッドには [外部コード] ノードが含まれる場合もあります。 |
外部コード
外部コードとは、書き記したコードによって実行されるシステムおよびフレームワーク コンポーネント内の関数です。外部コードには、アプリの開始と停止、UI の描画、スレッドの制御、およびアプリへの他の低レベル サービスの提供を行う関数が含まれます。外部コードを確認することはほとんどないため、CPU 使用率コール ツリーはユーザー メソッドの外部関数を 1 つの [外部コード] ノードにまとめます。
外部コードのコール パスを表示する場合、[フィルター ビュー] リストから [外部コードの表示] をクリックし、[適用] をクリックします。
多くの外部コードの呼び出しチェーンは複雑な入れ子になっているため、関数名列の幅は、一部の大型コンピューター モニターを除いてディスプレイの幅に収まりきらない可能性があります。その場合、関数名は […] と表示されます。
検索ボックスを使って目的のノードを探した後、水平スクロール バーを使ってデータを表示させます。
コール ツリー データの列
合計 CPU (%) |
選択した時間の範囲内で、関数への呼び出しおよび関数が呼び出した関数によって使用されたアプリの CPU アクティビティの割合です。これは、特定の時間の範囲で、アプリの合計アクティビティと利用可能な合計 CPU 容量を比較する CPU 使用率のタイムライン グラフとは異なります。 |
セルフ CPU (%) |
選択した時間の範囲内で、関数への呼び出しによって使用されたアプリの CPU アクティビティの割合で、関数が呼び出した関数によるアクティビティは除外されます。 |
合計 CPU (ms) |
選択した時間の範囲内で、関数への呼び出しおよび関数が呼び出した関数によって使用されたミリ秒です。 |
セルフ CPU (ms) |
選択した時間の範囲内で、関数への呼び出しおよび関数が呼び出した関数によって使用されたミリ秒です。 |
モジュール |
関数が含まれるモジュールの名前、または [外部コード] ノード内の関数が含まれるモジュールの数です。 |
CPU 使用率コール ツリー内の非同期関数
コンパイラが非同期メソッドを検出すると、メソッドの実行を制御するために非表示のクラスを作成します。概念的には、クラスとはステート マシンで、元のメソッドの操作を非同期で呼び出すコンパイラ生成関数のリスト、および正しく機能するために必要なコールバック、スケジューラ、および反復子が含まれます。元のメソッドが親メソッドによって呼び出されると、ランタイムは親の実行コンテキストからメソッドを削除し、アプリの実行を制御するシステムとフレームワーク コードのコンテキストにある非表示のクラスのメソッドを実行します。非同期のメソッドは、毎回ではありませんが多くの場合、1 つ以上の異なるスレッドで実行されます。このコードは、CPU 使用率コール ツリーで、ツリーのトップ ノードのすぐ下にある [外部コード] ノードの子として表示されます。
これをこの例で表示するには、タイムラインで GetMaxNumberAsyncButton_Click セグメントを再度選択します。
[外部コード] の下にある最初の 2 つのノードは、ステート マシン クラスのコンパイラ生成メソッドです。3 つ目は、元のメソッドへの呼び出しです。生成されたメソッドを展開すると詳細が表示されます。
MainPage::GetMaxNumberAsyncButton_Click の機能は少なく、タスクの値のリストを管理し、結果の最大値を計算し、出力を表示します。
MainPage+<GetMaxNumberAsyncButton_Click>d__3::MoveNext は、呼び出しを GetNumberAsync にラップする 48 個のタスクをスケジュールして起動するために必要なアクティビティを表示します。
MainPage::<GetNumberAsync>b__b は、GetNumber を呼び出すタスクのアクティビティを表示します。
次の手順
CpuUseDemo アプリには目立った機能がないかもしれませんが、非同期操作やパフォーマンスと診断ハブの他のツールで試行錯誤することによって、ユーティリティを拡張することができます。
MainPage::<GetNumberAsync>b__b は、GetNumber メソッドの実行よりも、[外部コード] でより多くの時間を費やします。この時間の大半は、非同期操作のオーバーヘッドです。GetNumber で、タスクの数を増やし (MainPage.xaml.cs の NUM_TASKS 定数で設定)、イテレーションの数を減らしてみます (MIN_ITERATIONS の値を変更)。コレクション シナリオを実行し、MainPage::<GetNumberAsync>b__b の CPU アクティビティを元の CPU 使用率の診断セッションのものと比較します。タスクを減らし、イテレーションを増やしてみます。
ユーザーは多くの場合、アプリの実際のパフォーマンスは気にしていませんが、アプリの見かけ上のパフォーマンスや応答性は気にします。XAML UI 応答性ツールは、見かけ上の応答性に影響するアクティビティの詳細を UI スレッドに表示します。
診断とパフォーマンス ハブで新しいセッションを作成し、XAML UI 応答性ツールと CPU 使用率ツールの両方を追加します。コレクション シナリオを実行します。ここまで読み進めることができた場合、レポートにはすでにわかっていること以外の情報はないでしょう。しかし、2 つのメソッドの 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);
}
}
}