added benchmarks
This commit is contained in:
@@ -0,0 +1,77 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Encodings.Web;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using ParadoxSaveParser.Lib;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Locates and lazily loads the benchmark corpus:
|
||||||
|
/// a real EU4 <c>gamestate</c> file and its compact JSON twin (for jq).
|
||||||
|
/// Override the directory with the PSP_BENCH_DATA environment variable.
|
||||||
|
/// </summary>
|
||||||
|
public static class BenchData
|
||||||
|
{
|
||||||
|
public const string PdxQuery = "countries.*.technology";
|
||||||
|
|
||||||
|
/// <summary>jq program extracting the same data as <see cref="PdxQuery" />.</summary>
|
||||||
|
public const string JqQuery = ".countries | map_values(.technology)";
|
||||||
|
|
||||||
|
public static string Dir { get; } =
|
||||||
|
Environment.GetEnvironmentVariable("PSP_BENCH_DATA")
|
||||||
|
?? Path.Combine(FindRepoRoot(), "ParadoxSaveParser.CLI", "bin", "Debug", "net10.0");
|
||||||
|
|
||||||
|
public static string PdxPath => Path.Combine(Dir, "gamestate");
|
||||||
|
public static string JsonPath => Path.Combine(Dir, "gamestate.min.json");
|
||||||
|
|
||||||
|
private static byte[]? _pdxBytes;
|
||||||
|
private static string? _pdxText;
|
||||||
|
|
||||||
|
/// <summary>Raw bytes of the save, kept in memory so benchmarks don't measure disk I/O.</summary>
|
||||||
|
public static byte[] PdxBytes => _pdxBytes ??= File.ReadAllBytes(PdxPath);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The save decoded as text for the regex engines.
|
||||||
|
/// Latin1 is used because EU4 saves contain non-UTF8 bytes inside dynasty names,
|
||||||
|
/// and it maps every byte to exactly one char.
|
||||||
|
/// </summary>
|
||||||
|
public static string PdxText => _pdxText ??= Encoding.Latin1.GetString(PdxBytes);
|
||||||
|
|
||||||
|
private static string FindRepoRoot()
|
||||||
|
{
|
||||||
|
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (dir != null && !File.Exists(Path.Combine(dir.FullName, "ParadoxSaveParser.sln")))
|
||||||
|
dir = dir.Parent;
|
||||||
|
return dir?.FullName ?? Directory.GetCurrentDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Produces the compact JSON twin of the save file, which is what jq is given.
|
||||||
|
/// Compact (not indented) so jq isn't penalized by parsing whitespace.
|
||||||
|
/// </summary>
|
||||||
|
public static void PrepareJson()
|
||||||
|
{
|
||||||
|
if (File.Exists(JsonPath))
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{JsonPath} already exists, skipping");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"parsing {PdxPath} ...");
|
||||||
|
using var input = File.OpenRead(PdxPath);
|
||||||
|
var root = new SaveParserEU4(input, null).Parse();
|
||||||
|
Console.WriteLine($"serializing to {JsonPath} ...");
|
||||||
|
using var output = File.Create(JsonPath);
|
||||||
|
JsonSerializer.Serialize(output, root, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = false,
|
||||||
|
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||||
|
MaxDepth = 1024,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||||
|
});
|
||||||
|
Console.WriteLine($"done: {new FileInfo(JsonPath).Length / 1024 / 1024} MB");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Engines;
|
||||||
|
using ParadoxSaveParser.Lib;
|
||||||
|
using PCRE;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// In-process engines, all extracting <c>countries.*.technology</c> from the same 110 MB save.
|
||||||
|
/// Input is preloaded into memory, so disk I/O is not part of any measurement.
|
||||||
|
/// </summary>
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 1, iterationCount: 5)]
|
||||||
|
public class ExtractionBenchmarks
|
||||||
|
{
|
||||||
|
private byte[] _pdx = null!;
|
||||||
|
private string _pdxText = null!;
|
||||||
|
private byte[] _json = null!;
|
||||||
|
private ISearchExpression _query = null!;
|
||||||
|
|
||||||
|
private Regex _flatInterpreted = null!;
|
||||||
|
private Regex _flatCompiled = null!;
|
||||||
|
private Regex _flatSourceGen = null!;
|
||||||
|
private Regex _pathAwareCompiled = null!;
|
||||||
|
private Regex _pathAwareNonBacktracking = null!;
|
||||||
|
private Regex _countriesBlock = null!;
|
||||||
|
private PcreRegex _pcreFlat = null!;
|
||||||
|
private PcreRegex _pcrePathAware = null!;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
_pdx = BenchData.PdxBytes;
|
||||||
|
_pdxText = BenchData.PdxText;
|
||||||
|
_json = File.ReadAllBytes(BenchData.JsonPath);
|
||||||
|
_query = SearchExpressionCompiler.Compile(BenchData.PdxQuery);
|
||||||
|
|
||||||
|
_flatInterpreted = new Regex(Extractors.FlatPattern, RegexOptions.None);
|
||||||
|
_flatCompiled = new Regex(Extractors.FlatPattern, RegexOptions.Compiled);
|
||||||
|
_flatSourceGen = Extractors.FlatSourceGen();
|
||||||
|
_pathAwareCompiled = new Regex(Extractors.PathAwarePattern, RegexOptions.Compiled);
|
||||||
|
_pathAwareNonBacktracking = new Regex(Extractors.PathAwarePattern, RegexOptions.NonBacktracking);
|
||||||
|
_countriesBlock = new Regex(Extractors.CountriesBlockPattern, RegexOptions.Compiled);
|
||||||
|
_pcreFlat = new PcreRegex(Extractors.FlatPattern, PcreOptions.Compiled);
|
||||||
|
_pcrePathAware = new PcreRegex(Extractors.PathAwarePattern, PcreOptions.Compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Benchmark(Baseline = true, Description = "SearchExpression (this project)")]
|
||||||
|
public long SearchExpression() => Extractors.SearchExpression(_pdx, _query);
|
||||||
|
|
||||||
|
[Benchmark(Description = "Full parse, then select")]
|
||||||
|
public long FullParse() => Extractors.FullParseThenSelect(_pdx);
|
||||||
|
|
||||||
|
[Benchmark(Description = ".NET Regex flat, interpreted")]
|
||||||
|
public long RegexFlatInterpreted() => Extractors.RegexScan(_flatInterpreted, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = ".NET Regex flat, compiled")]
|
||||||
|
public long RegexFlatCompiled() => Extractors.RegexScan(_flatCompiled, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = ".NET Regex flat, source generated")]
|
||||||
|
public long RegexFlatSourceGen() => Extractors.RegexScan(_flatSourceGen, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = ".NET Regex path aware, compiled")]
|
||||||
|
public long RegexPathAwareCompiled() => Extractors.RegexScan(_pathAwareCompiled, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = ".NET Regex path aware, NonBacktracking")]
|
||||||
|
public long RegexPathAwareNonBacktracking() => Extractors.RegexScan(_pathAwareNonBacktracking, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = ".NET Regex balanced block + flat")]
|
||||||
|
public long RegexBalancedTwoStage() => Extractors.RegexTwoStage(_countriesBlock, _flatCompiled, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = "PCRE.NET flat, JIT compiled")]
|
||||||
|
public long PcreFlat() => Extractors.PcreScan(_pcreFlat, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = "PCRE.NET path aware, JIT compiled")]
|
||||||
|
public long PcrePathAware() => Extractors.PcreScan(_pcrePathAware, _pdxText);
|
||||||
|
|
||||||
|
[Benchmark(Description = "Utf8JsonReader over JSON twin")]
|
||||||
|
public long Utf8JsonReader() => Extractors.Utf8JsonReaderScan(_json);
|
||||||
|
|
||||||
|
[Benchmark(Description = "JsonDocument over JSON twin")]
|
||||||
|
public long JsonDocument() => Extractors.JsonDocumentScan(_json);
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using ParadoxSaveParser.Lib;
|
||||||
|
using PCRE;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Every extractor solves the same task: pull the <c>technology</c> block
|
||||||
|
/// (adm/dip/mil levels) of every country out of an EU4 save, i.e. the path
|
||||||
|
/// <c>countries.*.technology</c>.
|
||||||
|
/// They all return the same checksum so the benchmarks can be verified against each other:
|
||||||
|
/// <c>number_of_countries * 1_000_000 + sum_of_all_tech_levels</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static partial class Extractors
|
||||||
|
{
|
||||||
|
public static long Checksum(long count, long techSum) => count * 1_000_000 + techSum;
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- this project
|
||||||
|
|
||||||
|
/// <summary>The parser of this repo: single streaming pass, guided by a compiled search expression.</summary>
|
||||||
|
public static long SearchExpression(byte[] pdx, ISearchExpression query)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(pdx, false);
|
||||||
|
var root = new SaveParserEU4(stream, query).Parse();
|
||||||
|
return ChecksumOfParsedTree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Same query, but the parser reads from the given stream instead of a preloaded byte[].</summary>
|
||||||
|
public static long SearchExpressionFromStream(Stream stream, ISearchExpression query)
|
||||||
|
{
|
||||||
|
var root = new SaveParserEU4(stream, query).Parse();
|
||||||
|
return ChecksumOfParsedTree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Same parser without a query: parses the whole save into memory (worst case reference point).</summary>
|
||||||
|
public static long FullParseThenSelect(byte[] pdx)
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream(pdx, false);
|
||||||
|
var root = new SaveParserEU4(stream, null).Parse();
|
||||||
|
return ChecksumOfParsedTree(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long ChecksumOfParsedTree(Dictionary<string, object> root)
|
||||||
|
{
|
||||||
|
long count = 0, sum = 0;
|
||||||
|
var countries = (Dictionary<string, object>)root["countries"];
|
||||||
|
foreach (var country in countries.Values)
|
||||||
|
{
|
||||||
|
if (country is not Dictionary<string, object> c
|
||||||
|
|| !c.TryGetValue("technology", out var t)
|
||||||
|
|| t is not Dictionary<string, object> tech)
|
||||||
|
continue;
|
||||||
|
count++;
|
||||||
|
foreach (var level in tech.Values)
|
||||||
|
sum += (long)level;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Checksum(count, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- .NET Regex
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flat scan: matches technology blocks anywhere in the file.
|
||||||
|
/// Fastest regex option, but it does not verify that the match really sits under <c>countries</c>.
|
||||||
|
/// </summary>
|
||||||
|
public const string FlatPattern =
|
||||||
|
@"\n\t\ttechnology=\{\n\t\t\tadm_tech=(\d+)\n\t\t\tdip_tech=(\d+)\n\t\t\tmil_tech=(\d+)\n\t\t\}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Path-aware: pairs a depth-1 country tag with the next technology block via a lazy gap.
|
||||||
|
/// This is what a regex user writes when the key must be associated with the value.
|
||||||
|
/// </summary>
|
||||||
|
public const string PathAwarePattern =
|
||||||
|
@"\n\t([A-Z0-9]{3})=\{[\s\S]*?\n\t\ttechnology=\{\n\t\t\tadm_tech=(\d+)\n\t\t\tdip_tech=(\d+)\n\t\t\tmil_tech=(\d+)\n\t\t\}";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Balancing-group pattern isolating the whole <c>countries={...}</c> block.
|
||||||
|
/// .NET-only feature; PCRE would need recursion instead.
|
||||||
|
/// </summary>
|
||||||
|
public const string CountriesBlockPattern =
|
||||||
|
@"\ncountries=\{(?>[^{}]+|\{(?<d>)|\}(?<-d>))*(?(d)(?!))\n\}";
|
||||||
|
|
||||||
|
public static long RegexScan(Regex regex, string text)
|
||||||
|
{
|
||||||
|
long count = 0, sum = 0;
|
||||||
|
foreach (Match m in regex.Matches(text))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
int g = m.Groups.Count - 3;
|
||||||
|
sum += int.Parse(m.Groups[g].ValueSpan)
|
||||||
|
+ int.Parse(m.Groups[g + 1].ValueSpan)
|
||||||
|
+ int.Parse(m.Groups[g + 2].ValueSpan);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Checksum(count, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Two stages: cut out the countries block with balanced braces, then scan inside it.</summary>
|
||||||
|
public static long RegexTwoStage(Regex blockRegex, Regex innerRegex, string text)
|
||||||
|
{
|
||||||
|
var block = blockRegex.Match(text);
|
||||||
|
if (!block.Success)
|
||||||
|
throw new Exception("countries block not found");
|
||||||
|
return RegexScan(innerRegex, block.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex(FlatPattern)]
|
||||||
|
public static partial Regex FlatSourceGen();
|
||||||
|
|
||||||
|
[GeneratedRegex(PathAwarePattern)]
|
||||||
|
public static partial Regex PathAwareSourceGen();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- PCRE.NET
|
||||||
|
|
||||||
|
public static long PcreScan(PcreRegex regex, string text)
|
||||||
|
{
|
||||||
|
long count = 0, sum = 0;
|
||||||
|
foreach (var m in regex.Matches(text))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
int g = m.Groups.Count - 3;
|
||||||
|
sum += int.Parse(m[g].Value)
|
||||||
|
+ int.Parse(m[g + 1].Value)
|
||||||
|
+ int.Parse(m[g + 2].Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Checksum(count, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- System.Text.Json
|
||||||
|
|
||||||
|
private static readonly byte[] CountriesUtf8 = "countries"u8.ToArray();
|
||||||
|
private static readonly byte[] TechnologyUtf8 = "technology"u8.ToArray();
|
||||||
|
|
||||||
|
/// <summary>Hand written streaming reader over the JSON twin: the closest JSON analogue of the search expression.</summary>
|
||||||
|
public static long Utf8JsonReaderScan(byte[] json)
|
||||||
|
{
|
||||||
|
long count = 0, sum = 0;
|
||||||
|
var reader = new Utf8JsonReader(json, isFinalBlock: true, state: default);
|
||||||
|
reader.Read(); // StartObject (root)
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
bool isCountries = reader.ValueTextEquals(CountriesUtf8);
|
||||||
|
reader.Read();
|
||||||
|
if (!isCountries)
|
||||||
|
{
|
||||||
|
reader.Skip();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inside countries: property name = country tag
|
||||||
|
while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
if (reader.TokenType != JsonTokenType.StartObject)
|
||||||
|
{
|
||||||
|
reader.Skip();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
bool isTech = reader.ValueTextEquals(TechnologyUtf8);
|
||||||
|
reader.Read();
|
||||||
|
if (!isTech)
|
||||||
|
{
|
||||||
|
reader.Skip();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
count++;
|
||||||
|
while (reader.Read() && reader.TokenType == JsonTokenType.PropertyName)
|
||||||
|
{
|
||||||
|
reader.Read();
|
||||||
|
sum += reader.GetInt64();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // countries handled, nothing else is needed
|
||||||
|
}
|
||||||
|
|
||||||
|
return Checksum(count, sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>DOM approach: parse the whole JSON document, then navigate. The .NET equivalent of what jq does.</summary>
|
||||||
|
public static long JsonDocumentScan(byte[] json)
|
||||||
|
{
|
||||||
|
long count = 0, sum = 0;
|
||||||
|
using var doc = JsonDocument.Parse(json, new JsonDocumentOptions { MaxDepth = 1024 });
|
||||||
|
foreach (var country in doc.RootElement.GetProperty("countries").EnumerateObject())
|
||||||
|
{
|
||||||
|
if (country.Value.ValueKind != JsonValueKind.Object
|
||||||
|
|| !country.Value.TryGetProperty("technology", out var tech))
|
||||||
|
continue;
|
||||||
|
count++;
|
||||||
|
foreach (var level in tech.EnumerateObject())
|
||||||
|
sum += level.Value.GetInt64();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Checksum(count, sum);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Engines;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// jq runs as an external process on the JSON twin of the save.
|
||||||
|
/// Numbers therefore include process start and reading the file from the OS page cache,
|
||||||
|
/// which is exactly what you pay when you shell out to jq.
|
||||||
|
/// <see cref="JqNoop" /> measures that fixed overhead so it can be subtracted.
|
||||||
|
/// </summary>
|
||||||
|
[SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 1, iterationCount: 5)]
|
||||||
|
public class JqBenchmarks
|
||||||
|
{
|
||||||
|
public static string JqPath { get; set; } = "jq";
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup() => Run("--version", null);
|
||||||
|
|
||||||
|
[Benchmark(Description = "jq .countries | map_values(.technology)")]
|
||||||
|
public int Jq() => Run("-c", BenchData.JqQuery);
|
||||||
|
|
||||||
|
[Benchmark(Description = "jq empty (parse only)")]
|
||||||
|
public int JqParseOnly() => Run("-c", "empty");
|
||||||
|
|
||||||
|
[Benchmark(Description = "jq --stream (no DOM)")]
|
||||||
|
public int JqStream() => Run("-c --stream", "empty");
|
||||||
|
|
||||||
|
[Benchmark(Description = "jq process startup only (tiny input)")]
|
||||||
|
public int JqNoop()
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo(JqPath)
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardInput = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
};
|
||||||
|
psi.ArgumentList.Add("-c");
|
||||||
|
psi.ArgumentList.Add("empty");
|
||||||
|
using var p = Process.Start(psi)!;
|
||||||
|
p.StandardInput.Write("{}");
|
||||||
|
p.StandardInput.Close();
|
||||||
|
p.StandardOutput.ReadToEnd();
|
||||||
|
p.WaitForExit();
|
||||||
|
return p.ExitCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int Run(string flags, string? program)
|
||||||
|
{
|
||||||
|
var psi = new ProcessStartInfo(JqPath)
|
||||||
|
{
|
||||||
|
RedirectStandardOutput = true,
|
||||||
|
RedirectStandardError = true,
|
||||||
|
UseShellExecute = false,
|
||||||
|
};
|
||||||
|
foreach (var f in flags.Split(' ', StringSplitOptions.RemoveEmptyEntries))
|
||||||
|
psi.ArgumentList.Add(f);
|
||||||
|
if (program != null)
|
||||||
|
{
|
||||||
|
psi.ArgumentList.Add(program);
|
||||||
|
psi.ArgumentList.Add(BenchData.JsonPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
using var p = Process.Start(psi)!;
|
||||||
|
// output must be drained, otherwise jq blocks on a full pipe
|
||||||
|
long bytes = 0;
|
||||||
|
var buf = new char[1 << 16];
|
||||||
|
int read;
|
||||||
|
while ((read = p.StandardOutput.Read(buf, 0, buf.Length)) > 0)
|
||||||
|
bytes += read;
|
||||||
|
p.StandardError.ReadToEnd();
|
||||||
|
p.WaitForExit();
|
||||||
|
if (p.ExitCode != 0)
|
||||||
|
throw new Exception($"jq exited with {p.ExitCode}");
|
||||||
|
return (int)(bytes & int.MaxValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using ParadoxSaveParser.Lib;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the regex approach associates the right country tag with the right
|
||||||
|
/// technology block. It cannot: 933 of the 2803 country blocks in the test save have no
|
||||||
|
/// technology block at all, so a lazy "tag ... technology" pattern silently pairs a tag
|
||||||
|
/// with a technology block belonging to a later country.
|
||||||
|
/// </summary>
|
||||||
|
public static class Mispairing
|
||||||
|
{
|
||||||
|
public static Dictionary<string, long> FromParser(byte[] pdx)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, long>();
|
||||||
|
using var stream = new MemoryStream(pdx, false);
|
||||||
|
var root = new SaveParserEU4(stream, SearchExpressionCompiler.Compile(BenchData.PdxQuery)).Parse();
|
||||||
|
foreach (var (tag, value) in (Dictionary<string, object>)root["countries"])
|
||||||
|
{
|
||||||
|
if (value is Dictionary<string, object> c
|
||||||
|
&& c.TryGetValue("technology", out var t)
|
||||||
|
&& t is Dictionary<string, object> tech
|
||||||
|
&& tech.TryGetValue("adm_tech", out var adm))
|
||||||
|
result[tag] = (long)adm;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Dictionary<string, long> FromRegex(string text, Regex pathAware)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, long>();
|
||||||
|
foreach (Match m in pathAware.Matches(text))
|
||||||
|
result[m.Groups[1].Value] = long.Parse(m.Groups[2].ValueSpan);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Report(byte[] pdx, string text)
|
||||||
|
{
|
||||||
|
var expected = FromParser(pdx);
|
||||||
|
var actual = FromRegex(text, new Regex(Extractors.PathAwarePattern, RegexOptions.Compiled));
|
||||||
|
int missing = 0, wrong = 0, extra = 0;
|
||||||
|
foreach (var (tag, adm) in expected)
|
||||||
|
{
|
||||||
|
if (!actual.TryGetValue(tag, out long got)) { missing++; Console.WriteLine($" missing tag: {tag}"); }
|
||||||
|
else if (got != adm) wrong++;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var tag in actual.Keys)
|
||||||
|
if (!expected.ContainsKey(tag))
|
||||||
|
extra++;
|
||||||
|
|
||||||
|
Console.WriteLine($"path aware regex pairing: {expected.Count} countries have technology, " +
|
||||||
|
$"regex reported {actual.Count}; missing={missing} wrong_value={wrong} invented={extra}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<LangVersion>latest</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>disable</ImplicitUsings>
|
||||||
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
|
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="BenchmarkDotNet" Version="0.15.4" />
|
||||||
|
<PackageReference Include="PCRE.NET" Version="1.6.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\ParadoxSaveParser.Lib\ParadoxSaveParser.Lib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using System.IO;
|
||||||
|
using BenchmarkDotNet.Attributes;
|
||||||
|
using BenchmarkDotNet.Engines;
|
||||||
|
using ParadoxSaveParser.Lib;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// SearchExpression only, parsing straight from the save file on disk:
|
||||||
|
/// the file is opened inside the benchmark and is never preloaded into a byte[].
|
||||||
|
/// Run it once with and once without the internal <c>MemoryStream</c> copy in
|
||||||
|
/// <see cref="SaveParserEU4" /> to see what that copy buys.
|
||||||
|
/// </summary>
|
||||||
|
[MemoryDiagnoser]
|
||||||
|
[SimpleJob(RunStrategy.Monitoring, launchCount: 1, warmupCount: 1, iterationCount: 5)]
|
||||||
|
public class ParserInputBenchmarks
|
||||||
|
{
|
||||||
|
private ISearchExpression _query = null!;
|
||||||
|
|
||||||
|
[GlobalSetup]
|
||||||
|
public void Setup() => _query = SearchExpressionCompiler.Compile(BenchData.PdxQuery);
|
||||||
|
|
||||||
|
[Benchmark(Description = "SearchExpression over FileStream")]
|
||||||
|
public long FromFile()
|
||||||
|
{
|
||||||
|
using var file = File.OpenRead(BenchData.PdxPath);
|
||||||
|
return Extractors.SearchExpressionFromStream(file, _query);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using BenchmarkDotNet.Running;
|
||||||
|
using PCRE;
|
||||||
|
using ParadoxSaveParser.Lib;
|
||||||
|
|
||||||
|
namespace ParadoxSaveParser.Benchmarks;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
|
{
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
switch (args.FirstOrDefault())
|
||||||
|
{
|
||||||
|
case "prepare":
|
||||||
|
BenchData.PrepareJson();
|
||||||
|
return;
|
||||||
|
case "verify":
|
||||||
|
Verify();
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
BenchmarkSwitcher.FromTypes([typeof(ExtractionBenchmarks), typeof(JqBenchmarks), typeof(ParserInputBenchmarks)])
|
||||||
|
.Run(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runs every extractor once and prints its checksum and wall time,
|
||||||
|
/// so that differences in what each engine actually finds are visible before benchmarking.
|
||||||
|
/// </summary>
|
||||||
|
private static void Verify()
|
||||||
|
{
|
||||||
|
Console.WriteLine($"save: {BenchData.PdxPath} {new FileInfo(BenchData.PdxPath).Length / 1024 / 1024} MB");
|
||||||
|
Console.WriteLine($"json: {BenchData.JsonPath} {new FileInfo(BenchData.JsonPath).Length / 1024 / 1024} MB");
|
||||||
|
|
||||||
|
var pdx = BenchData.PdxBytes;
|
||||||
|
var text = BenchData.PdxText;
|
||||||
|
var json = File.ReadAllBytes(BenchData.JsonPath);
|
||||||
|
var query = SearchExpressionCompiler.Compile(BenchData.PdxQuery);
|
||||||
|
|
||||||
|
Time("SearchExpression", () => Extractors.SearchExpression(pdx, query));
|
||||||
|
Time("FullParse", () => Extractors.FullParseThenSelect(pdx));
|
||||||
|
Time("Regex flat compiled",
|
||||||
|
() => Extractors.RegexScan(new Regex(Extractors.FlatPattern, RegexOptions.Compiled), text));
|
||||||
|
Time("Regex path aware compiled",
|
||||||
|
() => Extractors.RegexScan(new Regex(Extractors.PathAwarePattern, RegexOptions.Compiled), text));
|
||||||
|
Time("Regex path aware nonbacktracking",
|
||||||
|
() => Extractors.RegexScan(new Regex(Extractors.PathAwarePattern, RegexOptions.NonBacktracking), text));
|
||||||
|
Time("Regex balanced two stage",
|
||||||
|
() => Extractors.RegexTwoStage(new Regex(Extractors.CountriesBlockPattern, RegexOptions.Compiled),
|
||||||
|
new Regex(Extractors.FlatPattern, RegexOptions.Compiled), text));
|
||||||
|
Time("PCRE flat", () => Extractors.PcreScan(new PcreRegex(Extractors.FlatPattern, PcreOptions.Compiled), text));
|
||||||
|
Time("PCRE path aware",
|
||||||
|
() => Extractors.PcreScan(new PcreRegex(Extractors.PathAwarePattern, PcreOptions.Compiled), text));
|
||||||
|
Time("Utf8JsonReader", () => Extractors.Utf8JsonReaderScan(json));
|
||||||
|
Mispairing.Report(pdx, text);
|
||||||
|
Time("JsonDocument", () => Extractors.JsonDocumentScan(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Time(string name, Func<long> f)
|
||||||
|
{
|
||||||
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
long checksum;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
checksum = f();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"{name,-34} FAILED: {ex.Message}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sw.Stop();
|
||||||
|
Console.WriteLine($"{name,-34} countries={checksum / 1_000_000,5} techSum={checksum % 1_000_000,7} {sw.ElapsedMilliseconds,7} ms");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# ParadoxSaveParser.Benchmarks
|
||||||
|
|
||||||
|
Compares the query engine of this repo (`SearchExpression` + `SaveParserEU4`) against
|
||||||
|
regex engines and `jq` on one realistic task:
|
||||||
|
|
||||||
|
> extract the `technology` block (adm/dip/mil level) of every country from an EU4 save
|
||||||
|
|
||||||
|
| engine | query |
|
||||||
|
|---|---|
|
||||||
|
| this repo | `countries.*.technology` |
|
||||||
|
| jq | `.countries \| map_values(.technology)` |
|
||||||
|
| .NET `Regex` / PCRE.NET | see `Extractors.FlatPattern` / `Extractors.PathAwarePattern` |
|
||||||
|
| `Utf8JsonReader` / `JsonDocument` | hand written navigation |
|
||||||
|
|
||||||
|
## Corpus
|
||||||
|
|
||||||
|
* `gamestate` — a real 109 MB EU4 1.37 save (text format, 6.0M lines, 2803 country blocks,
|
||||||
|
1870 of which have a `technology` block).
|
||||||
|
* `gamestate.min.json` — 92 MB compact JSON twin, produced from the save by this project's
|
||||||
|
own parser. It exists because jq and the `System.Text.Json` engines cannot read the
|
||||||
|
Paradox format at all.
|
||||||
|
|
||||||
|
The save is **not** in the repository. Point `PSP_BENCH_DATA` at the directory holding it,
|
||||||
|
or drop it in `ParadoxSaveParser.CLI/bin/Debug/net10.0/` (the default location).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```sh
|
||||||
|
dotnet build ParadoxSaveParser.Benchmarks -c Release
|
||||||
|
cd ParadoxSaveParser.Benchmarks/bin/Release/net10.0
|
||||||
|
|
||||||
|
./ParadoxSaveParser.Benchmarks.exe prepare # build the JSON twin (once)
|
||||||
|
./ParadoxSaveParser.Benchmarks.exe verify # one run of every engine + correctness check
|
||||||
|
./ParadoxSaveParser.Benchmarks.exe --filter '*' # full BenchmarkDotNet run
|
||||||
|
```
|
||||||
|
|
||||||
|
Every engine returns the checksum `countries * 1_000_000 + sum_of_tech_levels`, so `verify`
|
||||||
|
shows immediately when an engine finds something different from the others.
|
||||||
|
|
||||||
|
## Fairness notes
|
||||||
|
|
||||||
|
* All in-process engines read from memory; file I/O is excluded. jq is an external process,
|
||||||
|
so its numbers include process start (measured separately by the `jq startup only`
|
||||||
|
benchmark) and reading the JSON from the OS page cache.
|
||||||
|
* The regex engines work on the save text, which is 109 MB; jq works on 92 MB of JSON.
|
||||||
|
* jq is the only engine here that is a general query language: it can do arithmetic,
|
||||||
|
filtering and reshaping, which neither the regexes nor `SearchExpression` can.
|
||||||
|
|
||||||
|
## Results
|
||||||
|
|
||||||
|
AMD Ryzen 7 5700X, 16 logical cores, Windows 10 21H2, .NET 10.0.12, jq 1.8.2,
|
||||||
|
109 MB save / 92 MB JSON twin, BenchmarkDotNet `RunStrategy.Monitoring`, 5 iterations.
|
||||||
|
|
||||||
|
| engine | mean | vs this project | allocated | correct? |
|
||||||
|
|---|---:|---:|---:|---|
|
||||||
|
| .NET Regex flat, source generated | 14.8 ms | 0.02x | 1.2 MB | path-blind |
|
||||||
|
| .NET Regex flat, compiled | 17.0 ms | 0.03x | 1.2 MB | path-blind |
|
||||||
|
| .NET Regex flat, interpreted | 19.3 ms | 0.03x | 1.2 MB | path-blind |
|
||||||
|
| PCRE.NET flat, JIT compiled | 97.4 ms | 0.15x | 0.9 MB | path-blind |
|
||||||
|
| .NET Regex balanced block + flat | 112.3 ms | 0.17x | 224 MB | yes |
|
||||||
|
| .NET Regex path aware, compiled | 116.2 ms | 0.18x | 1.4 MB | 1869/1870 |
|
||||||
|
| PCRE.NET path aware, JIT compiled | 122.9 ms | 0.19x | 0.9 MB | 1869/1870 |
|
||||||
|
| Utf8JsonReader over JSON twin | 155.7 ms | 0.24x | 0 B | yes |
|
||||||
|
| JsonDocument over JSON twin | 321.4 ms | 0.50x | 0 B (native) | yes |
|
||||||
|
| .NET Regex path aware, NonBacktracking | 444.5 ms | 0.69x | 32 MB | 1869/1870 |
|
||||||
|
| **SearchExpression (this project)** | **645.1 ms** | **1.00x** | **1.6 MB** | yes |
|
||||||
|
| Full parse, then select | 1831 ms | 2.84x | 1153 MB | yes |
|
||||||
|
| jq `.countries \| map_values(.technology)` | 4340 ms | 6.73x | n/a | yes |
|
||||||
|
| jq `empty` (parse the file, emit nothing) | 4227 ms | 6.55x | n/a | n/a |
|
||||||
|
| jq `--stream` | 17490 ms | 27.1x | n/a | n/a |
|
||||||
|
| jq process startup only | 4.2 ms | 0.01x | n/a | n/a |
|
||||||
|
|
||||||
|
### Reading the table
|
||||||
|
|
||||||
|
* **jq is ~6.7x slower than this parser** and 97% of that time is JSON parsing, not the
|
||||||
|
query: `jq empty` on the same file costs 4227 ms of the 4340 ms. Process startup is
|
||||||
|
negligible (4 ms). jq's `--stream` mode, often recommended for large inputs, is 4x
|
||||||
|
*slower* still. jq also needs the data converted to JSON first, which this parser has to
|
||||||
|
do anyway — so end to end jq is strictly more expensive here.
|
||||||
|
* **The regexes are 5-40x faster, but they are not doing the same job.** A regex never
|
||||||
|
parses the structure; it scans bytes for a literal and validates a short window around it.
|
||||||
|
`Regex` with a literal prefix (`technology={`) is vectorized, so 109 MB is scanned at
|
||||||
|
several GB/s. That speed is real and the cost is real too: nothing verifies that the hit
|
||||||
|
is under `countries`, at the right depth, or belongs to the tag matched before it.
|
||||||
|
* **Path-aware regexes lose both the speed and the correctness.** Forcing the tag into the
|
||||||
|
pattern costs 7x (17 ms -> 116 ms) and still returns 1869 of 1870 countries: the tag class
|
||||||
|
`[A-Z0-9]{3}` silently drops EU4's `---` pseudo-country. Pairing survives here only by
|
||||||
|
luck of the file layout — all 933 tech-less country blocks happen to be grouped at the end
|
||||||
|
of the save, so the lazy gap never runs across one. A save that interleaves them would
|
||||||
|
make the regex report a technology block under the wrong country tag with no error.
|
||||||
|
* **`RegexOptions.NonBacktracking`** guarantees linear time but is 26x slower than the
|
||||||
|
compiled backtracking engine on this pattern and allocates 32 MB.
|
||||||
|
* **PCRE.NET** (the most used non-BCL regex engine in .NET, ~460k downloads) is 6x slower
|
||||||
|
than `System.Text.RegularExpressions` on the flat pattern, because .NET's vectorized
|
||||||
|
literal prefix search beats PCRE2's JIT here. On the path-aware pattern the two are equal.
|
||||||
|
There is no reason to leave the BCL engine for this workload.
|
||||||
|
* **The query is what makes this parser fast, not the parsing.** Same parser, same file:
|
||||||
|
645 ms with `countries.*.technology`, 1831 ms and 1.1 GB allocated without a query. The
|
||||||
|
search expression prunes ~65% of the work and 99.9% of the allocations.
|
||||||
|
* **Against a JSON reader on equivalent data**, the parser is 4x slower than `Utf8JsonReader`
|
||||||
|
and 2x slower than `JsonDocument`. Those numbers exclude the pdx -> JSON conversion
|
||||||
|
(~4.7 s), so they are a ceiling for the format, not a usable alternative.
|
||||||
|
|
||||||
|
### Where this parser's time goes
|
||||||
|
|
||||||
|
645 ms for 109 MB is ~170 MB/s, or ~13 cycles per input byte. The lexer reads the save one
|
||||||
|
byte at a time through `Stream.ReadByte()` (`SaveParserEU4.LexTextSave`), which is a virtual
|
||||||
|
call plus bounds check per byte, and appends char by char into a `StringBuilder`. Reading
|
||||||
|
into a `byte[]` buffer and scanning it with `ReadOnlySpan<byte>.IndexOfAny` would be the
|
||||||
|
first thing to try — the regex numbers above show what the same hardware does when it scans
|
||||||
|
a span instead of a stream.
|
||||||
@@ -15,27 +15,80 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.Lib.Tests
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.CLI", "ParadoxSaveParser.CLI\ParadoxSaveParser.CLI.csproj", "{2D4448A6-390D-47F3-9BB7-6266669719DE}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.CLI", "ParadoxSaveParser.CLI\ParadoxSaveParser.CLI.csproj", "{2D4448A6-390D-47F3-9BB7-6266669719DE}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ParadoxSaveParser.Benchmarks", "ParadoxSaveParser.Benchmarks\ParadoxSaveParser.Benchmarks.csproj", "{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{7D377558-CE40-4D7E-B0B1-FB33C475AE6A}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{53ED0135-9513-4DE2-9187-CF2899F179B3}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.Build.0 = Release|Any CPU
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{23F4BE1B-3043-4821-9F65-74FF5F57FA59}.Release|x86.Build.0 = Release|Any CPU
|
||||||
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{2D4448A6-390D-47F3-9BB7-6266669719DE}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{0E35F6F0-3D74-4C18-BFC8-A1C45A04A410}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
# Paradox Save Parser
|
# Paradox Save Parser
|
||||||
Yet another save files parser.
|
Yet another save files parser.
|
||||||
|
|
||||||
## Supported games:
|
## Supported games:
|
||||||
- EU4
|
- EU4
|
||||||
|
|
||||||
## Project structure
|
## Project structure
|
||||||
- **[ParadoxSaveParser.Lib](./ParadoxSaveParser.Lib)** -
|
- **[ParadoxSaveParser.WebAPI](./ParadoxSaveParser.WebAPI)** -
|
||||||
Parser itself
|
Backend for my save file analytics website (TODO: add repo link).
|
||||||
- **[ParadoxSaveParser.Lib.Tests](./ParadoxSaveParser.Lib.Tests)** -
|
- **[ParadoxSaveParser.CLI](./ParadoxSaveParser.CLI)** -
|
||||||
Tests for parser
|
Command line tool to parse save files. Can be used in interactive mode.
|
||||||
- **[ParadoxSaveParser.CLI](./ParadoxSaveParser.CLI)** -
|
- **[ParadoxSaveParser.Lib](./ParadoxSaveParser.Lib)** -
|
||||||
Command line tool to parse save files. Can be used in interactive mode.
|
Parser itself
|
||||||
- **[ParadoxSaveParser.WebAPI](./ParadoxSaveParser.WebAPI)** -
|
- **[ParadoxSaveParser.Lib.Tests](./ParadoxSaveParser.Lib.Tests)** -
|
||||||
Backend for my save file analytics website (TODO: add repo link).
|
Tests for parser
|
||||||
|
- **[ParadoxSaveParser.Benchmarks](./ParadoxSaveParser.Benchmarks)** -
|
||||||
## Building
|
Performance comparison of the search expression engine against regex engines and jq
|
||||||
1. Install protobuf compiler https://protobuf.dev/installation/
|
|
||||||
|
## Building
|
||||||
|
1. Install protobuf compiler https://protobuf.dev/installation/
|
||||||
|
|||||||
Reference in New Issue
Block a user