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