Skip to content

Commit da8f7c3

Browse files
committed
Initial Log Parser and Visualizer Commit
1 parent d1bcd1a commit da8f7c3

26 files changed

Lines changed: 1339 additions & 0 deletions
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Build and Release
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
tags: [ 'v*' ]
7+
workflow_dispatch:
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- name: Setup .NET
15+
uses: actions/setup-dotnet@v4
16+
with:
17+
dotnet-version: '10.0.x'
18+
- name: Publish Windows
19+
run: dotnet publish FSMPLogVisualizer.UI/FSMPLogVisualizer.UI.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ./publish/win-x64
20+
- name: Publish Linux
21+
run: dotnet publish FSMPLogVisualizer.UI/FSMPLogVisualizer.UI.csproj -c Release -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -o ./publish/linux-x64
22+
- name: Upload Artifacts
23+
uses: actions/upload-artifact@v4
24+
with:
25+
name: FSMPLogVisualizer-Portable
26+
path: |
27+
./publish/win-x64/FSMPLogVisualizer.UI.exe
28+
./publish/linux-x64/FSMPLogVisualizer.UI
29+
30+
release:
31+
needs: build
32+
if: startsWith(github.ref, 'refs/tags/v')
33+
runs-on: ubuntu-latest
34+
permissions:
35+
contents: write
36+
steps:
37+
- name: Download Artifacts
38+
uses: actions/download-artifact@v4
39+
with:
40+
name: FSMPLogVisualizer-Portable
41+
path: ./release-assets
42+
- name: Create Release
43+
uses: softprops/action-gh-release@v2
44+
with:
45+
files: |
46+
./release-assets/FSMPLogVisualizer.UI.exe
47+
./release-assets/FSMPLogVisualizer.UI
48+
env:
49+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
[Bb]in/
2+
[Oo]bj/
3+
4+
# Visual Studio / VS Code
5+
.vs/
6+
.vscode/
7+
*.swp
8+
*.*swo
9+
*.user
10+
*.userosscache
11+
*.sln.docstates
12+
*.suo
13+
14+
# JetBrains / Rider
15+
.idea/
16+
*.sln.dotSettings.user
17+
18+
# Mac / Linux system files
19+
.DS_Store
20+
.directory
21+
22+
# App Specific
23+
*.db
24+
publish/
25+
sample\ logs/
26+
!sample\ logs/README.md
27+
28+
# Package restore folders
29+
packages/
30+
node_modules/
31+
32+
# User-specific files
33+
*.rsuser
34+
*.suo
35+
*.user
36+
*.userosscache
37+
*.sln.docstates
38+
39+
# Build results
40+
[Dd]ebug/
41+
[Rr]elease/
42+
x64/
43+
x86/
44+
build/
45+
46+
# MSBuild
47+
*.suo
48+
*.user
49+
*.sln.docstates
50+
*.docstates
51+
*.userosscache

FSMPLogVisualizer.Core/Class1.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace FSMPLogVisualizer.Core;
2+
3+
public class Class1
4+
{
5+
6+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<Version>1.1.0</Version>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="sqlite-net-pcl" Version="1.9.172" />
12+
</ItemGroup>
13+
14+
</Project>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text.RegularExpressions;
5+
using System.Threading.Tasks;
6+
7+
namespace FSMPLogVisualizer.Core
8+
{
9+
public class LogParser
10+
{
11+
// "smp cost in main loop (msecs): {:.2f}, cost outside main loop: {:.2f}, percentage outside vs total: {:.2f}"
12+
private static readonly Regex smpCostRegex = new Regex(
13+
@"smp cost in main loop \(msecs\):\s*([\d\.]+),\s*cost outside main loop:\s*([\d\.]+),\s*percentage outside vs total:\s*([\d\.]+)",
14+
RegexOptions.Compiled | RegexOptions.IgnoreCase);
15+
16+
// "msecs/activeSkeleton {:.2f} activeSkeletons/maxActive/total {}/{}/{} processTimeInMainLoop/targetTime {:.2f}/{:.2f}"
17+
private static readonly Regex activeSkeletonsRegex = new Regex(
18+
@"msecs/activeSkeleton\s*([\d\.]+)\s*activeSkeletons/maxActive/total\s*(\d+)/(\d+)/(\d+)\s*processTimeInMainLoop/targetTime\s*([\d\.]+)/([\d\.]+)",
19+
RegexOptions.Compiled | RegexOptions.IgnoreCase);
20+
21+
// Extracts timestamp "[16:45:25.906]"
22+
private static readonly Regex timestampRegex = new Regex(@"^\[([\d:\.]+)\]", RegexOptions.Compiled);
23+
24+
public async Task<(LogRunSession session, List<LogDataPoint> dataPoints)> ParseLogFileAsync(string filePath, string existingLastTimestamp = null)
25+
{
26+
var dataPoints = new List<LogDataPoint>();
27+
28+
var session = new LogRunSession
29+
{
30+
FileName = Path.GetFileName(filePath),
31+
ImportedAt = DateTime.Now
32+
};
33+
34+
bool skipUntilNew = !string.IsNullOrEmpty(existingLastTimestamp);
35+
36+
using var reader = new StreamReader(filePath);
37+
string? line;
38+
39+
// Read version from first line if possible
40+
if ((line = await reader.ReadLineAsync()) != null)
41+
{
42+
session.Version = ExtractVersion(line);
43+
session.SessionKey = $"{session.FileName}_{session.Version}"; // Basic key
44+
}
45+
46+
// Stateful tracking
47+
int? currentActiveSkeletons = null;
48+
int? currentMaxActive = null;
49+
int? currentTotal = null;
50+
double? currentMsecsPerAct = null;
51+
double? currentProcTime = null;
52+
double? currentTargetTime = null;
53+
54+
while ((line = await reader.ReadLineAsync()) != null)
55+
{
56+
var tsMatch = timestampRegex.Match(line);
57+
string timestamp = tsMatch.Success ? tsMatch.Groups[1].Value : string.Empty;
58+
59+
if (skipUntilNew && tsMatch.Success)
60+
{
61+
if (timestamp == existingLastTimestamp)
62+
{
63+
skipUntilNew = false; // We found the last known timestamp, resume parsing after this
64+
}
65+
continue;
66+
}
67+
68+
if (skipUntilNew) continue;
69+
70+
var skelMatch = activeSkeletonsRegex.Match(line);
71+
if (skelMatch.Success)
72+
{
73+
currentMsecsPerAct = double.Parse(skelMatch.Groups[1].Value);
74+
currentActiveSkeletons = int.Parse(skelMatch.Groups[2].Value);
75+
currentMaxActive = int.Parse(skelMatch.Groups[3].Value);
76+
currentTotal = int.Parse(skelMatch.Groups[4].Value);
77+
currentProcTime = double.Parse(skelMatch.Groups[5].Value);
78+
currentTargetTime = double.Parse(skelMatch.Groups[6].Value);
79+
80+
dataPoints.Add(new LogDataPoint
81+
{
82+
Timestamp = timestamp,
83+
MsecsPerActiveSkeleton = currentMsecsPerAct,
84+
ActiveSkeletons = currentActiveSkeletons,
85+
MaxActiveSkeletons = currentMaxActive,
86+
TotalSkeletons = currentTotal,
87+
ProcessTimeInMainLoop = currentProcTime,
88+
TargetTime = currentTargetTime
89+
});
90+
}
91+
92+
var costMatch = smpCostRegex.Match(line);
93+
if (costMatch.Success)
94+
{
95+
// Cost match means we emit a datapoint with the cost, and optionally the latest skeleton data if we have it
96+
dataPoints.Add(new LogDataPoint
97+
{
98+
Timestamp = timestamp,
99+
CostInMainLoop = double.Parse(costMatch.Groups[1].Value),
100+
CostOutsideMainLoop = double.Parse(costMatch.Groups[2].Value),
101+
PercentageOutside = double.Parse(costMatch.Groups[3].Value),
102+
103+
// Associate with the most recent skeleton count to allow grouping by skeletons
104+
ActiveSkeletons = currentActiveSkeletons,
105+
MaxActiveSkeletons = currentMaxActive,
106+
TotalSkeletons = currentTotal,
107+
});
108+
}
109+
}
110+
111+
return (session, dataPoints);
112+
}
113+
114+
private string ExtractVersion(string firstLine)
115+
{
116+
// e.g., "hdtSMP64 200500" or "[16:44:22.378] [2840 ] [I] hdtsmp64 v3-1-9-0"
117+
if (firstLine.Contains("v3", StringComparison.OrdinalIgnoreCase) || firstLine.Contains("hdtsmp", StringComparison.OrdinalIgnoreCase))
118+
{
119+
var match = Regex.Match(firstLine, @"(v\d+-\d+-\d+-\d+|\d{6})", RegexOptions.IgnoreCase);
120+
if (match.Success) return match.Value;
121+
122+
var vMatch = Regex.Match(firstLine, @"hdtSMP64\s+(.*)", RegexOptions.IgnoreCase);
123+
if (vMatch.Success) return vMatch.Groups[1].Value.Trim();
124+
}
125+
return "Unknown";
126+
}
127+
}
128+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
using SQLite;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Threading.Tasks;
5+
6+
namespace FSMPLogVisualizer.Core
7+
{
8+
public class LogRepository
9+
{
10+
private SQLiteAsyncConnection _database;
11+
12+
public LogRepository(string dbPath)
13+
{
14+
_database = new SQLiteAsyncConnection(dbPath);
15+
_database.CreateTableAsync<LogRunSession>().Wait();
16+
_database.CreateTableAsync<LogDataPoint>().Wait();
17+
}
18+
19+
public async Task<int> SaveSessionAsync(LogRunSession session)
20+
{
21+
if (session.Id != 0)
22+
{
23+
return await _database.UpdateAsync(session);
24+
}
25+
else
26+
{
27+
return await _database.InsertAsync(session);
28+
}
29+
}
30+
31+
public async Task<int> SaveDataPointsAsync(IEnumerable<LogDataPoint> points)
32+
{
33+
return await _database.InsertAllAsync(points);
34+
}
35+
36+
public async Task<List<LogRunSession>> GetSessionsAsync()
37+
{
38+
return await _database.Table<LogRunSession>().ToListAsync();
39+
}
40+
41+
public async Task<List<LogDataPoint>> GetDataPointsAsync(int sessionId)
42+
{
43+
return await _database.Table<LogDataPoint>().Where(p => p.SessionId == sessionId).ToListAsync();
44+
}
45+
46+
public async Task<List<LogDataPoint>> GetAllDataPointsAsync()
47+
{
48+
return await _database.Table<LogDataPoint>().ToListAsync();
49+
}
50+
51+
public async Task<bool> SessionExistsAsync(string sessionKey)
52+
{
53+
var existing = await _database.Table<LogRunSession>().Where(s => s.SessionKey == sessionKey).FirstOrDefaultAsync();
54+
return existing != null;
55+
}
56+
57+
public async Task ClearAllAsync()
58+
{
59+
await _database.DeleteAllAsync<LogDataPoint>();
60+
await _database.DeleteAllAsync<LogRunSession>();
61+
}
62+
}
63+
}

FSMPLogVisualizer.Core/Models.cs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using SQLite;
2+
using System;
3+
4+
namespace FSMPLogVisualizer.Core
5+
{
6+
public class LogRunSession
7+
{
8+
[PrimaryKey, AutoIncrement]
9+
public int Id { get; set; }
10+
public string Version { get; set; } = string.Empty;
11+
public string FileName { get; set; } = string.Empty;
12+
13+
// Use the first timestamp or a hash of the file name + size for deduplication
14+
public string SessionKey { get; set; } = string.Empty;
15+
16+
public DateTime ImportedAt { get; set; }
17+
}
18+
19+
public class LogDataPoint
20+
{
21+
[PrimaryKey, AutoIncrement]
22+
public int Id { get; set; }
23+
24+
[Indexed]
25+
public int SessionId { get; set; }
26+
27+
public string Timestamp { get; set; } = string.Empty;
28+
29+
// From "smp cost" line
30+
public double? CostInMainLoop { get; set; }
31+
public double? CostOutsideMainLoop { get; set; }
32+
public double? PercentageOutside { get; set; }
33+
34+
// From "activeSkeletons" line
35+
public double? MsecsPerActiveSkeleton { get; set; }
36+
public int? ActiveSkeletons { get; set; }
37+
public int? MaxActiveSkeletons { get; set; }
38+
public int? TotalSkeletons { get; set; }
39+
public double? ProcessTimeInMainLoop { get; set; }
40+
public double? TargetTime { get; set; }
41+
}
42+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net10.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<IsPackable>false</IsPackable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="coverlet.collector" Version="6.0.4" />
12+
<PackageReference Include="FluentAssertions" Version="8.9.0" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
14+
<PackageReference Include="xunit" Version="2.9.3" />
15+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
16+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
17+
<PrivateAssets>all</PrivateAssets>
18+
</PackageReference>
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<Using Include="Xunit" />
23+
</ItemGroup>
24+
25+
<ItemGroup>
26+
<ProjectReference Include="..\FSMPLogVisualizer.Core\FSMPLogVisualizer.Core.csproj" />
27+
</ItemGroup>
28+
29+
</Project>

0 commit comments

Comments
 (0)