۱۳۹۰/۰۴/۳۱

بررسي علت CPU Usage بالاي برنامه در حال اجرا


فرض كنيد به يك سرور مراجعه كرده‌ايد و شكايت از CPU Usage مربوط به پروسه w3wp.exe يا همان IIS Worker Process است كه بالاي 90 درصد مي‌باشد. بر روي اين سرور هم هيچ چيز ديگري نصب نيست و مطابق مقررات موجود، قرار هم نيست كه برنامه‌اي نصب شود. اكنون سؤال اين است كه چطور تشخيص مي‌دهيد، كدام قسمت يكي از برنامه‌ها‌ي دات نتي در حال اجرا (در اينجا يكي از برنامه‌هاي ASP.NET هاست شده)، سبب بروز اين مشكل شده است؟ كدام ترد بيشترين زمان CPU را به خود اختصاص داده است؟ چطور بايد خطايابي كرد؟
اولين كاري كه در اين موارد توصيه مي‌شود مراجعه به برنامه‌ي معروف process explorer و بررسي برگه‌ي threads آن است. در اينجا حتي مي‌توان call stacks مرتبط با يك ترد را هم مشاهده كرد. اما ... اين برگه در مورد پروسه‌ها و تردهاي دات نتي، اطلاعات چنداني را در اختيار ما قرار نمي‌دهد.
خوشبختانه امكان ديباگ پروسه‌هاي دات نتي در حال اجرا توسط كتابخانه‌ي MdbgCore.dll پيش بيني شده است. اين فايل را در يكي از مسير‌هاي ذيل مي‌توانيد پيدا كنيد:
C:\Program Files\Microsoft SDKs\Windows\vXYZ\bin\MdbgCore.dll
C:\Program Files\Microsoft SDKs\Windows\vXYZ\bin\NETFX 4.0 Tools\MdbgCore.dll

در ادامه مي‌خواهيم توسط امكانات اين كتابخانه، به stack trace تردهاي در حال اجراي يك برنامه دات نتي دسترسي پيدا كرده و سپس نام متدهاي مرتبط را نمايش دهيم:
using System;
using System.Collections;
using System.Diagnostics;
using Microsoft.Samples.Debugging.MdbgEngine;

namespace CpuAnalyzer
{
class Program
{
static void Main(string[] args)
{
var engine = new MDbgEngine();

var processesByName = Process.GetProcessesByName("MyApp");
if (processesByName.Length == 0)
throw new InvalidOperationException("specified process not found.");
var processId = processesByName[0].Id;

var process = engine.Attach(processId);
process.Go().WaitOne();

foreach (MDbgThread thread in (IEnumerable)process.Threads)
{
foreach (MDbgFrame frame in thread.Frames)
{
if (frame == null || frame.Function == null) continue;
Console.WriteLine(frame.Function.FullName);
}
}

process.Detach().WaitOne();
}
}
}
در اينجا در ابتدا نياز است تا pid يا process-id مرتبط با برنامه در حال اجرا يافت شود. سپس از اين pid جهت اتصال (engine.Attach) به پروسه مورد نظر استفاده خواهيم كرد. در ادامه كليه تردهاي اين پروسه در حال ديباگ ليست شده و سپس MDbgFrameهاي اين ترد بررسي مي‌شوند و نام متدهاي مرتبط در كنسول نمايش داده خواهند شد.
خوب در مرحله بعد شايد بد نباشد كه اين متدها را بر اساس CPU usage آن‌ها مرتب كنيم. به اين ترتيب بهتر مي‌توان تشخيص داد كه كدام متد مشكل ساز بوده است. براي اين منظور بايد به API ويندوز و تابع GetThreadTimes مراجعه كرد و اولين پارامتر ورودي آن، همان thread.CorThread.Handle اولين حلقه مثال فوق مي‌باشد. هر كدام كه جمع KernelTime + UserTime بيشتري داشت، همان است كه مشكل درست كرده است.
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool GetThreadTimes(IntPtr handle, out long creation, out long exit, out long kernel, out long user);
اين مورد را به عنوان تمرين بررسي كرده و ادامه دهيد! همچنين بهتر است جهت دستيابي به اطلاعاتي معتبر، اولين حلقه برنامه فوق، حداقل 10 بار اجرا شود تا اطلاعات آماري بهتري را بتوان ارائه داد. البته در اين حالت نكته‌ي زير بايد رعايت شود:
for (int i = 0; i < 10; i++)
{
foreach (MDbgThread thread in (IEnumerable)process.Threads)
{
//...
}
process.Go();
Thread.Sleep(1000);
process.AsyncStop().WaitOne();
}

در كل اين مثال جاي كار زياد دارد. براي مثال طراحي يك رابط كاربري براي آن و نمايش جزئيات بيشتر. به همين منظور حداقل سه پروژه مشابه را مي‌توان نام برد: