Включение таймингов мини-профилировщика MVC в асинхронные задачи - PullRequest
7 голосов
/ 01 декабря 2011

У меня длинный запрос 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 и await)

При использовании мини-профилировщика MVC я получаю время для «Ожидание асинхронного», но я не могу получить время для «асинхронного!» шаг.

Есть ли какой-нибудь способ получить эти результаты (возможно, только тайминги SQL) в трассировке для завершенной страницы?

Обновление

Я нашел способ включить шаги профилировщика в асинхронный метод:

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

Это почти работает, в этом "Async!" шаг появляется (несколько случайным образом, в зависимости от исполнения и иногда выглядит как отрицательный), но на самом деле это не то, что я хочу. Время и операторы SQL по-прежнему утеряны, и в этом случае они являются наиболее ценной информацией.

В идеале я бы хотел, чтобы шаг «Ожидание асинхронного» был связан со временем (а не с начальным этапом). Можно ли каким-то образом связать stepWait с временем профилировщика SQL для результата?

Есть идеи?

Ответы [ 2 ]

10 голосов
/ 20 декабря 2011

Я нашел способ сделать это, сохранив только временные параметры 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<T> в стиле dapper.(всегда буферизированные и не поддерживающие транзакции), хотя это может привести к большому количеству операций.Когда у меня будет больше времени, я посмотрю на добавление ProfiledTask<T> или аналогичного файла, который позволяет копировать профилированные результаты из выполненного задания.

Обновление 1 (работает в 1.9)

После комментария Сэма (см. Ниже) он совершенно прав: 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, ломает мой взлом выше (он добавляет флаг 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 отображаются последовательно против шага, инициировавшего асинхронное действие. "% в 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-запроса на этапе ожидания, а не на этапе инициализации, но я не вижу способа сделать это без изменения типа возвращаемого значения вызывающих методов для явной передачи времени (что сделает профилировщик значительно более навязчивым).

3 голосов
/ 19 декабря 2011

Что вы можете сделать, это создать новый профилировщик и подключить его к веб-сайту.

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

У нового профилировщика будут неправильные времена в его объявлении, но шаги будут в порядке.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...