Record high level profiling data in ASP.NET Core applications at runtime. Profiles can then be visualized using speedscope.
To use the recorder, register the middleware then request a BlackBox
recorder at runtime to instrument different subsystems. The blackbox will store a configurable number of request profiles (default is 16).
// use default profile naming (root stack frame will have a name like 'GET relative/url?queryparams')
builder.Services.AddRequestRecording();
// for custom naming, implement INomenclator
builder.Services.AddRequestRecording<MyNomenclator>();
At startup, register the middleware:
// always run
app.UseRequestRecorder();
// profile specific URL
app.UseWhen(context => context.Request.Path.StartsWithSegments("Foo") , appBuilder =>
{
appBuilder.UseRequestRecorder();
});
For example, to instrument output create an instrumented formatter and decorate the existing formatter classes.
public class InstrumentedOutputFormatter : IOutputFormatter
{
private readonly IOutputFormatter inner;
public InstrumentedOutputFormatter(IOutputFormatter inner) { this.inner = inner; }
public bool CanWriteResult(OutputFormatterCanWriteContext context)
{
return inner.CanWriteResult(context);
}
public async Task WriteAsync(OutputFormatterWriteContext context)
{
// record time spent in this method via Capture
using var frame = context.HttpContext.RequestServices.RecordStackFrame("OutputFormatter");
await inner.WriteAsync(context);
}
}
builder.Services
.AddMvcOptions(options =>
{
// wrap all the formatters with instrumentation
List<IOutputFormatter> newFormatters = new List<IOutputFormatter>(options.OutputFormatters.Count);
foreach (var f in options.OutputFormatters)
{
newFormatters.Add(new InstrumentedOutputFormatter(f));
}
options.OutputFormatters.Clear();
foreach (var nf in newFormatters)
{
options.OutputFormatters.Add(nf);
}
});
Add a controller to integrate with speedscope easily at runtime by sending HTTP GET to https://localhost/profile. This controller will first redirect the caller to speedscope with a parameterized profile URL. When speedscope requests the profile data, detect it via the origin header and return the profile.
builder.Services.AddCors(options =>
{
options.AddPolicy(ProfileDataController.CorsPolicyName,
builder =>
{
builder
.WithOrigins(@"https://www.speedscope.app")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
// ...
app.UseCors(ProfileDataController.CorsPolicyName);
[ApiController]
[Route("[controller]")]
public class ProfileController : ControllerBase
{
public const string CorsPolicyName = "allowSpeedscope";
[EnableCors(CorsPolicyName)]
public async Task<IActionResult> Get()
{
if (this.HttpContext.Request.Headers.TryGetValue("Origin", out var origin) && origin[0] == "https://www.speedscope.app")
{
var memoryStream = new MemoryStream();
using (var speedscopeWriter = new SpeedscopeWriter(memoryStream))
{
speedscopeWriter.WritePreAmble();
foreach (var request in BlackBox.History)
{
speedscopeWriter.WriteEvent(request);
}
speedscopeWriter.Flush();
}
memoryStream.Position = 0;
var fileResult = File(memoryStream, "application/json", "profile.json");
return fileResult;
}
if (!BlackBox.HasHistory)
{
return NotFound("No profiles have been recorded");
}
return Redirect($"https://speedscope.app#profileURL=https://{this.HttpContext.Request.Host}/Profile");
}
}