diff --git a/JsonApiDotNetCore.sln.DotSettings b/JsonApiDotNetCore.sln.DotSettings
index a6443a0787..595cae92d8 100644
--- a/JsonApiDotNetCore.sln.DotSettings
+++ b/JsonApiDotNetCore.sln.DotSettings
@@ -78,7 +78,10 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$);
SUGGESTION
WARNING
HINT
+ WARNING
WARNING
+ WARNING
+ WARNING
WARNING
<?xml version="1.0" encoding="utf-16"?><Profile name="JADNC Full Cleanup"><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" ArrangeTrailingCommas="True" ArrangeObjectCreation="True" ArrangeDefaultValue="True" /><CssAlphabetizeProperties>True</CssAlphabetizeProperties><JsInsertSemicolon>True</JsInsertSemicolon><FormatAttributeQuoteDescriptor>True</FormatAttributeQuoteDescriptor><CorrectVariableKindsDescriptor>True</CorrectVariableKindsDescriptor><VariablesToInnerScopesDescriptor>True</VariablesToInnerScopesDescriptor><StringToTemplatesDescriptor>True</StringToTemplatesDescriptor><JsReformatCode>True</JsReformatCode><JsFormatDocComments>True</JsFormatDocComments><RemoveRedundantQualifiersTs>True</RemoveRedundantQualifiersTs><OptimizeImportsTs>True</OptimizeImportsTs><OptimizeReferenceCommentsTs>True</OptimizeReferenceCommentsTs><PublicModifierStyleTs>True</PublicModifierStyleTs><ExplicitAnyTs>True</ExplicitAnyTs><TypeAnnotationStyleTs>True</TypeAnnotationStyleTs><RelativePathStyleTs>True</RelativePathStyleTs><AsInsteadOfCastTs>True</AsInsteadOfCastTs><HtmlReformatCode>True</HtmlReformatCode><AspOptimizeRegisterDirectives>True</AspOptimizeRegisterDirectives><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSUseAutoProperty>True</CSUseAutoProperty><CSMakeFieldReadonly>True</CSMakeFieldReadonly><CSMakeAutoPropertyGetOnly>True</CSMakeAutoPropertyGetOnly><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CssReformatCode>True</CssReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSShortenReferences>True</CSShortenReferences><CSReformatCode>True</CSReformatCode><CSharpFormatDocComments>True</CSharpFormatDocComments><CSReorderTypeMembers>True</CSReorderTypeMembers><XAMLCollapseEmptyTags>False</XAMLCollapseEmptyTags></Profile>
JADNC Full Cleanup
@@ -620,15 +623,19 @@ $left$ = $right$;
$collection$.IsNullOrEmpty()
$collection$ == null || !$collection$.Any()
WARNING
+ True
True
True
+ True
True
True
True
+ True
True
True
True
True
True
True
+ True
diff --git a/docs/docfx.json b/docs/docfx.json
index 6acc17ce7a..98e83101a5 100644
--- a/docs/docfx.json
+++ b/docs/docfx.json
@@ -8,10 +8,7 @@
}
],
"dest": "api",
- "disableGitFeatures": false,
- "properties": {
- "targetFramework": "netcoreapp3.1"
- }
+ "disableGitFeatures": false
}
],
"build": {
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs
index 19879aef27..04c3a3d551 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs
@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
namespace JsonApiDotNetCoreExample.Startups
{
@@ -14,7 +15,9 @@ public virtual void ConfigureServices(IServiceCollection services)
{
}
- public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+ // ReSharper disable once UnusedMemberInSuper.Global
+ // ReSharper disable once UnusedParameter.Global
+ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory)
{
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
index 84ddf4a4de..9a149ca9a3 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
@@ -1,5 +1,6 @@
using System;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCoreExample.Data;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Builder;
@@ -7,6 +8,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
@@ -14,10 +16,14 @@ namespace JsonApiDotNetCoreExample.Startups
{
public sealed class Startup : EmptyStartup
{
+ private readonly ICodeTimerSession _codeTimingSession;
private readonly string _connectionString;
public Startup(IConfiguration configuration)
{
+ _codeTimingSession = new DefaultCodeTimerSession();
+ CodeTimingSessionManager.Capture(_codeTimingSession);
+
string postgresPassword = Environment.GetEnvironmentVariable("PGPASSWORD") ?? "postgres";
_connectionString = configuration["Data:DefaultConnection"].Replace("###", postgresPassword);
}
@@ -25,43 +31,67 @@ public Startup(IConfiguration configuration)
// This method gets called by the runtime. Use this method to add services to the container.
public override void ConfigureServices(IServiceCollection services)
{
- services.AddSingleton();
-
- services.AddDbContext(options =>
+ using (CodeTimingSessionManager.Current.Measure("Configure other (startup)"))
{
- options.UseNpgsql(_connectionString);
+ services.AddSingleton();
+
+ services.AddDbContext(options =>
+ {
+ options.UseNpgsql(_connectionString);
#if DEBUG
- options.EnableSensitiveDataLogging();
- options.EnableDetailedErrors();
+ options.EnableSensitiveDataLogging();
+ options.EnableDetailedErrors();
#endif
- });
+ });
- services.AddJsonApi(options =>
- {
- options.Namespace = "api/v1";
- options.UseRelativeLinks = true;
- options.ValidateModelState = true;
- options.IncludeTotalResourceCount = true;
- options.SerializerSettings.Formatting = Formatting.Indented;
- options.SerializerSettings.Converters.Add(new StringEnumConverter());
+ using (CodeTimingSessionManager.Current.Measure("Configure JSON:API (startup)"))
+ {
+ services.AddJsonApi(options =>
+ {
+ options.Namespace = "api/v1";
+ options.UseRelativeLinks = true;
+ options.ValidateModelState = true;
+ options.IncludeTotalResourceCount = true;
+ options.SerializerSettings.Formatting = Formatting.Indented;
+ options.SerializerSettings.Converters.Add(new StringEnumConverter());
#if DEBUG
- options.IncludeExceptionStackTraceInErrors = true;
+ options.IncludeExceptionStackTraceInErrors = true;
#endif
- }, discovery => discovery.AddCurrentAssembly());
+ }, discovery => discovery.AddCurrentAssembly());
+ }
+ }
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+ public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment, ILoggerFactory loggerFactory)
{
- using (IServiceScope scope = app.ApplicationServices.CreateScope())
+ ILogger logger = loggerFactory.CreateLogger();
+
+ using (CodeTimingSessionManager.Current.Measure("Initialize other (startup)"))
+ {
+ using (IServiceScope scope = app.ApplicationServices.CreateScope())
+ {
+ var appDbContext = scope.ServiceProvider.GetRequiredService();
+ appDbContext.Database.EnsureCreated();
+ }
+
+ app.UseRouting();
+
+ using (CodeTimingSessionManager.Current.Measure("Initialize JSON:API (startup)"))
+ {
+ app.UseJsonApi();
+ }
+
+ app.UseEndpoints(endpoints => endpoints.MapControllers());
+ }
+
+ if (CodeTimingSessionManager.IsEnabled)
{
- var appDbContext = scope.ServiceProvider.GetRequiredService();
- appDbContext.Database.EnsureCreated();
+ string timingResults = CodeTimingSessionManager.Current.GetResults();
+ logger.LogInformation($"Measurement results for application startup:{Environment.NewLine}{timingResults}");
}
- app.UseRouting();
- app.UseJsonApi();
- app.UseEndpoints(endpoints => endpoints.MapControllers());
+ _codeTimingSession.Dispose();
}
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/appsettings.json b/src/Examples/JsonApiDotNetCoreExample/appsettings.json
index 5d13a80bef..ec2ea30102 100644
--- a/src/Examples/JsonApiDotNetCoreExample/appsettings.json
+++ b/src/Examples/JsonApiDotNetCoreExample/appsettings.json
@@ -7,7 +7,9 @@
"Default": "Warning",
"Microsoft.Hosting.Lifetime": "Warning",
"Microsoft.EntityFrameworkCore.Update": "Critical",
- "Microsoft.EntityFrameworkCore.Database.Command": "Critical"
+ "Microsoft.EntityFrameworkCore.Database.Command": "Critical",
+ "JsonApiDotNetCore.Middleware.JsonApiMiddleware": "Information",
+ "JsonApiDotNetCoreExample": "Information"
}
},
"AllowedHosts": "*"
diff --git a/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs
new file mode 100644
index 0000000000..2cfca080a1
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/AspNetCodeTimerSession.cs
@@ -0,0 +1,83 @@
+using System;
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.Http;
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// Code timing session management intended for use in ASP.NET Web Applications. Uses to isolate concurrent requests.
+ /// Can be used with async/wait, but it cannot distinguish between concurrently running threads within a single HTTP request, so you'll need to pass an
+ /// instance through the entire call chain in that case.
+ ///
+ [PublicAPI]
+ public sealed class AspNetCodeTimerSession : ICodeTimerSession
+ {
+ private const string HttpContextItemKey = "CascadingCodeTimer:Session";
+
+ private readonly HttpContext _httpContext;
+ private readonly IHttpContextAccessor _httpContextAccessor;
+
+ public ICodeTimer CodeTimer
+ {
+ get
+ {
+ HttpContext httpContext = GetHttpContext();
+ var codeTimer = (ICodeTimer)httpContext.Items[HttpContextItemKey];
+
+ if (codeTimer == null)
+ {
+ codeTimer = new CascadingCodeTimer();
+ httpContext.Items[HttpContextItemKey] = codeTimer;
+ }
+
+ return codeTimer;
+ }
+ }
+
+ public event EventHandler Disposed;
+
+ public AspNetCodeTimerSession(IHttpContextAccessor httpContextAccessor)
+ {
+ ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor));
+
+ _httpContextAccessor = httpContextAccessor;
+ }
+
+ public AspNetCodeTimerSession(HttpContext httpContext)
+ {
+ ArgumentGuard.NotNull(httpContext, nameof(httpContext));
+
+ _httpContext = httpContext;
+ }
+
+ public void Dispose()
+ {
+ HttpContext httpContext = TryGetHttpContext();
+ var codeTimer = (ICodeTimer)httpContext?.Items[HttpContextItemKey];
+
+ if (codeTimer != null)
+ {
+ codeTimer.Dispose();
+ httpContext.Items[HttpContextItemKey] = null;
+ }
+
+ OnDisposed();
+ }
+
+ private void OnDisposed()
+ {
+ Disposed?.Invoke(this, EventArgs.Empty);
+ }
+
+ private HttpContext GetHttpContext()
+ {
+ HttpContext httpContext = TryGetHttpContext();
+ return httpContext ?? throw new InvalidOperationException("An active HTTP request is required.");
+ }
+
+ private HttpContext TryGetHttpContext()
+ {
+ return _httpContext ?? _httpContextAccessor?.HttpContext;
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs
new file mode 100644
index 0000000000..5da5a33b01
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/CascadingCodeTimer.cs
@@ -0,0 +1,282 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Linq;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// Records execution times for nested code blocks.
+ ///
+ internal sealed class CascadingCodeTimer : ICodeTimer
+ {
+ private readonly Stopwatch _stopwatch = new();
+ private readonly Stack _activeScopeStack = new();
+ private readonly List _completedScopes = new();
+
+ static CascadingCodeTimer()
+ {
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ // Be default, measurements using Stopwatch can differ 25%-30% on the same function on the same computer.
+ // The steps below ensure to get an accuracy of 0.1%-0.2%. With this accuracy, algorithms can be tested and compared.
+ // https://www.codeproject.com/Articles/61964/Performance-Tests-Precise-Run-Time-Measurements-wi
+
+ // The most important thing is to prevent switching between CPU cores or processors. Switching dismisses the cache, etc. and has a huge performance impact on the test.
+ Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2);
+
+ // To get the CPU core more exclusively, we must prevent that other processes can use this CPU core. We set our process priority to achieve this.
+ // Note we should NOT set the thread priority, because async/await usage makes the code jump between pooled threads (depending on Synchronization Context).
+ Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
+ }
+ }
+
+ ///
+ public IDisposable Measure(string name, bool excludeInRelativeCost = false)
+ {
+ MeasureScope childScope = CreateChildScope(name, excludeInRelativeCost);
+ _activeScopeStack.Push(childScope);
+
+ return childScope;
+ }
+
+ private MeasureScope CreateChildScope(string name, bool excludeInRelativeCost)
+ {
+ if (_activeScopeStack.TryPeek(out MeasureScope topScope))
+ {
+ return topScope.SpawnChild(this, name, excludeInRelativeCost);
+ }
+
+ return new MeasureScope(this, name, excludeInRelativeCost);
+ }
+
+ private void Close(MeasureScope scope)
+ {
+ if (!_activeScopeStack.TryPeek(out MeasureScope topScope) || topScope != scope)
+ {
+ throw new InvalidOperationException($"Scope '{scope.Name}' cannot be disposed at this time, because it is not the currently active scope.");
+ }
+
+ _activeScopeStack.Pop();
+
+ if (!_activeScopeStack.Any())
+ {
+ _completedScopes.Add(scope);
+ }
+ }
+
+ ///
+ public string GetResults()
+ {
+ int paddingLength = GetPaddingLength();
+
+ var builder = new StringBuilder();
+ WriteResult(builder, paddingLength);
+
+ return builder.ToString();
+ }
+
+ private int GetPaddingLength()
+ {
+ int maxLength = 0;
+
+ foreach (MeasureScope scope in _completedScopes)
+ {
+ int nextLength = scope.GetPaddingLength();
+ maxLength = Math.Max(maxLength, nextLength);
+ }
+
+ if (_activeScopeStack.Any())
+ {
+ MeasureScope scope = _activeScopeStack.Peek();
+ int nextLength = scope.GetPaddingLength();
+ maxLength = Math.Max(maxLength, nextLength);
+ }
+
+ return maxLength + 3;
+ }
+
+ private void WriteResult(StringBuilder builder, int paddingLength)
+ {
+ foreach (MeasureScope scope in _completedScopes)
+ {
+ scope.WriteResult(builder, 0, paddingLength);
+ }
+
+ if (_activeScopeStack.Any())
+ {
+ MeasureScope scope = _activeScopeStack.Peek();
+ scope.WriteResult(builder, 0, paddingLength);
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_stopwatch.IsRunning)
+ {
+ _stopwatch.Stop();
+ }
+
+ _completedScopes.Clear();
+ _activeScopeStack.Clear();
+ }
+
+ private sealed class MeasureScope : IDisposable
+ {
+ private readonly CascadingCodeTimer _owner;
+ private readonly IList _children = new List();
+ private readonly bool _excludeInRelativeCost;
+ private readonly TimeSpan _startedAt;
+ private TimeSpan? _stoppedAt;
+
+ public string Name { get; }
+
+ public MeasureScope(CascadingCodeTimer owner, string name, bool excludeInRelativeCost)
+ {
+ _owner = owner;
+ _excludeInRelativeCost = excludeInRelativeCost;
+ Name = name;
+
+ EnsureRunning();
+ _startedAt = owner._stopwatch.Elapsed;
+ }
+
+ private void EnsureRunning()
+ {
+ if (!_owner._stopwatch.IsRunning)
+ {
+ _owner._stopwatch.Start();
+ }
+ }
+
+ public MeasureScope SpawnChild(CascadingCodeTimer owner, string name, bool excludeInRelativeCost)
+ {
+ var childScope = new MeasureScope(owner, name, excludeInRelativeCost);
+ _children.Add(childScope);
+ return childScope;
+ }
+
+ public int GetPaddingLength()
+ {
+ return GetPaddingLength(0);
+ }
+
+ private int GetPaddingLength(int indent)
+ {
+ int selfLength = indent * 2 + Name.Length;
+ int maxChildrenLength = 0;
+
+ foreach (MeasureScope child in _children)
+ {
+ int nextLength = child.GetPaddingLength(indent + 1);
+ maxChildrenLength = Math.Max(nextLength, maxChildrenLength);
+ }
+
+ return Math.Max(selfLength, maxChildrenLength);
+ }
+
+ private TimeSpan GetElapsedInSelf()
+ {
+ return GetElapsedInTotal() - GetElapsedInChildren();
+ }
+
+ private TimeSpan GetElapsedInTotal()
+ {
+ TimeSpan stoppedAt = _stoppedAt ?? _owner._stopwatch.Elapsed;
+ return stoppedAt - _startedAt;
+ }
+
+ private TimeSpan GetElapsedInChildren()
+ {
+ TimeSpan elapsedInChildren = TimeSpan.Zero;
+
+ foreach (MeasureScope childScope in _children)
+ {
+ elapsedInChildren += childScope.GetElapsedInTotal();
+ }
+
+ return elapsedInChildren;
+ }
+
+ private TimeSpan GetSkippedInTotal()
+ {
+ TimeSpan skippedInSelf = _excludeInRelativeCost ? GetElapsedInSelf() : TimeSpan.Zero;
+ TimeSpan skippedInChildren = GetSkippedInChildren();
+
+ return skippedInSelf + skippedInChildren;
+ }
+
+ private TimeSpan GetSkippedInChildren()
+ {
+ TimeSpan skippedInChildren = TimeSpan.Zero;
+
+ foreach (MeasureScope childScope in _children)
+ {
+ skippedInChildren += childScope.GetSkippedInTotal();
+ }
+
+ return skippedInChildren;
+ }
+
+ public void WriteResult(StringBuilder builder, int indent, int paddingLength)
+ {
+ TimeSpan timeElapsedGlobal = GetElapsedInTotal() - GetSkippedInTotal();
+ WriteResult(builder, indent, timeElapsedGlobal, paddingLength);
+ }
+
+ private void WriteResult(StringBuilder builder, int indent, TimeSpan timeElapsedGlobal, int paddingLength)
+ {
+ TimeSpan timeElapsedInSelf = GetElapsedInSelf();
+ double scaleElapsedInSelf = timeElapsedGlobal != TimeSpan.Zero ? timeElapsedInSelf / timeElapsedGlobal : 0;
+
+ WriteIndent(builder, indent);
+ builder.Append(Name);
+ WritePadding(builder, indent, paddingLength);
+ builder.AppendFormat(CultureInfo.InvariantCulture, "{0,19:G}", timeElapsedInSelf);
+
+ if (!_excludeInRelativeCost)
+ {
+ builder.Append(" ... ");
+ builder.AppendFormat(CultureInfo.InvariantCulture, "{0,7:#0.00%}", scaleElapsedInSelf);
+ }
+
+ if (_stoppedAt == null)
+ {
+ builder.Append(" (active)");
+ }
+
+ builder.AppendLine();
+
+ foreach (MeasureScope child in _children)
+ {
+ child.WriteResult(builder, indent + 1, timeElapsedGlobal, paddingLength);
+ }
+ }
+
+ private static void WriteIndent(StringBuilder builder, int indent)
+ {
+ builder.Append(new string(' ', indent * 2));
+ }
+
+ private void WritePadding(StringBuilder builder, int indent, int paddingLength)
+ {
+ string padding = new('.', paddingLength - Name.Length - indent * 2);
+ builder.Append(' ');
+ builder.Append(padding);
+ builder.Append(' ');
+ }
+
+ public void Dispose()
+ {
+ if (_stoppedAt == null)
+ {
+ _stoppedAt = _owner._stopwatch.Elapsed;
+ _owner.Close(this);
+ }
+ }
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs
new file mode 100644
index 0000000000..9160791f87
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/CodeTimingSessionManager.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Linq;
+
+#pragma warning disable AV1008 // Class should not be static
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// Provides access to the "current" measurement, which removes the need to pass along a instance through the entire
+ /// call chain.
+ ///
+ public static class CodeTimingSessionManager
+ {
+ public static readonly bool IsEnabled;
+ private static ICodeTimerSession _session;
+
+ public static ICodeTimer Current
+ {
+ get
+ {
+ if (!IsEnabled)
+ {
+ return DisabledCodeTimer.Instance;
+ }
+
+ AssertHasActiveSession();
+
+ return _session.CodeTimer;
+ }
+ }
+
+ static CodeTimingSessionManager()
+ {
+#if DEBUG
+ IsEnabled = !IsRunningInTest();
+#else
+ IsEnabled = false;
+#endif
+ }
+
+ // ReSharper disable once UnusedMember.Local
+ private static bool IsRunningInTest()
+ {
+ const string testAssemblyName = "xunit.core";
+
+ return AppDomain.CurrentDomain.GetAssemblies().Any(assembly =>
+ assembly.FullName != null && assembly.FullName.StartsWith(testAssemblyName, StringComparison.Ordinal));
+ }
+
+ private static void AssertHasActiveSession()
+ {
+ if (_session == null)
+ {
+ throw new InvalidOperationException($"Call {nameof(Capture)} before accessing the current session.");
+ }
+ }
+
+ public static void Capture(ICodeTimerSession session)
+ {
+ ArgumentGuard.NotNull(session, nameof(session));
+
+ AssertNoActiveSession();
+
+ if (IsEnabled)
+ {
+ session.Disposed += SessionOnDisposed;
+ _session = session;
+ }
+ }
+
+ private static void AssertNoActiveSession()
+ {
+ if (_session != null)
+ {
+ throw new InvalidOperationException("Sessions cannot be nested. Dispose the current session first.");
+ }
+ }
+
+ private static void SessionOnDisposed(object sender, EventArgs args)
+ {
+ if (_session != null)
+ {
+ _session.Disposed -= SessionOnDisposed;
+ _session = null;
+ }
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs
new file mode 100644
index 0000000000..b56eeab962
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/DefaultCodeTimerSession.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Threading;
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// General code timing session management. Can be used with async/wait, but it cannot distinguish between concurrently running threads, so you'll need
+ /// to pass an instance through the entire call chain in that case.
+ ///
+ public sealed class DefaultCodeTimerSession : ICodeTimerSession
+ {
+ private readonly AsyncLocal _codeTimerInContext = new();
+
+ public ICodeTimer CodeTimer
+ {
+ get
+ {
+ AssertNotDisposed();
+
+ return _codeTimerInContext.Value;
+ }
+ }
+
+ public event EventHandler Disposed;
+
+ public DefaultCodeTimerSession()
+ {
+ _codeTimerInContext.Value = new CascadingCodeTimer();
+ }
+
+ private void AssertNotDisposed()
+ {
+ if (_codeTimerInContext.Value == null)
+ {
+ throw new ObjectDisposedException(nameof(DefaultCodeTimerSession));
+ }
+ }
+
+ public void Dispose()
+ {
+ _codeTimerInContext.Value?.Dispose();
+ _codeTimerInContext.Value = null;
+
+ OnDisposed();
+ }
+
+ private void OnDisposed()
+ {
+ Disposed?.Invoke(this, EventArgs.Empty);
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs
new file mode 100644
index 0000000000..1739ed3e81
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/DisabledCodeTimer.cs
@@ -0,0 +1,30 @@
+using System;
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// Doesn't record anything. Intended for Release builds and to not break existing tests.
+ ///
+ internal sealed class DisabledCodeTimer : ICodeTimer
+ {
+ public static readonly DisabledCodeTimer Instance = new();
+
+ private DisabledCodeTimer()
+ {
+ }
+
+ public IDisposable Measure(string name, bool excludeInRelativeCost = false)
+ {
+ return this;
+ }
+
+ public string GetResults()
+ {
+ return string.Empty;
+ }
+
+ public void Dispose()
+ {
+ }
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs
new file mode 100644
index 0000000000..9e8abfe9e1
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimer.cs
@@ -0,0 +1,27 @@
+using System;
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// Records execution times for code blocks.
+ ///
+ public interface ICodeTimer : IDisposable
+ {
+ ///
+ /// Starts recording the duration of a code block. Wrap this call in a using statement, so the recording stops when the return value goes out of
+ /// scope.
+ ///
+ ///
+ /// Description of what is being recorded.
+ ///
+ ///
+ /// When set, indicates to exclude this measurement in calculated percentages. false by default.
+ ///
+ IDisposable Measure(string name, bool excludeInRelativeCost = false);
+
+ ///
+ /// Returns intermediate or final results.
+ ///
+ string GetResults();
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs b/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs
new file mode 100644
index 0000000000..5da473c38d
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/ICodeTimerSession.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ ///
+ /// Removes the need to pass along a instance through the entire call chain when using code timing.
+ ///
+ public interface ICodeTimerSession : IDisposable
+ {
+ ICodeTimer CodeTimer { get; }
+
+ event EventHandler Disposed;
+ }
+}
diff --git a/src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs b/src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs
new file mode 100644
index 0000000000..4fd42b2e30
--- /dev/null
+++ b/src/JsonApiDotNetCore/Diagnostics/MeasurementSettings.cs
@@ -0,0 +1,10 @@
+#pragma warning disable AV1008 // Class should not be static
+
+namespace JsonApiDotNetCore.Diagnostics
+{
+ internal static class MeasurementSettings
+ {
+ public static readonly bool ExcludeDatabaseInPercentages = bool.Parse(bool.TrueString);
+ public static readonly bool ExcludeJsonSerializationInPercentages = bool.Parse(bool.FalseString);
+ }
+}
diff --git a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
index 3ad6355d2e..efc3613979 100644
--- a/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
+++ b/src/JsonApiDotNetCore/Middleware/JsonApiMiddleware.cs
@@ -6,13 +6,16 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Objects;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Controllers;
using Microsoft.AspNetCore.Routing;
+using Microsoft.Extensions.Logging;
using Microsoft.Net.Http.Headers;
using Newtonsoft.Json;
@@ -29,57 +32,74 @@ public sealed class JsonApiMiddleware
private readonly RequestDelegate _next;
- public JsonApiMiddleware(RequestDelegate next)
+ public JsonApiMiddleware(RequestDelegate next, IHttpContextAccessor httpContextAccessor)
{
_next = next;
+
+ var session = new AspNetCodeTimerSession(httpContextAccessor);
+ CodeTimingSessionManager.Capture(session);
}
public async Task InvokeAsync(HttpContext httpContext, IControllerResourceMapping controllerResourceMapping, IJsonApiOptions options,
- IJsonApiRequest request, IResourceContextProvider resourceContextProvider)
+ IJsonApiRequest request, IResourceContextProvider resourceContextProvider, ILogger logger)
{
ArgumentGuard.NotNull(httpContext, nameof(httpContext));
ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping));
ArgumentGuard.NotNull(options, nameof(options));
ArgumentGuard.NotNull(request, nameof(request));
ArgumentGuard.NotNull(resourceContextProvider, nameof(resourceContextProvider));
+ ArgumentGuard.NotNull(logger, nameof(logger));
- if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings))
- {
- return;
- }
-
- RouteValueDictionary routeValues = httpContext.GetRouteData().Values;
- ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider);
-
- if (primaryResourceContext != null)
+ using (CodeTimingSessionManager.Current.Measure("JSON:API middleware"))
{
- if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) ||
- !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings))
+ if (!await ValidateIfMatchHeaderAsync(httpContext, options.SerializerSettings))
{
return;
}
- SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request);
+ RouteValueDictionary routeValues = httpContext.GetRouteData().Values;
+ ResourceContext primaryResourceContext = CreatePrimaryResourceContext(httpContext, controllerResourceMapping, resourceContextProvider);
- httpContext.RegisterJsonApiRequest();
- }
- else if (IsRouteForOperations(routeValues))
- {
- if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) ||
- !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings))
+ if (primaryResourceContext != null)
{
- return;
+ if (!await ValidateContentTypeHeaderAsync(HeaderConstants.MediaType, httpContext, options.SerializerSettings) ||
+ !await ValidateAcceptHeaderAsync(MediaType, httpContext, options.SerializerSettings))
+ {
+ return;
+ }
+
+ SetupResourceRequest((JsonApiRequest)request, primaryResourceContext, routeValues, resourceContextProvider, httpContext.Request);
+
+ httpContext.RegisterJsonApiRequest();
}
+ else if (IsRouteForOperations(routeValues))
+ {
+ if (!await ValidateContentTypeHeaderAsync(HeaderConstants.AtomicOperationsMediaType, httpContext, options.SerializerSettings) ||
+ !await ValidateAcceptHeaderAsync(AtomicOperationsMediaType, httpContext, options.SerializerSettings))
+ {
+ return;
+ }
- SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request);
+ SetupOperationsRequest((JsonApiRequest)request, options, httpContext.Request);
- httpContext.RegisterJsonApiRequest();
- }
+ httpContext.RegisterJsonApiRequest();
+ }
- // Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394
- httpContext.Features.Set(new FixedQueryFeature(httpContext.Features));
+ // Workaround for bug https://github.com/dotnet/aspnetcore/issues/33394
+ httpContext.Features.Set(new FixedQueryFeature(httpContext.Features));
- await _next(httpContext);
+ using (CodeTimingSessionManager.Current.Measure("Subsequent middleware"))
+ {
+ await _next(httpContext);
+ }
+ }
+
+ if (CodeTimingSessionManager.IsEnabled)
+ {
+ string timingResults = CodeTimingSessionManager.Current.GetResults();
+ string url = httpContext.Request.GetDisplayUrl();
+ logger.LogInformation($"Measurement results for {httpContext.Request.Method} {url}:{Environment.NewLine}{timingResults}");
+ }
}
private async Task ValidateIfMatchHeaderAsync(HttpContext httpContext, JsonSerializerSettings serializerSettings)
diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
index a61e9c6555..236a3d80f6 100644
--- a/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
+++ b/src/JsonApiDotNetCore/Queries/Internal/QueryLayerComposer.cs
@@ -4,6 +4,7 @@
using System.Linq;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Queries.Expressions;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
@@ -83,6 +84,8 @@ public QueryLayer ComposeFromConstraints(ResourceContext requestResource)
private QueryLayer ComposeTopLayer(IEnumerable constraints, ResourceContext resourceContext)
{
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Top-level query composition");
+
// @formatter:wrap_chained_method_calls chop_always
// @formatter:keep_existing_linebreaks true
@@ -113,6 +116,8 @@ private QueryLayer ComposeTopLayer(IEnumerable constraints, R
private IncludeExpression ComposeChildren(QueryLayer topLayer, ICollection constraints)
{
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Nested query composition");
+
// @formatter:wrap_chained_method_calls chop_always
// @formatter:keep_existing_linebreaks true
diff --git a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs
index fe4064a9c2..68f9cc4a6c 100644
--- a/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs
+++ b/src/JsonApiDotNetCore/QueryStrings/Internal/QueryStringReader.cs
@@ -1,8 +1,10 @@
+using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Controllers.Annotations;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Errors;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
@@ -35,6 +37,8 @@ public QueryStringReader(IJsonApiOptions options, IRequestQueryStringAccessor qu
///
public virtual void ReadAll(DisableQueryStringAttribute disableQueryStringAttribute)
{
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Parse query string");
+
DisableQueryStringAttribute disableQueryStringAttributeNotNull = disableQueryStringAttribute ?? DisableQueryStringAttribute.Empty;
foreach ((string parameterName, StringValues parameterValue) in _queryStringAccessor.Query)
diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs
index 2daa011d82..a86d002b51 100644
--- a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs
+++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreRepository.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
@@ -71,8 +72,15 @@ public virtual async Task> GetAsync(QueryLayer la
ArgumentGuard.NotNull(layer, nameof(layer));
- IQueryable query = ApplyQueryLayer(layer);
- return await query.ToListAsync(cancellationToken);
+ using (CodeTimingSessionManager.Current.Measure("Repository - Get resource(s)"))
+ {
+ IQueryable query = ApplyQueryLayer(layer);
+
+ using (CodeTimingSessionManager.Current.Measure("Execute SQL (data)", MeasurementSettings.ExcludeDatabaseInPercentages))
+ {
+ return await query.ToListAsync(cancellationToken);
+ }
+ }
}
///
@@ -83,15 +91,22 @@ public virtual async Task CountAsync(FilterExpression topFilter, Cancellati
topFilter
});
- ResourceContext resourceContext = _resourceGraph.GetResourceContext();
-
- var layer = new QueryLayer(resourceContext)
+ using (CodeTimingSessionManager.Current.Measure("Repository - Count resources"))
{
- Filter = topFilter
- };
+ ResourceContext resourceContext = _resourceGraph.GetResourceContext();
+
+ var layer = new QueryLayer(resourceContext)
+ {
+ Filter = topFilter
+ };
- IQueryable query = ApplyQueryLayer(layer);
- return await query.CountAsync(cancellationToken);
+ IQueryable query = ApplyQueryLayer(layer);
+
+ using (CodeTimingSessionManager.Current.Measure("Execute SQL (count)", MeasurementSettings.ExcludeDatabaseInPercentages))
+ {
+ return await query.CountAsync(cancellationToken);
+ }
+ }
}
protected virtual IQueryable ApplyQueryLayer(QueryLayer layer)
@@ -103,33 +118,40 @@ protected virtual IQueryable ApplyQueryLayer(QueryLayer layer)
ArgumentGuard.NotNull(layer, nameof(layer));
- IQueryable source = GetAll();
+ using (CodeTimingSessionManager.Current.Measure("Convert QueryLayer to System.Expression"))
+ {
+ IQueryable source = GetAll();
- // @formatter:wrap_chained_method_calls chop_always
- // @formatter:keep_existing_linebreaks true
+ // @formatter:wrap_chained_method_calls chop_always
+ // @formatter:keep_existing_linebreaks true
- QueryableHandlerExpression[] queryableHandlers = _constraintProviders
- .SelectMany(provider => provider.GetConstraints())
- .Where(expressionInScope => expressionInScope.Scope == null)
- .Select(expressionInScope => expressionInScope.Expression)
- .OfType()
- .ToArray();
+ QueryableHandlerExpression[] queryableHandlers = _constraintProviders
+ .SelectMany(provider => provider.GetConstraints())
+ .Where(expressionInScope => expressionInScope.Scope == null)
+ .Select(expressionInScope => expressionInScope.Expression)
+ .OfType()
+ .ToArray();
- // @formatter:keep_existing_linebreaks restore
- // @formatter:wrap_chained_method_calls restore
+ // @formatter:keep_existing_linebreaks restore
+ // @formatter:wrap_chained_method_calls restore
- foreach (QueryableHandlerExpression queryableHandler in queryableHandlers)
- {
- source = queryableHandler.Apply(source);
- }
+ foreach (QueryableHandlerExpression queryableHandler in queryableHandlers)
+ {
+ source = queryableHandler.Apply(source);
+ }
- var nameFactory = new LambdaParameterNameFactory();
+ var nameFactory = new LambdaParameterNameFactory();
- var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph,
- _dbContext.Model);
+ var builder = new QueryableBuilder(source.Expression, source.ElementType, typeof(Queryable), nameFactory, _resourceFactory, _resourceGraph,
+ _dbContext.Model);
- Expression expression = builder.ApplyQuery(layer);
- return source.Provider.CreateQuery(expression);
+ Expression expression = builder.ApplyQuery(layer);
+
+ using (CodeTimingSessionManager.Current.Measure("Convert System.Expression to IQueryable"))
+ {
+ return source.Provider.CreateQuery(expression);
+ }
+ }
}
protected virtual IQueryable GetAll()
@@ -158,6 +180,8 @@ public virtual async Task CreateAsync(TResource resourceFromRequest, TResource r
ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest));
ArgumentGuard.NotNull(resourceForDatabase, nameof(resourceForDatabase));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Create resource");
+
using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext);
foreach (RelationshipAttribute relationship in _targetedFields.Relationships)
@@ -210,6 +234,8 @@ await _resourceDefinitionAccessor.OnSetToManyRelationshipAsync(leftResource, has
///
public virtual async Task GetForUpdateAsync(QueryLayer queryLayer, CancellationToken cancellationToken)
{
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Get resource for update");
+
IReadOnlyCollection resources = await GetAsync(queryLayer, cancellationToken);
return resources.FirstOrDefault();
}
@@ -226,6 +252,8 @@ public virtual async Task UpdateAsync(TResource resourceFromRequest, TResource r
ArgumentGuard.NotNull(resourceFromRequest, nameof(resourceFromRequest));
ArgumentGuard.NotNull(resourceFromDatabase, nameof(resourceFromDatabase));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Update resource");
+
using var collector = new PlaceholderResourceCollector(_resourceFactory, _dbContext);
foreach (RelationshipAttribute relationship in _targetedFields.Relationships)
@@ -295,6 +323,8 @@ public virtual async Task DeleteAsync(TId id, CancellationToken cancellationToke
id
});
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Delete resource");
+
// This enables OnWritingAsync() to fetch the resource, which adds it to the change tracker.
// If so, we'll reuse the tracked resource instead of a placeholder resource.
var emptyResource = _resourceFactory.CreateInstance();
@@ -374,6 +404,8 @@ public virtual async Task SetRelationshipAsync(TResource leftResource, object ri
rightValue
});
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Set relationship");
+
RelationshipAttribute relationship = _targetedFields.Relationships.Single();
object rightValueEvaluated =
@@ -402,6 +434,8 @@ public virtual async Task AddToToManyRelationshipAsync(TId leftId, ISet(leftId, relationship, rightResourceIds, cancellationToken);
@@ -433,6 +467,8 @@ public virtual async Task RemoveFromToManyRelationshipAsync(TResource leftResour
ArgumentGuard.NotNull(rightResourceIds, nameof(rightResourceIds));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Repository - Remove from to-many relationship");
+
var relationship = (HasManyAttribute)_targetedFields.Relationships.Single();
await _resourceDefinitionAccessor.OnRemoveFromRelationshipAsync(leftResource, relationship, rightResourceIds, cancellationToken);
@@ -502,6 +538,8 @@ protected virtual async Task SaveChangesAsync(CancellationToken cancellationToke
try
{
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Persist EF Core changes", MeasurementSettings.ExcludeDatabaseInPercentages);
+
await _dbContext.SaveChangesAsync(cancellationToken);
}
catch (Exception exception) when (exception is DbUpdateException || exception is InvalidOperationException)
diff --git a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs
index 8cf2653f9e..b329392c9f 100644
--- a/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs
+++ b/src/JsonApiDotNetCore/Serialization/BaseDeserializer.cs
@@ -5,6 +5,7 @@
using System.Linq;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Resources.Internal;
@@ -61,19 +62,28 @@ protected object DeserializeBody(string body)
{
ArgumentGuard.NotNullNorEmpty(body, nameof(body));
- JToken bodyJToken = LoadJToken(body);
- Document = bodyJToken.ToObject();
+ using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages))
+ {
+ JToken bodyJToken = LoadJToken(body);
+ Document = bodyJToken.ToObject();
+ }
if (Document != null)
{
if (Document.IsManyData)
{
- return Document.ManyData.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance);
+ using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (list)"))
+ {
+ return Document.ManyData.Select(ParseResourceObject).ToHashSet(IdentifiableComparer.Instance);
+ }
}
if (Document.SingleData != null)
{
- return ParseResourceObject(Document.SingleData);
+ using (CodeTimingSessionManager.Current.Measure("Deserializer.Build (single)"))
+ {
+ return ParseResourceObject(Document.SingleData);
+ }
}
}
diff --git a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs
index 33ebf99f64..8519d20fd2 100644
--- a/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs
+++ b/src/JsonApiDotNetCore/Serialization/BaseSerializer.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using JsonApiDotNetCore.Serialization.Building;
@@ -43,6 +44,8 @@ protected BaseSerializer(IResourceObjectBuilder resourceObjectBuilder)
protected Document Build(IIdentifiable resource, IReadOnlyCollection attributes,
IReadOnlyCollection relationships)
{
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (single)");
+
if (resource == null)
{
return new Document();
@@ -75,6 +78,8 @@ protected Document Build(IReadOnlyCollection resources, IReadOnly
{
ArgumentGuard.NotNull(resources, nameof(resources));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Serializer.Build (list)");
+
var data = new List();
foreach (IIdentifiable resource in resources)
@@ -92,6 +97,8 @@ protected string SerializeObject(object value, JsonSerializerSettings defaultSet
{
ArgumentGuard.NotNull(defaultSettings, nameof(defaultSettings));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Newtonsoft.Serialize", MeasurementSettings.ExcludeJsonSerializationInPercentages);
+
var serializer = JsonSerializer.CreateDefault(defaultSettings);
changeSerializer?.Invoke(serializer);
diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs
index d0dac14349..d9e861d4a2 100644
--- a/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs
+++ b/src/JsonApiDotNetCore/Serialization/JsonApiReader.cs
@@ -8,6 +8,7 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
@@ -47,6 +48,8 @@ public async Task ReadAsync(InputFormatterContext context)
{
ArgumentGuard.NotNull(context, nameof(context));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Read request body");
+
string body = await GetRequestBodyAsync(context.HttpContext.Request.Body);
string url = context.HttpContext.Request.GetEncodedUrl();
diff --git a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs
index fe5463def9..08c81da0f5 100644
--- a/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs
+++ b/src/JsonApiDotNetCore/Serialization/JsonApiWriter.cs
@@ -6,6 +6,7 @@
using System.Text;
using System.Threading.Tasks;
using JetBrains.Annotations;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Serialization.Objects;
@@ -47,6 +48,8 @@ public async Task WriteAsync(OutputFormatterWriteContext context)
{
ArgumentGuard.NotNull(context, nameof(context));
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Write response body");
+
HttpRequest request = context.HttpContext.Request;
HttpResponse response = context.HttpContext.Response;
diff --git a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs
index ab372fea3f..8220936b20 100644
--- a/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs
+++ b/src/JsonApiDotNetCore/Serialization/RequestDeserializer.cs
@@ -6,6 +6,7 @@
using Humanizer;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
@@ -74,8 +75,13 @@ public object Deserialize(string body)
private object DeserializeOperationsDocument(string body)
{
- JToken bodyToken = LoadJToken(body);
- var document = bodyToken.ToObject();
+ AtomicOperationsDocument document;
+
+ using (CodeTimingSessionManager.Current.Measure("Newtonsoft.Deserialize", MeasurementSettings.ExcludeJsonSerializationInPercentages))
+ {
+ JToken bodyToken = LoadJToken(body);
+ document = bodyToken.ToObject();
+ }
if ((document?.Operations).IsNullOrEmpty())
{
diff --git a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
index 8ae06cd907..0c3d9c521a 100644
--- a/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
+++ b/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs
@@ -7,6 +7,7 @@
using System.Threading.Tasks;
using JetBrains.Annotations;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Diagnostics;
using JsonApiDotNetCore.Errors;
using JsonApiDotNetCore.Middleware;
using JsonApiDotNetCore.Queries;
@@ -61,6 +62,8 @@ public virtual async Task> GetAsync(CancellationT
{
_traceWriter.LogMethodStart();
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get resources");
+
if (_options.IncludeTotalResourceCount)
{
FilterExpression topFilter = _queryLayerComposer.GetTopFilterFromConstraints(_request.PrimaryResource);
@@ -91,6 +94,8 @@ public virtual async Task GetAsync(TId id, CancellationToken cancella
id
});
+ using IDisposable _ = CodeTimingSessionManager.Current.Measure("Service - Get single resource");
+
return await GetPrimaryResourceByIdAsync(id, TopFieldSelection.PreserveExisting, cancellationToken);
}
@@ -103,6 +108,8 @@ public virtual async Task