【问题标题】:Getting MVC mini-profiler timings into async tasks将 MVC 迷你分析器计时纳入异步任务
【发布时间】:2011-12-01 12:18:07
【问题描述】:

我在使用异步任务加速的页面中有一个长时间运行的 SQL 查询:

using System.Threading.Tasks;
...

var asyncTask = new Task<ResultClass>(
    () =>
    {
        using (var stepAsync = MiniProfiler.Current.Step("Async!"))
        {
            // exec long running SQL
        }
    });

asyncTask.Start();

// do lots of other slow stuff

ResultClass result;
using (var stepWait = MiniProfiler.Current.Step("Wait for Async"))
{
    result = asyncTask.Result;
}

(请注意,一旦 C# 5 出现 asyncawait,此语法会更好)

使用 MVC 迷你分析器时,我得到了“等待异步”的时间,但我无法获取“异步!”的时间一步。

有没有办法将这些结果(可能只是 SQL 计时)放入已完成页面的跟踪中?

更新

我找到了一种将分析器步骤放入异步方法的方法:

var asyncTask = new Task<ResultClass>(
    profiler =>
    {
        using (var step = (profiler as MiniProfiler).Step("Async!"))
        {
            // exec long running SQL
        }
    }, MiniProfiler.Current);

这几乎可以工作,因为“异步!” step 出现(有点随机,取决于执行,并且有时显示为负数)但并不是我真正想要的。 SQL 时序和语句仍然丢失,在这种情况下它们是最有价值的信息。

理想情况下,我希望将“等待异步”步骤链接到时间(而不是开始步骤)。有什么方法可以将stepWait 链接到结果的 SQL 分析器时间?

有什么想法吗?

【问题讨论】:

  • 棘手,您是否尝试过在异步任务中定义一个新的分析上下文,然后您可以获得一个全新的时机。
  • @SamSaffron - 干杯,我会试一试的。
  • @SamSaffron - 好的,尝试过(类似于下面安德烈斯的答案,但在任务的 lambda 中生成新的上下文)。它使 SQL 计时正确,但同时报告多个异步步骤,所有步骤都具有相同的持续时间(比最慢的 SQL 语句长约 100 毫秒)。有什么方法可以让 SQL 计时脱离一般的计时并将它们与我的“等待异步”步骤相关联?
  • @SamSaffron - 没关系,我想我已经弄清楚了,见下文:)

标签: c# asynchronous mvc-mini-profiler


【解决方案1】:

我找到了一种方法来做到这一点,只保持 SQL 计时,主页步骤仍然正确:

var asyncTask = new Task<T>(
    profiler =>
    {
        var currentProfiler = (profiler as MiniProfiler);

        // Create a new profiler just for this step, we're only going to use the SQL timings
        MiniProfiler newProfiler = null;
        if (currentProfiler != null)
        {
            newProfiler = new MiniProfiler("Async step", currentProfiler.Level);
        }

        using(var con = /* create new DB connection */)
        using(var profiledCon = new ProfiledDbConnection(con, newProfiler))
        {
            // ### Do long running SQL stuff ###
            profiledCon.Query...
        }

        // If we have a profiler and a current step
        if (currentProfiler != null && currentProfiler.Head != null)
        {
            // Add the SQL timings to the step that's active when the SQL completes
            var currentStep = currentProfiler.Head;
            foreach (var sqlTiming in newProfiler.GetSqlTimings())
            {
                currentStep.AddSqlTiming(sqlTiming);
            }
        }

        return result;
    }, MiniProfiler.Current);

这会导致长时间运行的查询的 SQL 计时在 SQL 完成时与当前步骤相关联。通常这是等待异步结果的步骤,但如果 SQL 在我必须等待此之前完成,则将是更早的步骤。

我已经将它包装在一个简洁的QueryAsync&lt;T&gt; 扩展方法中(始终缓冲并且不支持事务),尽管它可以做很多整理工作。当我有更多时间时,我会考虑添加一个ProfiledTask&lt;T&gt; 或类似名称,以允许从已完成的任务中复制分析结果。

更新 1(适用于 1.9)

按照 Sam 的评论(见下文),他说得很对:AddSqlTiming 不是线程安全的。因此,为了解决这个问题,我将其移至同步延续:

// explicit result class for the first task
class ProfiledResult<T>
{
    internal List<SqlTiming> SqlTimings { get; set; }
    internal T Result { get; set; }
}

var currentStep = MiniProfiler.Current.Head;

// Create a task that has its own profiler
var asyncTask = new Task<ProfiledResult<T>>(
    () =>
    {
        // Create a new profiler just for this step, we're only going to use the SQL timings
        var newProfiler = new MiniProfiler("Async step");
        var result = new ProfiledResult<T>();

        result.Result = // ### Do long running SQL stuff ###

        // Get the SQL timing results
        result.SqlTimings = newProfiler.GetSqlTimings();
        return result;
    });

// When the task finishes continue on the main thread to add the SQL timings
var asyncWaiter = asyncTask.ContinueWith<T>(
    t =>
    {
        // Get the wrapped result and add the timings from SQL to the current step
        var completedResult = t.Result;
        foreach (var sqlTiming in completedResult.SqlTimings)
        {
            currentStep.AddSqlTiming(sqlTiming);
        }

        return completedResult.Result;
    }, TaskContinuationOptions.ExecuteSynchronously);


asyncTask.Start();

return asyncWaiter;

这适用于 MvcMiniProfiler 1.9,但不适用于 MiniProfiler 2...

更新 2:MiniProfiler >=2

第 2 版中添加的 EF 东西打破了我上面的技巧(它添加了一个仅限内部的 IsActive 标志),这意味着我需要一种新方法:BaseProfilerProvider 的新实现异步任务:

public class TaskProfilerProvider<T> :
    BaseProfilerProvider
{
    Timing step;
    MiniProfiler asyncProfiler;

    public TaskProfilerProvider(Timing parentStep)
    {
        this.step = parentStep;
    }

    internal T Result { get; set; }

    public override MiniProfiler GetCurrentProfiler()
    {
        return this.asyncProfiler;
    }

    public override MiniProfiler Start(ProfileLevel level)
    {
        var result = new MiniProfiler("TaskProfilerProvider<" + typeof(T).Name + ">", level);
        this.asyncProfiler = result;

        BaseProfilerProvider.SetProfilerActive(result);

        return result;
    }

    public override void Stop(bool discardResults)
    {
        if (this.asyncProfiler == null)
        {
            return;
        }

        if (!BaseProfilerProvider.StopProfiler(this.asyncProfiler))
        {
            return;
        }

        if (discardResults)
        {
            this.asyncProfiler = null;
            return;
        }

        BaseProfilerProvider.SaveProfiler(this.asyncProfiler);
    }

    public T SaveToParent()
    {
        // Add the timings from SQL to the current step
        var asyncProfiler = this.GetCurrentProfiler();
        foreach (var sqlTiming in asyncProfiler.GetSqlTimings())
        {
            this.step.AddSqlTiming(sqlTiming);
        }

        // Clear the results, they should have been copied to the main thread.
        this.Stop(true);

        return this.Result;
    }

    public static T SaveToParent(Task<TaskProfilerProvider<T>> continuedTask)
    {
        return continuedTask.Result.SaveToParent();
    }
}

那么要使用这个提供程序,我只需要在启动任务时启动它,并同步连接延续(和以前一样):

// Create a task that has its own profiler
var asyncTask = new Task<TaskProfilerProvider<T>>(
    () =>
    {
        // Use the provider to start a new MiniProfiler
        var result = new TaskProfilerProvider<T>(currentStep);
        var asyncProfiler = result.Start(level);

        result.Result = // ### Do long running SQL stuff ###

        // Get the results
        return result;
    });

// When the task finishes continue on the main thread to add the SQL timings
var asyncWaiter = asyncTask.ContinueWith<T>(
    TaskProfilerProvider<T>.SaveToParent, 
    TaskContinuationOptions.ExecuteSynchronously);

asyncTask.Start();

return asyncWaiter;

现在 SQL 计时与启动异步操作的步骤一致。 "% in sql" 超过 100%,额外的 82.4% 是并行执行 SQL 节省的时间。

                   duration (ms)  from start (ms)  query time (ms)
Start ...Async     0.0            +19.0            1 sql    4533.0
Wait for ...Async  4132.3         +421.3
                                                    182.4 % in sql

理想情况下,我会在等待步骤而不是初始化步骤上运行长时间运行的 SQL 查询,但是如果不更改调用方法的返回类型以显式传递时间,我看不到一种方法(这将使分析器更加突兀)。

【讨论】:

  • 我关心的是 currentStep.AddSqlTiming(sqlTiming);可能不是线程安全的,请务必通读实现。这也可能会在随机位置插入数据,也许您想在运行查询之前存储头部。
  • @SamSaffron - 你说的很对,AddSqlTiming 不是线程安全的。我已将其移至同步延续并更新了上面的答案。不过还是不太对。
  • @SamSaffron - 啊,别介意 SQL 持续时间是正确的。我有 3 个并行 SQL 语句,每个语句需要 1 到 4 秒。该页面在 5 秒内完成,但分析器的 DurationMillisecondsInSql (正确地)持有 3 个语句的总和(大约 7 秒)。所以“182.4% in sql”意味着我通过并行任务节省了超过 82.4% 的持续时间。
  • 最优秀的。前几天我在想并行 SQL 的想法,巧妙地使用它真的很有帮助。正在考虑向 Dapper 添加一个并行扩展,因此 .Query 返回一个延迟的 IEnumerable(如果结果尚未准备好,它会暂停以从消费者端获取它们)
  • @Kobi - 我还没有看到,但我也有很多同步 SQL 计时(所以MiniProfiler.Current.HasSqlTimings 对我来说总是如此)。您所有的 SQL 调用都是异步的吗?如果是这样,这可能是一个错误,我不确定 - 该步骤应该有HasSqlTimings,因为它只是检查AddSqlTiming 添加的相同List。我认为也许这必须级联到ParentTiming,但也许HasSqlTimings 应该向下级联。
【解决方案2】:

您可以做的是创建一个新的分析器并将其附加到网络上。

var newProfiler = new MiniProfiler("- Other task (discard this time)", ProfileLevel.Verbose);
MiniProfiler.Current.AddProfilerResults(newProfiler);

var asyncTask = new Task(() =>
{
    using (newProfiler.Step("Async!"))
    {
        Thread.Sleep(500);
        using (newProfiler.Step("Async 2!"))
        {
            Thread.Sleep(1000);
        }
    }
});

asyncTask.Start();

新的分析器在其声明中会有错误的时间,但步骤会没问题。

【讨论】:

  • 谢谢,但这与我在上面将MiniProfiler.Current 传递给任务的更新具有相同的效果:我得到相同数量的负时间并且 SQL 配置文件丢失了。
  • 几乎可以进行一些调整 - 我在 Task lambda 中创建了 newProfiler,将 MiniProfiler.Current 传递给它,并在执行步骤后调用 AddProfilerResults。它适用于单个任务,但同时多个任务似乎都以相同的时间完成。
  • 您是生成一个newProfiler 还是为每个Task 生成一个?您可以发布您编写的新代码吗?
  • 是的,但这并不是我真正想要的分析器步骤 - 异步步骤与同步步骤混合总是会有点混乱。我希望 SQL 分析器计时出现在结果中,最好与等待结果的步骤相关联。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-12-21
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多