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;
///
/// Every extractor solves the same task: pull the technology block
/// (adm/dip/mil levels) of every country out of an EU4 save, i.e. the path
/// countries.*.technology.
/// They all return the same checksum so the benchmarks can be verified against each other:
/// number_of_countries * 1_000_000 + sum_of_all_tech_levels.
///
public static partial class Extractors
{
public static long Checksum(long count, long techSum) => count * 1_000_000 + techSum;
// ---------------------------------------------------------------- this project
/// The parser of this repo: single streaming pass, guided by a compiled search expression.
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);
}
/// Same query, but the parser reads from the given stream instead of a preloaded byte[].
public static long SearchExpressionFromStream(Stream stream, ISearchExpression query)
{
var root = new SaveParserEU4(stream, query).Parse();
return ChecksumOfParsedTree(root);
}
/// Same parser without a query: parses the whole save into memory (worst case reference point).
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 root)
{
long count = 0, sum = 0;
var countries = (Dictionary)root["countries"];
foreach (var country in countries.Values)
{
if (country is not Dictionary c
|| !c.TryGetValue("technology", out var t)
|| t is not Dictionary tech)
continue;
count++;
foreach (var level in tech.Values)
sum += (long)level;
}
return Checksum(count, sum);
}
// ---------------------------------------------------------------- .NET Regex
///
/// Flat scan: matches technology blocks anywhere in the file.
/// Fastest regex option, but it does not verify that the match really sits under countries.
///
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\}";
///
/// 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.
///
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\}";
///
/// Balancing-group pattern isolating the whole countries={...} block.
/// .NET-only feature; PCRE would need recursion instead.
///
public const string CountriesBlockPattern =
@"\ncountries=\{(?>[^{}]+|\{(?)|\}(?<-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);
}
/// Two stages: cut out the countries block with balanced braces, then scan inside it.
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();
/// Hand written streaming reader over the JSON twin: the closest JSON analogue of the search expression.
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);
}
/// DOM approach: parse the whole JSON document, then navigate. The .NET equivalent of what jq does.
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);
}
}