Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ClimateExplorer.Web.Client/Components/Chart/ChartView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace ClimateExplorer.Web.Client.Components.Chart;
using ClimateExplorer.Core.Model;
using ClimateExplorer.Core.ViewModel;
using ClimateExplorer.Web.Client.Components.Common;
using ClimateExplorer.Web.Client.Infrastructure;
using ClimateExplorer.Web.Client.UiModel;
using ClimateExplorer.Web.UiLogic;
using ClimateExplorer.Web.UiModel;
Expand Down Expand Up @@ -346,6 +347,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender)

protected async Task BuildDataSets()
{
using var perf = PerformanceLogScope.Start(Logger!, "ChartView.BuildDataSets", ("pageName", PageName), ("locationId", LocationId), ("seriesCount", ChartSeriesList?.Count ?? 0));
await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.mark", "chart-build-start", new { pageName = PageName, locationId = LocationId, seriesCount = ChartSeriesList?.Count ?? 0 });
// This method is called whenever anything has occurred that may require the chart to
// be re-rendered.
//
Expand Down Expand Up @@ -432,16 +435,23 @@ protected async Task BuildDataSets()

l.LogInformation("Set ChartSeriesWithData after call to RetrieveDataSets(). ChartSeriesWithData now has " + usableChartSeries.Count() + " entries.");

await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.mark", "chart-data-loaded", new { pageName = PageName, locationId = LocationId, seriesCount = ChartSeriesWithData?.Count ?? 0, recordCount = ChartSeriesWithData?.Sum(x => x.SourceDataSet?.DataRecords?.Count ?? 0) ?? 0 });

// Render the series
await RenderChart();

buildDataSetsInProcess = false;

await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.mark", "chart-render-complete", new { pageName = PageName, locationId = LocationId, seriesCount = ChartSeriesWithData?.Count ?? 0 });
await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.measure", "chart-data-load", "chart-build-start", "chart-data-loaded", new { pageName = PageName, locationId = LocationId });
await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.measure", "chart-build-to-render", "chart-build-start", "chart-render-complete", new { pageName = PageName, locationId = LocationId });

l.LogInformation("Leaving");
}

protected async Task RenderChart()
{
using var perf = PerformanceLogScope.Start(Logger!, "ChartView.RenderChart", ("pageName", PageName), ("locationId", LocationId), ("seriesCount", ChartSeriesWithData?.Count ?? 0));
var l = new LogAugmenter(Logger!, "RenderChart");

l.LogInformation("Entering");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ namespace ClimateExplorer.Web.Client.Components.Location;
using ClimateExplorer.Core.ViewModel;
using ClimateExplorer.Web.Client.Components.Common;
using ClimateExplorer.Web.Client.Components.Info;
using ClimateExplorer.Web.Client.Infrastructure;
using ClimateExplorer.Web.Client.Services;
using ClimateExplorer.Web.Client.UiModel;
using ClimateExplorer.Web.UiModel;
Expand Down Expand Up @@ -109,6 +110,7 @@ protected override void OnInitialized()

protected override async Task OnParametersSetAsync()
{
using var perf = PerformanceLogScope.Start(Logger!, "LocationDashboard.OnParametersSetAsync", ("locationId", Location?.Id), ("locationName", Location?.Name));
if (Location == null || DataSetDefinitions == null)
{
RecentObservationsSupported = false;
Expand Down Expand Up @@ -140,6 +142,7 @@ protected override async Task OnParametersSetAsync()

protected override async Task OnAfterRenderAsync(bool firstRender)
{
using var perf = PerformanceLogScope.Start(Logger!, "LocationDashboard.OnAfterRenderAsync", ("firstRender", firstRender), ("locationId", Location?.Id), ("loadStripeData", LoadStripeData));
if (Location == null || DataSetDefinitions == null)
{
return;
Expand All @@ -151,6 +154,8 @@ protected override async Task OnAfterRenderAsync(bool firstRender)

Logger!.LogInformation("Loading data");

await JS!.InvokeVoidAsync("climateExplorerPerformance.mark", "location-dashboard-data-start", new { locationId = Location?.Id, locationName = Location?.Name });

var temperatureAnomaly = await CalculateAnomaly(DataSubstitute.StandardTemperatureDataMatches(), ContainerAggregationFunctions.Mean);
if (temperatureAnomaly != null)
{
Expand Down Expand Up @@ -192,6 +197,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
var recentObservationsSupport = await DataService!.GetRecentObservations(Location!.Id, DataType.TempMax, isLocationSupported: true);
RecentObservationsSupported = recentObservationsSupport.IsSupported;

await JS!.InvokeVoidAsync("climateExplorerPerformance.mark", "location-dashboard-data-loaded", new { locationId = Location?.Id, hasTemperatureAnomaly = temperatureAnomaly != null, recentObservationsSupported = RecentObservationsSupported });
await JS!.InvokeVoidAsync("climateExplorerPerformance.measure", "location-dashboard-data", "location-dashboard-data-start", "location-dashboard-data-loaded", new { locationId = Location?.Id });

StripeLoadingIndicatorVisible = false;
StateHasChanged();

Expand Down
39 changes: 39 additions & 0 deletions ClimateExplorer.Web.Client/Infrastructure/PerformanceLogScope.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
namespace ClimateExplorer.Web.Client.Infrastructure;

using System.Diagnostics;

public sealed class PerformanceLogScope : IDisposable
{
private readonly ILogger logger;
private readonly string name;
private readonly IReadOnlyDictionary<string, object?> context;
private readonly long startTimestamp;
private bool disposed;

private PerformanceLogScope(ILogger logger, string name, IReadOnlyDictionary<string, object?> context)
{
this.logger = logger;
this.name = name;
this.context = context;
startTimestamp = Stopwatch.GetTimestamp();

logger.LogInformation("PerfStart {Name} {@Context}", name, context);
}

public static PerformanceLogScope Start(ILogger logger, string name, params (string Key, object? Value)[] context)
{
return new PerformanceLogScope(logger, name, context.ToDictionary(x => x.Key, x => x.Value));
}

public void Dispose()
{
if (disposed)
{
return;
}

disposed = true;
var elapsedMilliseconds = Stopwatch.GetElapsedTime(startTimestamp).TotalMilliseconds;
logger.LogInformation("PerfEnd {Name} elapsedMs={ElapsedMilliseconds:0.0} {@Context}", name, elapsedMilliseconds, context);
}
}
13 changes: 12 additions & 1 deletion ClimateExplorer.Web.Client/Pages/Index.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ namespace ClimateExplorer.Web.Client.Pages;
using ClimateExplorer.Core.ViewModel;
using ClimateExplorer.Web.Client.Components.Common;
using ClimateExplorer.Web.Client.Components.Location;
using ClimateExplorer.Web.Client.Infrastructure;
using ClimateExplorer.Web.Client.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.WebUtilities;
Expand Down Expand Up @@ -73,6 +74,8 @@ public override void Dispose()

protected override async Task OnInitializedAsync()
{
using var perf = PerformanceLogScope.Start(Logger!, "Index.OnInitializedAsync", ("locationString", LocationString));
await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.mark", "index-initialized-start", new { locationString = LocationString });
// Check to see if a named location is being requested. That will look like /location/<location-name>
Location = await GetLocationFromPath();

Expand Down Expand Up @@ -105,6 +108,8 @@ protected override async Task OnParametersSetAsync()

protected override async Task OnAfterRenderAsync(bool firstRender)
{
using var perf = PerformanceLogScope.Start(Logger!, "Index.OnAfterRenderAsync", ("firstRender", firstRender), ("hasDataSetDefinitions", DataSetDefinitions is not null), ("locationId", Location?.Id));
await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.mark", firstRender ? "index-first-render-start" : "index-render-start", new { locationId = Location?.Id, hasDataSetDefinitions = DataSetDefinitions is not null });
if (firstRender)
{
SiteOverviewService!.ShowRequested += HandleShowRequested;
Expand All @@ -116,7 +121,10 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
var locationsTask = DataService!.GetLocations(false);
var regionsTask = DataService!.GetRegions();

await Task.WhenAll(dataSetDefinitionsTask, locationsTask, regionsTask);
using (PerformanceLogScope.Start(Logger!, "Index.LoadReferenceData"))
{
await Task.WhenAll(dataSetDefinitionsTask, locationsTask, regionsTask);
}

DataSetDefinitions = [.. await dataSetDefinitionsTask];
LocationDictionary = (await locationsTask).ToDictionary(x => x.Id, x => x);
Expand Down Expand Up @@ -155,6 +163,9 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
}
}

await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.mark", "index-reference-data-loaded", new { locationCount = LocationDictionary?.Count, dataSetDefinitionCount = DataSetDefinitions?.Count(), regionCount = Regions?.Count() });
await JsRuntime!.InvokeVoidAsync("climateExplorerPerformance.measure", "index-reference-data", "index-first-render-start", "index-reference-data-loaded", new { locationId = Location?.Id });

StateHasChanged();
}

Expand Down
1 change: 1 addition & 0 deletions ClimateExplorer.Web/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
the chart to silently fail to initialise and remain stuck on the loading spinner.
Execution order of the two chart scripts is preserved because they are sequential.
-->
<script src="@Assets["js/performance-instrumentation.js"]"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"
integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz"
crossorigin="anonymous"
Expand Down
27 changes: 27 additions & 0 deletions ClimateExplorer.Web/wwwroot/js/performance-instrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
(function () {
const prefix = "ClimateExplorer";

window.climateExplorerPerformance = {
mark: function (name, detail) {
if (!window.performance || !window.performance.mark) return;
performance.mark(`${prefix}:${name}`, { detail: detail || {} });
console.debug(`${prefix}:mark`, name, detail || {});
},
measure: function (name, startMark, endMark, detail) {
if (!window.performance || !window.performance.measure) return;
const measureName = `${prefix}:${name}`;
const start = `${prefix}:${startMark}`;
const end = endMark ? `${prefix}:${endMark}` : undefined;
try {
performance.measure(measureName, { start: start, end: end, detail: detail || {} });
const entries = performance.getEntriesByName(measureName);
const latest = entries[entries.length - 1];
console.info(`${prefix}:measure`, name, latest ? Math.round(latest.duration) : null, detail || {});
} catch (error) {
console.debug(`${prefix}:measure skipped`, name, error);
}
}
};

window.climateExplorerPerformance.mark("document-script-loaded", { url: window.location.href });
})();
95 changes: 53 additions & 42 deletions ClimateExplorer.WebApiClient/Services/DataService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,24 @@
using ClimateExplorer.Core.Model;
using ClimateExplorer.Core.ViewModel;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging;
using static ClimateExplorer.Core.Enums;

public class DataService : IDataService
{
private readonly HttpClient httpClient;
private readonly IDataServiceCache dataServiceCache;
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly JsonSerializerOptions jsonSerializerOptions;
private readonly ILogger<DataService> logger;

public DataService(
HttpClient httpClient,
IDataServiceCache dataServiceCache)
IDataServiceCache dataServiceCache,
ILogger<DataService> logger)
{
this.httpClient = httpClient;
this.dataServiceCache = dataServiceCache;
this.logger = logger;
jsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web) { Converters = { new JsonStringEnumConverter() } };
}

Expand All @@ -41,9 +45,11 @@ public async Task<DataSet> PostDataSet(
short? year,
DataResolution? minimumDataResolution)
{
var endpoint = "dataset";
var started = System.Diagnostics.Stopwatch.GetTimestamp();
var response =
await httpClient.PostAsJsonAsync(
"dataset",
endpoint,
new PostDataSetsRequestBody
{
BinAggregationFunction = binAggregationFunction,
Expand All @@ -69,6 +75,8 @@ await httpClient.PostAsJsonAsync(
}

var result = await response.Content.ReadFromJsonAsync<DataSet>();
var elapsed = System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
logger.LogInformation("PerfApiClient endpoint={Endpoint} method=POST cached=false elapsedMs={ElapsedMilliseconds:0.0} recordCount={RecordCount} seriesCount={SeriesCount} binGranularity={BinGranularity}", endpoint, elapsed, result?.DataRecords?.Count, seriesSpecifications.Length, binGranularity);

return result!;
}
Expand All @@ -82,13 +90,7 @@ public async Task<ApiMetadataModel> GetAbout()
public async Task<IEnumerable<DataSetDefinitionViewModel>> GetDataSetDefinitions()
{
var url = "/datasetdefinition";
var result = dataServiceCache.Get<DataSetDefinitionViewModel[]>(url);
if (result == null)
{
result = await httpClient.GetFromJsonAsync<DataSetDefinitionViewModel[]>(url, jsonSerializerOptions);

dataServiceCache.Put(url, result!);
}
var result = await GetFromJsonWithTiming<DataSetDefinitionViewModel[]>(url, jsonSerializerOptions, cacheable: true);

return result!;
}
Expand All @@ -102,13 +104,7 @@ public async Task<IEnumerable<Location>> GetLocations(bool permitCreateCache = t
url = QueryHelpers.AddQueryString(url, "permitCreateCache", permitCreateCache.ToString().ToLowerInvariant());
}

var result = dataServiceCache.Get<Location[]>(url);
if (result == null)
{
result = await httpClient.GetFromJsonAsync<Location[]>(url);

dataServiceCache.Put(url, result!);
}
var result = await GetFromJsonWithTiming<Location[]>(url, cacheable: true);

return result!;
}
Expand All @@ -128,13 +124,7 @@ public async Task<IEnumerable<LocationDistance>> GetNearbyLocations(Guid locatio
url = QueryHelpers.AddQueryString(url, "skip", skip.Value.ToString());
}

var result = dataServiceCache.Get<LocationDistance[]>(url);
if (result == null)
{
result = await httpClient.GetFromJsonAsync<LocationDistance[]>(url);

dataServiceCache.Put(url, result!);
}
var result = await GetFromJsonWithTiming<LocationDistance[]>(url, cacheable: true);

return result!;
}
Expand All @@ -149,13 +139,7 @@ public async Task<Dictionary<string, string>> GetCountries()
public async Task<IEnumerable<Region>> GetRegions()
{
var url = $"/region";
var result = dataServiceCache.Get<Region[]>(url);
if (result == null)
{
result = await httpClient.GetFromJsonAsync<Region[]>(url);

dataServiceCache.Put(url, result!);
}
var result = await GetFromJsonWithTiming<Region[]>(url, cacheable: true);
return result!;
}

Expand Down Expand Up @@ -186,9 +170,7 @@ public async Task<IEnumerable<HeatingScoreRow>> GetHeatingScoreTable()
if (result == null)
{
var url = $"/heating-score-table";
result = await httpClient.GetFromJsonAsync<HeatingScoreRow[]>(url);

dataServiceCache.Put(heatingScoreTableKey, result!);
result = await GetFromJsonWithTiming<HeatingScoreRow[]>(url, cacheable: true);
}

return result!;
Expand Down Expand Up @@ -231,13 +213,7 @@ public async Task<ClimateRecordsResponse> GetClimateRecords(Guid locationId, Dat
url = QueryHelpers.AddQueryString(url, "monthly", "true");
}

var result = dataServiceCache.Get<ClimateRecordsResponse>(url);
if (result == null)
{
result = await httpClient.GetFromJsonAsync<ClimateRecordsResponse>(url, jsonSerializerOptions);

dataServiceCache.Put(url, result!);
}
var result = await GetFromJsonWithTiming<ClimateRecordsResponse>(url, jsonSerializerOptions, cacheable: true);

return result!;
}
Expand All @@ -253,7 +229,42 @@ public async Task<RecentObservationsResponse> GetRecentObservations(Guid locatio
url = QueryHelpers.AddQueryString(url, "isLocationSupported", "true");
}

var result = await httpClient.GetFromJsonAsync<RecentObservationsResponse>(url, jsonSerializerOptions);
var result = await GetFromJsonWithTiming<RecentObservationsResponse>(url, jsonSerializerOptions);
return result!;
}


private async Task<T> GetFromJsonWithTiming<T>(string url, JsonSerializerOptions? options = null, bool cacheable = false)
{
var cached = cacheable ? dataServiceCache.Get<T>(url) : default;
if (cached is not null)
{
logger.LogInformation("PerfApiClient endpoint={Endpoint} cached=true elapsedMs={ElapsedMilliseconds:0.0} recordCount={RecordCount}", url, 0, GetRecordCount(cached));
return cached;
}

var started = System.Diagnostics.Stopwatch.GetTimestamp();
var result = await httpClient.GetFromJsonAsync<T>(url, options);
var elapsed = System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
logger.LogInformation("PerfApiClient endpoint={Endpoint} cached=false elapsedMs={ElapsedMilliseconds:0.0} recordCount={RecordCount} responseType={ResponseType}", url, elapsed, GetRecordCount(result), typeof(T).Name);

if (cacheable)
{
dataServiceCache.Put(url, result!);
}

return result!;
}

private static int? GetRecordCount<T>(T? result)
{
return result switch
{
System.Collections.ICollection collection => collection.Count,
ClimateRecordsResponse response => response.Records?.Count(),
RecentObservationsResponse response => response.Records?.Count(),
_ => null,
};
}

}
Loading