出售域名 11365.com.cn
有需要请联系 16826375@qq.com
在手机上浏览
在手机上浏览

.net性能跟踪

发布日期:2022-11-25

网上有很多开源的性能分析组件,比如MiniProfiler,都很优秀,在这里我们手工实现基本的性能跟踪。
asp.net mvc或者.net core mvc都有自己的生命周期,这里以asp.net mvc为例!

 

一、创建日志类
这个类需要穿插到各个模块中,所以尽量放在一个独立的组件。附代码:

namespace Singoo.Common
{
    public static class PerfAnalyze
    {
        static PerfAnalyze()
        {
            HandleCollection = new List<PerfInfoModel>();
        }

        public static List<PerfInfoModel> HandleCollection { get; set; }

        public static void Input(string item)
        {
            Input(new PerfInfoModel() { EventItem = item, CurrDatetime = DateTime.Now });
        }

        const string firstEventItem = "Application_BeginRequest";
        const string lastEventItem = "Application_EndRequest";

        private static void Input(PerfInfoModel model)
        {
            if (!System.Configuration.ConfigurationManager.AppSettings.AllKeys.Contains("PerfAnalyzeLog") || System.Configuration.ConfigurationManager.AppSettings["PerfAnalyzeLog"].ToString() != "true")
                return;

            HandleCollection.Add(model);
        }

        public static void OutputLog()
        {
            Func<string, string> funcLenPadd = (x) =>
            {
                return x.Length < 50
                ? x.PadRight(50 - x.Length, ' ')
                : x;
            };

            if (HandleCollection == null || HandleCollection.Count == 0) return;

            var builder = new StringBuilder();
            builder.Append("------------------start-----------------------\r\n");

            DateTime firstDatetime = HandleCollection.Min(p => p.CurrDatetime);
            DateTime lastDatetime = new DateTime(1900, 1, 1);
            double costTime = 0; //花费时间(毫秒)
            foreach (var item in HandleCollection.OrderBy(p => p.CurrDatetime))
            {
                lastDatetime = item.CurrDatetime;
                costTime = (lastDatetime - firstDatetime).TotalMilliseconds;

                builder.AppendFormat("事件: {0}    日志时间: {1}     消耗时间(毫秒): {2} \r\n", funcLenPadd(item.EventItem), item.CurrDatetime.ToString("yyyy-MM-dd HH:mm:ss ffff"), costTime);
            }

            builder.Append("-------------------end------------------------\r\n");
            FileLog.Log(builder.ToString(), ""); //生成的日志文件在 \LogFile\年月日
        }

        public static string OutputConsole()
        {
            if (HandleCollection == null || HandleCollection.Count == 0) return "";

            var arrId = "arrPerfAnalyze_"   DateTime.Now.ToString("ffff");
            var builder = new StringBuilder("<script>");
            builder.Append($"let {arrId}=[];");

            DateTime firstDatetime = HandleCollection.Min(p => p.CurrDatetime);
            DateTime lastDatetime = new DateTime(1900, 1, 1);
            double costTime = 0; //花费时间(毫秒)
            foreach (var item in HandleCollection.OrderBy(p => p.CurrDatetime))
            {
                lastDatetime = item.CurrDatetime;
                costTime = (lastDatetime - firstDatetime).TotalMilliseconds;

                builder.AppendFormat("{3}.push({{\"事件\":\"{0}\",\"日志时间\":\"{1}\",\"消耗时间(毫秒)\":{2}}});", item.EventItem, item.CurrDatetime.ToString("yyyy-MM-dd HH:mm:ss ffff"), costTime, arrId);
            }

            //剩下的EndRequest其实差别不大,加上
            builder.AppendFormat("{3}.push({{\"事件\":\"{0}\",\"日志时间\":\"{1}\",\"消耗时间(毫秒)\":{2}}});", "Application_EndRequest(请求结束)", lastDatetime.ToString("yyyy-MM-dd HH:mm:ss ffff"), costTime, arrId);

            builder.Append("console.warn(`在web.config-appsetting配置<add key=\"PerfAnalyzeLog\" value=\"true/false\" />进行开启和关闭`);");
            builder.Append($"console.table({arrId});");
            builder.Append("</script>");

            return builder.ToString();
        }
    }

    public class PerfInfoModel
    {
        /// <summary>
        /// 事件
        /// </summary>
        public string EventItem { get; set; }
        /// <summary>
        /// 当前时间
        /// </summary>
        public DateTime CurrDatetime { get; set; }
        /// <summary>
        /// 花费时间(毫秒)
        /// </summary>
        public double CostTime { get; set; } = 0;
    }
}

记录日志时,只记录事件和时间,在输出的时候计算耗时。


二、在各事件中记录日志
1)在生命周期事件中记录日志

#region 一堆的事件   

protected void Application_Start()
{
    PerfAnalyze.Input("Application_Start(系统初始化)");
}

protected void Application_BeginRequest()
{
    PerfAnalyze.Input("Application_BeginRequest(请求开始)");
}

protected void Application_PreRequestHandlerExecute()
{
    PerfAnalyze.Input("Application_PreRequestHandlerExecute");
}

protected void Application_PostRequestHandlerExecute()
{
    PerfAnalyze.Input("Application_PostRequestHandlerExecute");
}

protected void Application_PostAuthorizeRequest()
{
    PerfAnalyze.Input("Application_PostAuthorizeRequest");
}

protected void Application_ResolveRequestCache()
{
    PerfAnalyze.Input("Application_ResolveRequestCache");
}

protected void Application_PostAcquireRequestState()
{
    PerfAnalyze.Input("Application_PostAcquireRequestState");
}

protected void Application_PostResolveRequestCache()
{
    PerfAnalyze.Input("Application_PostResolveRequestCache");
}

protected void Application_PostMapRequestHandler()
{
    PerfAnalyze.Input("Application_PostMapRequestHandler");
}
protected void Application_AcquireRequestState()
{
    PerfAnalyze.Input("Application_AcquireRequestState");
}

protected void Application_UpdateRequestCache()
{
    PerfAnalyze.Input("Application_UpdateRequestCache");
}

protected void Application_ReleaseRequestState()
{
    PerfAnalyze.Input("Application_ReleaseRequestState");
}

protected void Application_AuthorizeRequest()
{
    PerfAnalyze.Input("Application_AuthorizeRequest");
}

protected void Application_EndRequest()
{
    PerfAnalyze.Input("Application_EndRequest(请求结束)");
    PerfAnalyze.OutputLog(); //输出到txt日志
    PerfAnalyze.HandleCollection.Clear();
}

protected void Application_End()
{
    PerfAnalyze.Input("Application_End(停止系统)");
}

#endregion

2)在横切点,全局筛选器中写入日志

创建一个筛选器,记录Action的执行和合并View的结果输出所消耗的时间。

public class GlobalFilterAttribute : ActionFilterAttribute
{
    private HtmlTextWriter tw;
    private StringWriter sw;
    private StringBuilder sb;
    private HttpWriter output;

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        sb = new StringBuilder();
        sw = new StringWriter(sb);
        tw = new HtmlTextWriter(sw);
        output = (HttpWriter)context.RequestContext.HttpContext.Response.Output;
        context.RequestContext.HttpContext.Response.Output = tw;//注意,加上这个语句,后面才能取到Result数据          

        string ControllerName = context.RouteData != null && context.RouteData.Values.Keys.Contains("controller")
            ? context.RouteData.Values["controller"].ToString()
            : "Home";
        string ActionName = context.RouteData != null && context.RouteData.Values.Keys.Contains("action")
            ? context.RouteData.Values["action"].ToString()
            : "Index";

        PerfAnalyze.Input($"{ControllerName}-{ActionName}-GlobalFilter-OnActionExecuting(开始执行方法)");            
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        base.OnActionExecuted(context);

        string ControllerName = context.RouteData != null && context.RouteData.Values.Keys.Contains("controller")
            ? context.RouteData.Values["controller"].ToString()
            : "Home";
        string ActionName = context.RouteData != null && context.RouteData.Values.Keys.Contains("action")
            ? context.RouteData.Values["action"].ToString()
            : "Index";

        PerfAnalyze.Input($"{ControllerName}-{ActionName}-GlobalFilter-OnActionExecuted(方法执行完毕)");
    }

    public override void OnResultExecuting(ResultExecutingContext context)
    {
        string ControllerName = context.RouteData != null && context.RouteData.Values.Keys.Contains("controller")
            ? context.RouteData.Values["controller"].ToString()
            : "Home";
        string ActionName = context.RouteData != null && context.RouteData.Values.Keys.Contains("action")
            ? context.RouteData.Values["action"].ToString()
            : "Index";

        PerfAnalyze.Input($"{ControllerName}-{ActionName}-GlobalFilter-OnResultExecuting(开始整合View)");
        base.OnResultExecuting(context);
    }

    public override void OnResultExecuted(ResultExecutedContext context)
    {
        string response = sb.ToString();

        string ControllerName = context.RouteData != null && context.RouteData.Values.Keys.Contains("controller")
            ? context.RouteData.Values["controller"].ToString()
            : "Home";
        string ActionName = context.RouteData != null && context.RouteData.Values.Keys.Contains("action")
            ? context.RouteData.Values["action"].ToString()
            : "Index";

        var actionResult = context.Result;
        if (actionResult is ViewResult)
        {
            PerfAnalyze.Input($"{ControllerName}-{ActionName}-GlobalFilter-OnResultExecuted(整合View完毕并输出)");

            string js = PerfAnalyze.OutputConsole();
            if (js != null && js.Trim().Length > 0)
                response  = js;
        }

        output.Write(response);
        base.OnResultExecuted(context);
    }
}

可以看到,我们使用了一个js,拼成json数组,最后用console.table输出日志,这部分最后只到action完成并输出结果,后续的操作基本无消耗,可以等同完成了。若要查看全部完整的日志,可以查看txt文本日志

 

三、查看日志
1)通过谷歌浏览器DevTool查看Console日志

可以看到,日志从开始请求到结束请求一系列的操作,并消耗的时间(毫秒)。

怎么在我们的程序中写入日志?很简单,在我们的程序中写入:

public virtual ActionResult Index(string dsfm_code)
{
    PerfAnalyze.Input("SmartCodeSingleController-Index-Start(开始执行低代码首页方法)");
    //...
}

2)查看文本日志