Skip to content

Commit

Permalink
feat: Minecraft stats over time, cached and background loading for fa…
Browse files Browse the repository at this point in the history
…ster perf
  • Loading branch information
itssimple committed Dec 30, 2024
1 parent 82171c9 commit fe02e2d
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 156 deletions.
1 change: 1 addition & 0 deletions CFLookup/CFLookup.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.5" />
<PackageReference Include="Highsoft.Highcharts" Version="11.4.1" />
<PackageReference Include="Highsoft.Highstock" Version="11.4.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="NSec.Cryptography" Version="24.4.0" />
<PackageReference Include="CurseForge.APIClient" Version="3.0.0" />
<PackageReference Include="StackExchange.Redis" Version="2.8.16" />
Expand Down
178 changes: 178 additions & 0 deletions CFLookup/Jobs/CacheMCOverTime.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
using Hangfire.Server;
using Highsoft.Web.Mvc.Charts.Rendering;
using Highsoft.Web.Mvc.Charts;
using StackExchange.Redis;

namespace CFLookup.Jobs
{
public class CacheMCOverTime
{
public static async Task RunAsync(PerformContext context)
{
using (var scope = Program.ServiceProvider.CreateScope())
{
var _db = scope.ServiceProvider.GetRequiredService<MSSQLDB>();
var _redis = scope.ServiceProvider.GetRequiredService<ConnectionMultiplexer>();

var _rdb = _redis.GetDatabase(5);

var stats = await SharedMethods.GetMinecraftStatsOverTime(_db, CancellationToken.None, null);

var renderers = new List<string>();
var ModLoaderStats = new Dictionary<string, List<Series>>();
var modloaderStats = new Dictionary<string, Dictionary<DateTimeOffset, Dictionary<string, long>>>();

foreach (var stat in stats)
{
var date = stat.Key;
foreach (var modloaderHolder in stat.Value)
{
var gameVersion = modloaderHolder.Key;

if (gameVersion.Contains("snapshot", StringComparison.InvariantCultureIgnoreCase)) continue;

foreach (var gameInfo in modloaderHolder.Value)
{
var modloader = gameInfo.Key;
var count = gameInfo.Value;

if (modloader.Contains("LiteLoader", StringComparison.InvariantCultureIgnoreCase)) continue;

if (!modloaderStats.ContainsKey(modloader))
{
modloaderStats[modloader] = new Dictionary<DateTimeOffset, Dictionary<string, long>>();
}

if (!modloaderStats[modloader].ContainsKey(date))
{
modloaderStats[modloader][date] = new Dictionary<string, long>();
}

modloaderStats[modloader][date][gameVersion] = count;
}
}
}

// Generate different series per modloader and game version for Highstock as separate graphs, where the game versions are the line series

foreach (var kv in modloaderStats)
{
var loader = kv.Key;

var testGraph = new Dictionary<string, List<LineSeriesData>>();

foreach (var d in kv.Value)
{
var date = d.Key;
var gameVersions = d.Value;

foreach (var gameVersion in gameVersions)
{
if (!testGraph.ContainsKey(gameVersion.Key))
{
testGraph[gameVersion.Key] = new List<LineSeriesData>();
}

testGraph[gameVersion.Key].Add(new LineSeriesData { X = date.ToUnixTimeMilliseconds(), Y = gameVersion.Value });
}
}

var viewData = new List<Series>();

foreach (var series in testGraph)
{
viewData.Add(new LineSeries
{
Name = series.Key,
Data = series.Value,
TurboThreshold = 100,
Selected = false
});
}

ModLoaderStats[$"{loader}Data"] = viewData;
}

foreach (var loaderData in ModLoaderStats)
{
var loader = loaderData.Key;
var chartOptions =
new Highcharts
{
ID = $"{loader.Replace("Data", "")}Chart",
Chart = new Chart
{
HeightNumber = 800,
ZoomType = ChartZoomType.X
},
XAxis = new List<XAxis>
{
new XAxis
{
Type = "datetime",
MinRange = 3600000
}
},
Legend = new Legend
{
Enabled = true
},
YAxis = new List<YAxis>
{
new YAxis
{
Labels = new YAxisLabels
{
},
PlotLines = new List<YAxisPlotLines>
{
new YAxisPlotLines
{
Value = 0,
Width = 2,
Color = "silver"
}
}
}
},
Tooltip = new Tooltip
{
PointFormat = @"<span style='color:{series.color}'>{series.name}</span>: <b>{point.y}</b><br/>",
ValueDecimals = 0
},
PlotOptions = new PlotOptions
{
Series = new PlotOptionsSeries
{
TurboThreshold = 10000
},
Area = new PlotOptionsArea
{
Marker = new PlotOptionsAreaMarker
{
Radius = 2
},
LineWidth = 1,
States = new PlotOptionsAreaStates
{
Hover = new PlotOptionsAreaStatesHover
{
LineWidth = 1
}
},
Threshold = null
}
},
Series = loaderData.Value,
Title = new Title { Text = $"Amount of mods for {loader.Replace("Data", "")} over time" }
};

var renderer = new HighchartsRenderer(chartOptions);
renderers.Add(renderer.RenderHtml());
}

await _rdb.StringSetAsync("cf-mcmodloader-stats", string.Join("<hr />", renderers), TimeSpan.FromHours(1));
}
}
}
}
2 changes: 1 addition & 1 deletion CFLookup/Jobs/SaveMinecraftModStats.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public static async Task RunAsync(PerformContext context)
{ "Forge", ModLoaderType.Forge },
{ "Fabric", ModLoaderType.Fabric },
{ "Quilt", ModLoaderType.Quilt },
{ "NeoForge", (ModLoaderType)6 }
{ "NeoForge", ModLoaderType.NeoForge }
};

var modsPerVersion = new Dictionary<string, Dictionary<string, long>>();
Expand Down
75 changes: 3 additions & 72 deletions CFLookup/Pages/MinecraftModStatsOverTime.cshtml
Original file line number Diff line number Diff line change
@@ -1,80 +1,11 @@
@page
@using Highsoft.Web.Mvc.Stocks
@using Highsoft.Web.Mvc.Stocks.Rendering
@model CFLookup.Pages.MinecraftModStatsOverTimeModel
@{
ViewData["Title"] = "Minecraft mod stats over time";
}
@section Head {
<script src="https://code.highcharts.com/stock/highstock.js"></script>
<script src="https://code.highcharts.com/highcharts.js"></script>
<script src="https://code.highcharts.com/modules/exporting.js"></script>
}

@{
var chartOptions =
new Highstock
{
RangeSelector = new RangeSelector
{
Selected = 4
},
Responsive = new Responsive {

},
Chart = new Chart {
HeightNumber = 800
},
XAxis = new List<XAxis>
{
new XAxis
{
Ordinal = true
}
},
Legend = new Legend
{
Enabled = true
},
YAxis = new List<YAxis>
{
new YAxis
{
Labels = new YAxisLabels
{
},
PlotLines = new List<YAxisPlotLines>
{
new YAxisPlotLines
{
Value = 0,
Width = 2,
Color = "silver"
}
}
}
},
PlotOptions = new PlotOptions
{
Series = new PlotOptionsSeries
{
Compare = PlotOptionsSeriesCompare.Value,
DataGrouping = new PlotOptionsSeriesDataGrouping
{
Enabled = true
}
}
},
Tooltip = new Tooltip
{
PointFormat = @"<span style='color:{series.color}'>{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>",
ValueDecimals = 2,
Split = true,
Outside = true
},
Series = Model.ModLoaderStats["ForgeData"],
};

chartOptions.ID = "forgeChart";
var renderer = new HighstockRenderer(chartOptions);
}

@Html.Raw(renderer.RenderHtml())
@Html.Raw(Model.ChartHtml)
82 changes: 10 additions & 72 deletions CFLookup/Pages/MinecraftModStatsOverTime.cshtml.cs
Original file line number Diff line number Diff line change
@@ -1,92 +1,30 @@
using Highsoft.Web.Mvc.Stocks;
using Microsoft.AspNetCore.Mvc.RazorPages;
using StackExchange.Redis;

namespace CFLookup.Pages
{
public class MinecraftModStatsOverTimeModel : PageModel
{
private readonly MSSQLDB _db;
private readonly ConnectionMultiplexer _db;

public Dictionary<string, Dictionary<DateTimeOffset, Dictionary<string, long>>> Stats { get; set; } = new Dictionary<string, Dictionary<DateTimeOffset, Dictionary<string, long>>>();
public string ChartHtml { get; set; }

public Dictionary<string, List<Series>> ModLoaderStats = new Dictionary<string, List<Series>>();

public MinecraftModStatsOverTimeModel(MSSQLDB db)
public MinecraftModStatsOverTimeModel(ConnectionMultiplexer db)
{
_db = db;
}

public async Task OnGetAsync()
public async Task OnGetAsync(CancellationToken cancellationToken)
{
var stats = await SharedMethods.GetMinecraftStatsOverTime(_db, 24 * 30);

var modloaderStats = new Dictionary<string, Dictionary<DateTimeOffset, Dictionary<string, long>>>();

foreach (var stat in stats)
{
var date = stat.Key;
foreach(var modloaderHolder in stat.Value)
{
var gameVersion = modloaderHolder.Key;

if(gameVersion.Contains("snapshot", StringComparison.InvariantCultureIgnoreCase)) continue;

foreach(var gameInfo in modloaderHolder.Value)
{
var modloader = gameInfo.Key;
var count = gameInfo.Value;

if(modloader.Contains("LiteLoader", StringComparison.InvariantCultureIgnoreCase)) continue;

if (!modloaderStats.ContainsKey(modloader))
{
modloaderStats[modloader] = new Dictionary<DateTimeOffset, Dictionary<string, long>>();
}

if (!modloaderStats[modloader].ContainsKey(date))
{
modloaderStats[modloader][date] = new Dictionary<string, long>();
}
var rdb = _db.GetDatabase(5);

modloaderStats[modloader][date][gameVersion] = count;
}
}
}

// Generate different series per modloader and game version for Highstock as separate graphs, where the game versions are the line series
var testGraph = new Dictionary<string, List<LineSeriesData>>();
var forgeData = modloaderStats["Forge"];
var statHtml = await rdb.StringGetAsync("cf-mcmodloader-stats");

foreach(var d in forgeData)
if(statHtml == RedisValue.Null)
{
var date = d.Key;
var gameVersions = d.Value;

foreach(var gameVersion in gameVersions)
{
if (!testGraph.ContainsKey(gameVersion.Key))
{
testGraph[gameVersion.Key] = new List<LineSeriesData>();
}

testGraph[gameVersion.Key].Add(new LineSeriesData { X = date.ToUnixTimeMilliseconds(), Y = gameVersion.Value });
}
ChartHtml = "No data loaded yet";
return;
}

var viewData = new List<Series>();

foreach(var series in testGraph)
{
viewData.Add(new LineSeries
{
Name = series.Key,
Data = series.Value,
TurboThreshold = 100,

});
}

ModLoaderStats["ForgeData"] = viewData;
}
}
}
4 changes: 2 additions & 2 deletions CFLookup/Pages/Shared/_Layout.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/MinecraftModpackStats" asp-area="" title="CFLookup - Minecraft modpack stats">Minecraft modpack stats</a>
</li>
</li>-->
<li class="nav-item">
<a class="nav-link" asp-page="/MinecraftModStatsOverTime" asp-area="" title="CFLookup - Minecraft mod stats over time">Minecraft stats over time</a>
</li>-->
</li>
<li class="nav-item">
<a class="nav-link" asp-page="/FileProcessingInfo" asp-area="" title="CFLookup - File processing info">File processing info</a>
</li>
Expand Down
Loading

0 comments on commit fe02e2d

Please sign in to comment.