1- using Dfe . ManageFreeSchoolProjects . API . Contracts . Project . Summary ;
1+ using System . Diagnostics ;
2+ using Dfe . ManageFreeSchoolProjects . API . Contracts . Project . Summary ;
3+ using Dfe . ManageFreeSchoolProjects . API . Diagnostics ;
24using Dfe . ManageFreeSchoolProjects . API . Extensions ;
35using Dfe . ManageFreeSchoolProjects . API . UseCases . Project ;
46using Dfe . ManageFreeSchoolProjects . Data ;
@@ -17,16 +19,31 @@ public record GetProjectSummaryByUserParameters
1719 public string ProjectManagedByEmail { get ; set ; }
1820 }
1921
20- public class GetProjectSummaryByUserService ( MfspContext context ) : IGetProjectSummaryByUserService
22+ public class GetProjectSummaryByUserService (
23+ MfspContext context ,
24+ IProcessWarmupState processWarmupState ,
25+ IHttpContextAccessor httpContextAccessor ,
26+ ILogger < GetProjectSummaryByUserService > logger ) : IGetProjectSummaryByUserService
2127 {
28+ private const string StageTimingsEventName = "ProjectSummaryByUser.StageTimings" ;
29+
2230 public async Task < ( List < GetProjectSummaryResponse > , int ) > Execute ( GetProjectSummaryByUserParameters parameters )
2331 {
32+ var isFirstBusinessRequestInProcess = ResolveIsFirstBusinessRequest ( ) ;
33+ var totalStopwatch = Stopwatch . StartNew ( ) ;
34+
2435 var query = context . Kpi . AsQueryable ( ) ;
2536
2637 query = ApplyFilters ( query , parameters ) ;
2738
39+ var countStopwatch = Stopwatch . StartNew ( ) ;
2840 var count = await query . CountAsync ( ) ;
41+ countStopwatch . Stop ( ) ;
42+
43+ // Wall-clock through the first EF round-trip (includes model-build/JIT when cold).
44+ var timeToFirstEfQueryMs = totalStopwatch . Elapsed . TotalMilliseconds ;
2945
46+ var toListStopwatch = Stopwatch . StartNew ( ) ;
3047 var projectRecords = await
3148 query
3249 . Select (
@@ -39,7 +56,9 @@ public class GetProjectSummaryByUserService(MfspContext context) : IGetProjectSu
3956 . OrderByDescending ( record => record . Kpi . ProjectStatusProvisionalOpeningDateAgreedWithTrust )
4057 . ThenBy ( record => record . Kpi . ProjectStatusCurrentFreeSchoolName )
4158 . ToListAsync ( ) ;
59+ toListStopwatch . Stop ( ) ;
4260
61+ var mappingStopwatch = Stopwatch . StartNew ( ) ;
4362 var result = projectRecords . Select ( record => new GetProjectSummaryResponse ( )
4463 {
4564 ProjectId = record . Kpi . ProjectStatusProjectId ,
@@ -57,10 +76,73 @@ public class GetProjectSummaryByUserService(MfspContext context) : IGetProjectSu
5776 SchoolType = ProjectMapper . ToSchoolType ( record . Kpi . SchoolDetailsSchoolTypeMainstreamApEtc ) . ToDescription ( ) ,
5877 UpdatedAt = record . PeriodStart
5978 } ) . ToList ( ) ;
79+ mappingStopwatch . Stop ( ) ;
80+
81+ totalStopwatch . Stop ( ) ;
82+
83+ EmitStageTimings (
84+ isFirstBusinessRequestInProcess ,
85+ timeToFirstEfQueryMs ,
86+ countStopwatch . Elapsed . TotalMilliseconds ,
87+ toListStopwatch . Elapsed . TotalMilliseconds ,
88+ mappingStopwatch . Elapsed . TotalMilliseconds ,
89+ result . Count ,
90+ totalStopwatch . Elapsed . TotalMilliseconds ) ;
6091
6192 return ( result , count ) ;
6293 }
6394
95+ private bool ResolveIsFirstBusinessRequest ( )
96+ {
97+ var httpContext = httpContextAccessor . HttpContext ;
98+ if ( httpContext ? . Items . TryGetValue ( ProcessWarmupState . HttpContextItemKey , out var value ) == true
99+ && value is bool markedByMiddleware )
100+ {
101+ return markedByMiddleware ;
102+ }
103+
104+ return processWarmupState . MarkBusinessRequest ( ) ;
105+ }
106+
107+ private void EmitStageTimings (
108+ bool isFirstBusinessRequestInProcess ,
109+ double timeToFirstEfQueryMs ,
110+ double countAsyncDurationMs ,
111+ double toListAsyncDurationMs ,
112+ double mappingDurationMs ,
113+ int rowCount ,
114+ double totalDurationMs )
115+ {
116+ if ( ! logger . IsEnabled ( LogLevel . Information ) )
117+ {
118+ return ;
119+ }
120+
121+ using ( logger . BeginScope ( new Dictionary < string , object >
122+ {
123+ [ "EventName" ] = StageTimingsEventName ,
124+ [ "IsFirstBusinessRequestInProcess" ] = isFirstBusinessRequestInProcess ,
125+ [ "TimeToFirstEfQueryMs" ] = timeToFirstEfQueryMs ,
126+ [ "CountAsyncDurationMs" ] = countAsyncDurationMs ,
127+ [ "ToListAsyncDurationMs" ] = toListAsyncDurationMs ,
128+ [ "MappingDurationMs" ] = mappingDurationMs ,
129+ [ "RowCount" ] = rowCount ,
130+ [ "TotalDurationMs" ] = totalDurationMs
131+ } ) )
132+ {
133+ logger . LogInformation (
134+ "{EventName}: TimeToFirstEfQuery={TimeToFirstEfQueryMs}ms, CountAsync={CountAsyncDurationMs}ms, ToListAsync={ToListAsyncDurationMs}ms, Mapping={MappingDurationMs}ms, RowCount={RowCount}, IsFirstBusinessRequestInProcess={IsFirstBusinessRequestInProcess}, Total={TotalDurationMs}ms" ,
135+ StageTimingsEventName ,
136+ timeToFirstEfQueryMs ,
137+ countAsyncDurationMs ,
138+ toListAsyncDurationMs ,
139+ mappingDurationMs ,
140+ rowCount ,
141+ isFirstBusinessRequestInProcess ,
142+ totalDurationMs ) ;
143+ }
144+ }
145+
64146 private static IQueryable < Kpi > ApplyFilters ( IQueryable < Kpi > query , GetProjectSummaryByUserParameters parameters )
65147 {
66148 query = query . Where ( kpi => parameters . ProjectManagedByEmail == kpi . KeyContactsFsgLeadContactEmail ) ;
0 commit comments