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 /// /// Matches a single technology block. On its own it would match anywhere in the file, /// so it is only used as the second stage of , inside the /// already extracted countries block. /// public const string TechnologyBlockPattern = @"\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(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); } }