mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-01-31 09:09:00 +03:00
Add profiler base (#235)
This PR adds the base classes required for profiling code inside of Octobot. The implementation of the profiler is similar to Minecraft, however it is more detailed and provides per-event logs for each event. This PR does not change any code to be profiled and this is intentional. Changes required for profiling will come as separate PRs, one for commands, one for responders, and one for background services. Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
parent
d4871bb23d
commit
7d9a85d815
4 changed files with 153 additions and 0 deletions
|
@ -4,6 +4,7 @@ using Microsoft.Extensions.Hosting;
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Octobot.Commands.Events;
|
||||
using Octobot.Services;
|
||||
using Octobot.Services.Profiler;
|
||||
using Octobot.Services.Update;
|
||||
using Remora.Discord.API.Abstractions.Gateway.Commands;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
@ -86,6 +87,8 @@ public sealed class Octobot
|
|||
.AddPreparationErrorEvent<LoggingPreparationErrorEvent>()
|
||||
.AddPostExecutionEvent<ErrorLoggingPostExecutionEvent>()
|
||||
// Services
|
||||
.AddTransient<Profiler>()
|
||||
.AddSingleton<ProfilerFactory>()
|
||||
.AddSingleton<Utility>()
|
||||
.AddSingleton<GuildDataService>()
|
||||
.AddHostedService<GuildDataService>(provider => provider.GetRequiredService<GuildDataService>())
|
||||
|
|
114
src/Services/Profiler/Profiler.cs
Normal file
114
src/Services/Profiler/Profiler.cs
Normal file
|
@ -0,0 +1,114 @@
|
|||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Results;
|
||||
|
||||
// TODO: remove in future profiler PRs
|
||||
// ReSharper disable All
|
||||
|
||||
namespace Octobot.Services.Profiler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides the ability to profile how long certain parts of code take to complete using <see cref="Stopwatch"/>es.
|
||||
/// </summary>
|
||||
/// <remarks>Resolve <see cref="ProfilerFactory"/> instead in singletons.</remarks>
|
||||
public sealed class Profiler
|
||||
{
|
||||
private const int MaxProfilerTime = 1000; // milliseconds
|
||||
private readonly List<ProfilerEvent> _events = [];
|
||||
private readonly ILogger<Profiler> _logger;
|
||||
|
||||
public Profiler(ILogger<Profiler> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes an event to the profiler.
|
||||
/// </summary>
|
||||
/// <param name="id">The ID of the event.</param>
|
||||
public void Push(string id)
|
||||
{
|
||||
_events.Add(new ProfilerEvent
|
||||
{
|
||||
Id = id,
|
||||
Stopwatch = Stopwatch.StartNew()
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pops the last pushed event from the profiler.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Thrown if the profiler contains no events.</exception>
|
||||
public void Pop()
|
||||
{
|
||||
if (_events.Count is 0)
|
||||
{
|
||||
throw new InvalidOperationException("Nothing to pop");
|
||||
}
|
||||
|
||||
_events.Last().Stopwatch.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If the profiler took too long to execute, this will log a warning with per-event time usage
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
private void Report()
|
||||
{
|
||||
var main = _events[0];
|
||||
if (main.Stopwatch.ElapsedMilliseconds < MaxProfilerTime)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var unprofiled = main.Stopwatch.ElapsedMilliseconds;
|
||||
var builder = new StringBuilder().AppendLine();
|
||||
for (var i = 1; i < _events.Count; i++)
|
||||
{
|
||||
var profilerEvent = _events[i];
|
||||
if (profilerEvent.Stopwatch.IsRunning)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Tried to report on a profiler with running stopwatches: {profilerEvent.Id}");
|
||||
}
|
||||
|
||||
builder.AppendLine($"{profilerEvent.Id}: {profilerEvent.Stopwatch.ElapsedMilliseconds}ms");
|
||||
unprofiled -= profilerEvent.Stopwatch.ElapsedMilliseconds;
|
||||
}
|
||||
|
||||
builder.AppendLine($"<unprofiled>: {unprofiled}ms");
|
||||
|
||||
_logger.LogWarning("Profiler {ID} took {Elapsed} milliseconds to execute (max: {Max}ms):{Events}", main.Id,
|
||||
main.Stopwatch.ElapsedMilliseconds, MaxProfilerTime, builder.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="Pop"/> the profiler and <see cref="Report"/> on it afterwards.
|
||||
/// </summary>
|
||||
public void PopAndReport()
|
||||
{
|
||||
Pop();
|
||||
Report();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="PopAndReport"/> on the profiler and return a <see cref="Result{TEntity}"/>.
|
||||
/// </summary>
|
||||
/// <param name="result"></param>
|
||||
/// <returns></returns>
|
||||
public Result ReportWithResult(Result result)
|
||||
{
|
||||
PopAndReport();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="ReportWithResult"/> with <see cref="Result.FromSuccess"/>
|
||||
/// </summary>
|
||||
/// <returns>A successful result.</returns>
|
||||
public Result ReportWithSuccess()
|
||||
{
|
||||
return ReportWithResult(Result.FromSuccess());
|
||||
}
|
||||
}
|
9
src/Services/Profiler/ProfilerEvent.cs
Normal file
9
src/Services/Profiler/ProfilerEvent.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using System.Diagnostics;
|
||||
|
||||
namespace Octobot.Services.Profiler;
|
||||
|
||||
public struct ProfilerEvent
|
||||
{
|
||||
public string Id { get; init; }
|
||||
public Stopwatch Stopwatch { get; init; }
|
||||
}
|
27
src/Services/Profiler/ProfilerFactory.cs
Normal file
27
src/Services/Profiler/ProfilerFactory.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Octobot.Services.Profiler;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a method to create a <see cref="Profiler"/>. Useful in singletons.
|
||||
/// </summary>
|
||||
public sealed class ProfilerFactory
|
||||
{
|
||||
private readonly IServiceScopeFactory _scopeFactory;
|
||||
|
||||
public ProfilerFactory(IServiceScopeFactory scopeFactory)
|
||||
{
|
||||
_scopeFactory = scopeFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="Profiler"/>.
|
||||
/// </summary>
|
||||
/// <returns>A new <see cref="Profiler"/>.</returns>
|
||||
// TODO: remove in future profiler PRs
|
||||
// ReSharper disable once UnusedMember.Global
|
||||
public Profiler Create()
|
||||
{
|
||||
return _scopeFactory.CreateScope().ServiceProvider.GetRequiredService<Profiler>();
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue