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