diff --git a/frontend/src/index.ejs b/frontend/src/index.ejs
index c86078033..0ff5e8bb5 100644
--- a/frontend/src/index.ejs
+++ b/frontend/src/index.ejs
@@ -58,6 +58,8 @@
};
+ __MINI_PROFILER__
+
<% for (key in htmlWebpackPlugin.files.js) { %><% } %>
<% for (key in htmlWebpackPlugin.files.css) { %><% } %>
diff --git a/src/NzbDrone.Common/Options/AppOptions.cs b/src/NzbDrone.Common/Options/AppOptions.cs
index 74cdf1d29..e38f5664c 100644
--- a/src/NzbDrone.Common/Options/AppOptions.cs
+++ b/src/NzbDrone.Common/Options/AppOptions.cs
@@ -5,4 +5,6 @@ public class AppOptions
public string InstanceName { get; set; }
public string Theme { get; set; }
public bool? LaunchBrowser { get; set; }
+ public bool? ProfilerEnabled { get; set; }
+ public string ProfilerPosition { get; set; }
}
diff --git a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs
index 415e20576..8cd48849b 100644
--- a/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs
+++ b/src/NzbDrone.Core/Configuration/ConfigFileProvider.cs
@@ -69,6 +69,8 @@ namespace NzbDrone.Core.Configuration
string PostgresMainDbConnectionString { get; }
string PostgresLogDbConnectionString { get; }
bool TrustCgnatIpAddresses { get; }
+ bool ProfilerEnabled { get; }
+ string ProfilerPosition { get; }
}
public class ConfigFileProvider : IConfigFileProvider
@@ -234,6 +236,8 @@ namespace NzbDrone.Core.Configuration
? enumValue
: GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
+ public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
+
public bool AnalyticsEnabled => _logOptions.AnalyticsEnabled ?? GetValueBoolean("AnalyticsEnabled", true, persist: false);
public string Branch => _updateOptions.Branch ?? GetValue("Branch", "main").ToLowerInvariant();
@@ -312,6 +316,9 @@ namespace NzbDrone.Core.Configuration
public string SyslogLevel => _logOptions.SyslogLevel ?? GetValue("SyslogLevel", LogLevel, persist: false).ToLowerInvariant();
+ public bool ProfilerEnabled => _appOptions.ProfilerEnabled ?? GetValueBoolean("ProfilerEnabled", false, persist: false);
+ public string ProfilerPosition => _appOptions.ProfilerPosition ?? GetValue("ProfilerPosition", "bottom-right", persist: false);
+
public int GetValueInt(string key, int defaultValue, bool persist = true)
{
return Convert.ToInt32(GetValue(key, defaultValue, persist));
@@ -499,7 +506,5 @@ namespace NzbDrone.Core.Configuration
{
SetValue("ApiKey", GenerateApiKey());
}
-
- public bool TrustCgnatIpAddresses => _authOptions.TrustCgnatIpAddresses ?? GetValueBoolean("TrustCgnatIpAddresses", false, persist: false);
}
}
diff --git a/src/NzbDrone.Core/Datastore/Database.cs b/src/NzbDrone.Core/Datastore/Database.cs
index 741a22f0b..02e687552 100644
--- a/src/NzbDrone.Core/Datastore/Database.cs
+++ b/src/NzbDrone.Core/Datastore/Database.cs
@@ -1,5 +1,4 @@
using System;
-using System.Data;
using System.Data.Common;
using System.Data.SQLite;
using Dapper;
@@ -10,7 +9,7 @@ namespace NzbDrone.Core.Datastore
{
public interface IDatabase
{
- IDbConnection OpenConnection();
+ DbConnection OpenConnection();
Version Version { get; }
int Migration { get; }
DatabaseType DatabaseType { get; }
@@ -20,17 +19,17 @@ namespace NzbDrone.Core.Datastore
public class Database : IDatabase
{
private readonly string _databaseName;
- private readonly Func _datamapperFactory;
+ private readonly Func _datamapperFactory;
private readonly Logger _logger = NzbDroneLogger.GetLogger(typeof(Database));
- public Database(string databaseName, Func datamapperFactory)
+ public Database(string databaseName, Func datamapperFactory)
{
_databaseName = databaseName;
_datamapperFactory = datamapperFactory;
}
- public IDbConnection OpenConnection()
+ public DbConnection OpenConnection()
{
return _datamapperFactory();
}
@@ -50,7 +49,7 @@ namespace NzbDrone.Core.Datastore
get
{
using var db = _datamapperFactory();
- var dbConnection = db as DbConnection;
+ var dbConnection = db;
return DatabaseVersionParser.ParseServerVersion(dbConnection.ServerVersion);
}
diff --git a/src/NzbDrone.Core/Datastore/LogDatabase.cs b/src/NzbDrone.Core/Datastore/LogDatabase.cs
index a770c2661..f996986d8 100644
--- a/src/NzbDrone.Core/Datastore/LogDatabase.cs
+++ b/src/NzbDrone.Core/Datastore/LogDatabase.cs
@@ -1,5 +1,5 @@
using System;
-using System.Data;
+using System.Data.Common;
namespace NzbDrone.Core.Datastore
{
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.Datastore
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
}
- public IDbConnection OpenConnection()
+ public DbConnection OpenConnection()
{
return _database.OpenConnection();
}
diff --git a/src/NzbDrone.Core/Datastore/MainDatabase.cs b/src/NzbDrone.Core/Datastore/MainDatabase.cs
index 521293299..7e39b1356 100644
--- a/src/NzbDrone.Core/Datastore/MainDatabase.cs
+++ b/src/NzbDrone.Core/Datastore/MainDatabase.cs
@@ -1,5 +1,7 @@
using System;
-using System.Data;
+using System.Data.Common;
+using StackExchange.Profiling;
+using StackExchange.Profiling.Data;
namespace NzbDrone.Core.Datastore
{
@@ -18,9 +20,16 @@ namespace NzbDrone.Core.Datastore
_databaseType = _database == null ? DatabaseType.SQLite : _database.DatabaseType;
}
- public IDbConnection OpenConnection()
+ public DbConnection OpenConnection()
{
- return _database.OpenConnection();
+ var connection = _database.OpenConnection();
+
+ if (_databaseType == DatabaseType.PostgreSQL)
+ {
+ return new ProfiledImplementations.NpgSqlConnection(connection, MiniProfiler.Current);
+ }
+
+ return new ProfiledDbConnection(connection, MiniProfiler.Current);
}
public Version Version => _database.Version;
diff --git a/src/NzbDrone.Core/Datastore/ProfiledImplementations.cs b/src/NzbDrone.Core/Datastore/ProfiledImplementations.cs
new file mode 100644
index 000000000..040866066
--- /dev/null
+++ b/src/NzbDrone.Core/Datastore/ProfiledImplementations.cs
@@ -0,0 +1,15 @@
+using System.Data.Common;
+using StackExchange.Profiling.Data;
+
+namespace NzbDrone.Core.Datastore;
+
+public static class ProfiledImplementations
+{
+ public class NpgSqlConnection : ProfiledDbConnection
+ {
+ public NpgSqlConnection(DbConnection connection, IDbProfiler profiler)
+ : base(connection, profiler)
+ {
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/Sonarr.Core.csproj b/src/NzbDrone.Core/Sonarr.Core.csproj
index 92f2334dd..cb37130f2 100644
--- a/src/NzbDrone.Core/Sonarr.Core.csproj
+++ b/src/NzbDrone.Core/Sonarr.Core.csproj
@@ -9,6 +9,7 @@
+
diff --git a/src/NzbDrone.Host/Sonarr.Host.csproj b/src/NzbDrone.Host/Sonarr.Host.csproj
index f3fc5e09d..1a672d797 100644
--- a/src/NzbDrone.Host/Sonarr.Host.csproj
+++ b/src/NzbDrone.Host/Sonarr.Host.csproj
@@ -4,6 +4,7 @@
Library
+
diff --git a/src/NzbDrone.Host/Startup.cs b/src/NzbDrone.Host/Startup.cs
index fc1c7e957..85b1bb55e 100644
--- a/src/NzbDrone.Host/Startup.cs
+++ b/src/NzbDrone.Host/Startup.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
+using System.Threading.Tasks;
using DryIoc;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
@@ -35,6 +36,7 @@ using Sonarr.Http.ClientSchema;
using Sonarr.Http.ErrorManagement;
using Sonarr.Http.Frontend;
using Sonarr.Http.Middleware;
+using StackExchange.Profiling;
using IPNetwork = System.Net.IPNetwork;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
@@ -236,6 +238,45 @@ namespace NzbDrone.Host
});
services.AddAppAuthentication();
+
+ services.AddOptions()
+ .Configure((options, configFileProvider) =>
+ {
+ options.RouteBasePath = "/profiler";
+
+ switch (configFileProvider.Theme)
+ {
+ case "light":
+ options.ColorScheme = ColorScheme.Light;
+ break;
+ case "dark":
+ options.ColorScheme = ColorScheme.Dark;
+ break;
+ default:
+ options.ColorScheme = ColorScheme.Auto;
+ break;
+ }
+
+ switch (configFileProvider.ProfilerPosition)
+ {
+ case "top-left":
+ options.PopupRenderPosition = RenderPosition.Left;
+ break;
+ case "top-right":
+ options.PopupRenderPosition = RenderPosition.Right;
+ break;
+ case "bottom-left":
+ options.PopupRenderPosition = RenderPosition.BottomLeft;
+ break;
+ default:
+ options.PopupRenderPosition = RenderPosition.BottomRight;
+ break;
+ }
+
+ options.IgnoredPaths.Add("/MediaCover");
+ });
+
+ services.AddMiniProfiler();
}
public void Configure(IApplicationBuilder app,
@@ -312,6 +353,7 @@ namespace NzbDrone.Host
app.UseMiddleware(new List { "/api/v3/command", "/api/v5/command" });
app.UseWebSockets();
+ app.UseMiniProfiler();
// Enable middleware to serve generated Swagger as a JSON endpoint.
if (BuildInfo.IsDebug)
@@ -325,6 +367,7 @@ namespace NzbDrone.Host
app.UseEndpoints(x =>
{
x.MapHub("/signalr/messages").RequireAuthorization("SignalR");
+ x.MapPost("/profiler/results", context => Task.CompletedTask).RequireAuthorization("UI");
x.MapControllers();
});
}
diff --git a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs
index efa8a7882..f0605b24b 100644
--- a/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/HtmlMapperBase.cs
@@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Text.RegularExpressions;
+using Microsoft.AspNetCore.Http;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
@@ -27,9 +28,9 @@ namespace Sonarr.Http.Frontend.Mappers
protected string HtmlPath;
protected string UrlBase;
- protected override Stream GetContentStream(string filePath)
+ protected override Stream GetContentStream(HttpContext context, string filePath)
{
- var text = GetHtmlText();
+ var text = GetHtmlText(context);
var stream = new MemoryStream();
var writer = new StreamWriter(stream);
@@ -39,7 +40,7 @@ namespace Sonarr.Http.Frontend.Mappers
return stream;
}
- protected virtual string GetHtmlText()
+ protected virtual string GetHtmlText(HttpContext context)
{
if (RuntimeInfo.IsProduction && _generatedContent != null)
{
diff --git a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs
index 0f8ebe74d..82572e307 100644
--- a/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/IMapHttpRequestsToDisk.cs
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
namespace Sonarr.Http.Frontend.Mappers
@@ -7,6 +8,6 @@ namespace Sonarr.Http.Frontend.Mappers
{
string Map(string resourceUrl);
bool CanHandle(string resourceUrl);
- Task GetResponse(string resourceUrl);
+ Task GetResponse(HttpContext context, string resourceUrl);
}
}
diff --git a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs
index d8cbaab63..0c8763d11 100644
--- a/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/IndexHtmlMapper.cs
@@ -1,9 +1,12 @@
using System;
using System.IO;
+using Microsoft.AspNetCore.Http;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
+using NzbDrone.Common.Extensions;
using NzbDrone.Core.Configuration;
+using StackExchange.Profiling;
namespace Sonarr.Http.Frontend.Mappers
{
@@ -38,5 +41,30 @@ namespace Sonarr.Http.Frontend.Mappers
!resourceUrl.Contains('.') &&
!resourceUrl.StartsWith("/login");
}
+
+ protected override string GetHtmlText(HttpContext context)
+ {
+ var html = base.GetHtmlText(context);
+
+ if (_configFileProvider.ProfilerEnabled)
+ {
+ var includes = MiniProfiler.Current?.RenderIncludes(context);
+
+ if (includes == null || includes.Value.IsNullOrWhiteSpace())
+ {
+ html = html.Replace("__MINI_PROFILER__", "");
+ }
+ else
+ {
+ html = html.Replace("__MINI_PROFILER__", includes.Value);
+ }
+ }
+ else
+ {
+ html = html.Replace("__MINI_PROFILER__", "");
+ }
+
+ return html;
+ }
}
}
diff --git a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs
index ca325add6..2f2c8faee 100644
--- a/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/LoginHtmlMapper.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using Microsoft.AspNetCore.Http;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
@@ -33,9 +34,9 @@ namespace Sonarr.Http.Frontend.Mappers
return resourceUrl.StartsWith("/login");
}
- protected override string GetHtmlText()
+ protected override string GetHtmlText(HttpContext context)
{
- var html = base.GetHtmlText();
+ var html = base.GetHtmlText(context);
var theme = _configFileProvider.Theme;
html = html.Replace("_THEME_", theme);
diff --git a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs
index 295e31645..6351a55d2 100644
--- a/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/MediaCoverProxyMapper.cs
@@ -2,6 +2,7 @@ using System;
using System.Net;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using NzbDrone.Core.MediaCover;
@@ -31,7 +32,7 @@ namespace Sonarr.Http.Frontend.Mappers
return resourceUrl.StartsWith("/MediaCoverProxy/", StringComparison.InvariantCultureIgnoreCase);
}
- public async Task GetResponse(string resourceUrl)
+ public async Task GetResponse(HttpContext context, string resourceUrl)
{
var match = _regex.Match(resourceUrl);
diff --git a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs
index 3f3982ac1..ace688b2f 100644
--- a/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/StaticResourceMapperBase.cs
@@ -2,6 +2,7 @@ using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Net.Http.Headers;
@@ -31,7 +32,7 @@ namespace Sonarr.Http.Frontend.Mappers
public abstract bool CanHandle(string resourceUrl);
- public Task GetResponse(string resourceUrl)
+ public Task GetResponse(HttpContext context, string resourceUrl)
{
var filePath = Map(resourceUrl);
@@ -42,7 +43,7 @@ namespace Sonarr.Http.Frontend.Mappers
contentType = "application/octet-stream";
}
- return Task.FromResult(new FileStreamResult(GetContentStream(filePath), new MediaTypeHeaderValue(contentType)
+ return Task.FromResult(new FileStreamResult(GetContentStream(context, filePath), new MediaTypeHeaderValue(contentType)
{
Encoding = contentType == "text/plain" ? Encoding.UTF8 : null
}));
@@ -53,7 +54,7 @@ namespace Sonarr.Http.Frontend.Mappers
return Task.FromResult(null);
}
- protected virtual Stream GetContentStream(string filePath)
+ protected virtual Stream GetContentStream(HttpContext context, string filePath)
{
return File.OpenRead(filePath);
}
diff --git a/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs
index c79d16464..b1bf41ef7 100644
--- a/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs
+++ b/src/Sonarr.Http/Frontend/Mappers/UrlBaseReplacementResourceMapperBase.cs
@@ -1,4 +1,5 @@
using System.IO;
+using Microsoft.AspNetCore.Http;
using NLog;
using NzbDrone.Common.Disk;
using NzbDrone.Common.EnvironmentInfo;
@@ -27,7 +28,7 @@ namespace Sonarr.Http.Frontend.Mappers
return FilePath;
}
- protected override Stream GetContentStream(string filePath)
+ protected override Stream GetContentStream(HttpContext context, string filePath)
{
var text = GetFileText();
diff --git a/src/Sonarr.Http/Frontend/StaticResourceController.cs b/src/Sonarr.Http/Frontend/StaticResourceController.cs
index 49bc495b7..508f9ceec 100644
--- a/src/Sonarr.Http/Frontend/StaticResourceController.cs
+++ b/src/Sonarr.Http/Frontend/StaticResourceController.cs
@@ -54,7 +54,7 @@ namespace Sonarr.Http.Frontend
if (mapper != null)
{
- var result = await mapper.GetResponse(path);
+ var result = await mapper.GetResponse(Request.HttpContext, path);
if (result != null)
{
diff --git a/src/Sonarr.Http/Sonarr.Http.csproj b/src/Sonarr.Http/Sonarr.Http.csproj
index c7ce3317c..c34f21318 100644
--- a/src/Sonarr.Http/Sonarr.Http.csproj
+++ b/src/Sonarr.Http/Sonarr.Http.csproj
@@ -5,6 +5,7 @@
+