将 MVC 迷你分析器计时纳入异步任务

发布于 2024-12-19 00:51:47 字数 1154 浏览 2 评论 0原文

我在页面内有一个长时间运行的 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 推出 async,此语法将会好得多等待

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

有什么方法可以将这些结果(也许只是 SQL 计时)放入已完成页面的跟踪中?

更新

我找到了一种让探查器进入异步方法的方法:

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

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

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

有什么想法吗?

I have a long running SQL query inside a page that I've sped up by using an async task:

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;
}

(Note that this syntax will be a lot nicer once C# 5 comes out with async and await)

When using MVC mini profiler I get the timing for "Wait for Async", but I can't get the timing for the "Async!" step.

Is there any way to get those results (maybe just the SQL timings) into the trace for the completed page?

Update

I've found a way to get the profiler steps into the async method:

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

That almost works, in that the "Async!" step appears (somewhat randomly, depending on the execution, and with some times appearing as negative) but isn't really what I want. The SQL timings and statements are still lost, and in this case they're the most valuable information.

Ideally I'd like the "Wait for Async" step to be linked to the timings (rather than the start step). Is there some way that stepWait could be linked to the SQL profiler times for the result?

Any ideas?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

玩套路吗 2024-12-26 00:51:47

我找到了一种方法来做到这一点,只需保持主页面步骤的 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 扩展方法中(始终缓冲且不支持事务),尽管它可以做很多整理工作。当我有更多时间时,我会考虑添加 ProfiledTask 或类似的内容,以允许从已完成的任务复制分析结果。

更新 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

EF 内容版本 2 中添加的内容破坏了上面的 hack(它添加了一个仅限内部的 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 查询,但我看不到一种方法可以在不更改调用方法的返回类型以显式传递计时的情况下执行此操作(这将使分析器更加引人注目)。

I've found a way to do this, by only keeping the SQL timings the main page steps still add up right:

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);

This results in the SQL timings for the long running query being associated with the current step when the SQL completes. Typically this is the step waiting for the async result, but will be an earlier step if the SQL completes before I have to wait for this.

I've wrapped this in a dapper-style QueryAsync<T> extension method (always buffered and not supporting transactions) though it could do with a lot of tidy up. When I have more time I'll look into adding a ProfiledTask<T> or similar that allows the profiled results to be copied from the completed task.

Update 1 (works in 1.9)

Following Sam's comment (see below) he's quite right: AddSqlTiming is not thread safe. So to get around that I've moved that to a synchronous continuation:

// 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;

This works in MvcMiniProfiler 1.9, but doesn't work in MiniProfiler 2...

Update 2: MiniProfiler >=2

The EF stuff added in version 2 breaks my hack above (it adds an internal-only IsActive flag), meaning that I needed a new approach: a new implementation of BaseProfilerProvider for async tasks:

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();
    }
}

So then to use this provider I just need to start it when starting the task, and hook up the continuation synchronously (as before):

// 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;

Now the SQL timings appear consistently against the step that initiated the async action. The "% in sql" is more than 100% though, that extra 82.4% is the time saved by doing the SQL in parallel.

                   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

Ideally I'd have the long running SQL query on the wait step rather than the init step, but I can't see a way to do that without changing the return type of the calling methods to explicitly pass around the timings (which would make the profiler considerably more obtrusive).

爱给你人给你 2024-12-26 00:51:47

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

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();

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

What you can do is to create a new profiler and attach it to the web one.

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();

The new profiler will have wrong times in its declaration but the steps are gonna be ok.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文