210 lines
8.0 KiB
C#
210 lines
8.0 KiB
C#
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);
|
|
}
|
|
}
|