#!/usr/bin/env python3 """Summarize EdgeWeightGPS route-result records from RED4ext logs.""" from __future__ import annotations import argparse import collections import dataclasses import json import re from pathlib import Path from typing import Any RE_TIMESTAMP = re.compile(r"^(\d{4}-\d\d-\d\d \d\d:\d\d:\d\d\.\d{3})") RE_ELAPSED_MS = re.compile(r" \+(\d+)ms ") RE_RECORD_BLOCK = re.compile(r"gpsResult_ptr28_rec40=\[(.*?)\]") RE_RECORD = re.compile(r"(\d+):([0-9a-f]+),0x([0-9a-f]{8}),([^;\]]+)") RE_JOB_COUNT = re.compile(r"GPSRouteJobBuild result call=(\d+).*?job_count50=(\d+)/0x([0-9a-f]+)") RE_FIELD_DEC_HEX = re.compile(r"([A-Za-z0-9_]+)=(-?\d+)/0x([0-9a-fA-F]+)") RE_FIELD_HEX = re.compile(r"([A-Za-z0-9_]+)=0x([0-9a-fA-F]+)") RE_FIELD_DEC = re.compile(r"([A-Za-z0-9_]+)=(-?\d+)(?= |$)") FLAGS = { "FromRoadSpline": 1, "Bidirectional": 2, "PatrolRoute": 4, "Pavement": 8, "Road": 16, "Intersection": 32, "NeverDeadEnd": 64, "TrafficDisabled": 128, "CrossWalk": 256, "GPSOnly": 512, "ShowDebug": 1024, "Blockade": 2048, "Yield": 4096, "NoAIDriving": 8192, "Highway": 16384, "NoAutodrive": 32768, } @dataclasses.dataclass class SubmitInfo: line_no: int timestamp: str elapsed_ms: int | None call: int fields: dict[str, int] @dataclasses.dataclass class RouteRecord: index: int handle: int packed: int parts: list[str] @property def low_class(self) -> int: return self.packed & 0xFF @dataclasses.dataclass class RouteResult: line_no: int timestamp: str elapsed_ms: int | None query_id: int | None submit_call: int | None submit_ret_rva: int | None fields: dict[str, int] records: list[RouteRecord] def bytes_le(value: int) -> tuple[int, int, int, int]: return tuple((value >> (8 * index)) & 0xFF for index in range(4)) def flag_value(flags: Any) -> int: if isinstance(flags, dict): return int(flags.get("Value", flags.get("$value", 0)) or 0) return int(flags or 0) def lane_category(flags: int) -> str: if flags & FLAGS["Highway"]: return "highway" if flags & FLAGS["GPSOnly"]: return "gpsonly" if flags & FLAGS["Road"]: return "road" if flags & FLAGS["Pavement"]: return "pavement" return "other" def parse_timestamp(line: str) -> str: match = RE_TIMESTAMP.search(line) return match.group(1) if match else "" def parse_elapsed_ms(line: str) -> int | None: match = RE_ELAPSED_MS.search(line) return int(match.group(1)) if match else None def parse_fields(line: str) -> dict[str, int]: fields: dict[str, int] = {} for match in RE_FIELD_DEC_HEX.finditer(line): fields[match.group(1)] = int(match.group(2)) for match in RE_FIELD_HEX.finditer(line): fields.setdefault(match.group(1), int(match.group(2), 16)) for match in RE_FIELD_DEC.finditer(line): fields.setdefault(match.group(1), int(match.group(2))) return fields def load_lane_lookup(path: Path | None) -> dict[int, dict[str, Any]]: if not path: return {} data = json.loads(path.read_text(encoding="utf-8")) lanes = data["Data"]["RootChunk"]["data"]["lanes"] lookup: dict[int, dict[str, Any]] = {} for index, lane in enumerate(lanes): node_hash = lane.get("nodeRefHash") if node_hash is None: continue flags = flag_value(lane.get("flags")) lookup[int(node_hash)] = { "index": index, "flags": flags, "category": lane_category(flags), "speed": flag_value(lane.get("maxSpeed")), "length": lane.get("length"), } return lookup def parse_route_records(block: str) -> list[RouteRecord]: records: list[RouteRecord] = [] for record_match in RE_RECORD.finditer(block): pieces = record_match.group(4).split(",") if len(pieces) < 4: continue records.append( RouteRecord( index=int(record_match.group(1)), handle=int(record_match.group(2), 16), packed=int(record_match.group(3), 16), parts=pieces, ) ) return records def iter_route_results(path: Path) -> tuple[dict[int, SubmitInfo], list[RouteResult], list[int]]: submits: dict[int, SubmitInfo] = {} results: list[RouteResult] = [] job_counts: list[int] = [] for line_no, line in enumerate(path.read_text(encoding="utf-8", errors="replace").splitlines(), start=1): fields = parse_fields(line) if "hook GPSQuerySubmit 0x70a42c" in line: call = fields.get("call") if call is not None: submits[call] = SubmitInfo( line_no=line_no, timestamp=parse_timestamp(line), elapsed_ms=parse_elapsed_ms(line), call=call, fields=fields, ) job_match = RE_JOB_COUNT.search(line) if job_match: job_counts.append(int(job_match.group(2))) block_match = RE_RECORD_BLOCK.search(line) if not block_match: continue records = parse_route_records(block_match.group(1)) if not records: continue results.append( RouteResult( line_no=line_no, timestamp=parse_timestamp(line), elapsed_ms=parse_elapsed_ms(line), query_id=fields.get("queryId"), submit_call=fields.get("submitCall"), submit_ret_rva=fields.get("submitRetRva"), fields=fields, records=records, ) ) return submits, results, job_counts def summarize_route_result(route: RouteResult, lane_lookup: dict[int, dict[str, Any]]) -> tuple[dict[str, Any], int]: class_counts = collections.Counter(record.low_class for record in route.records) category_counts: collections.Counter[str] = collections.Counter() category_lengths: collections.Counter[str] = collections.Counter() speed_lengths: collections.Counter[int] = collections.Counter() unmatched = 0 for record in route.records: lane = lane_lookup.get(record.handle) if not lane: unmatched += 1 continue category = lane["category"] length = float(lane.get("length") or 0.0) category_counts[category] += 1 category_lengths[category] += length speed_lengths[int(lane["speed"])] += length total_length = sum(category_lengths.values()) summary = { "class_counts": class_counts, "category_counts": category_counts, "category_lengths": category_lengths, "speed_lengths": speed_lengths, "total_length": total_length, "unmatched": unmatched, } return summary, len(route.records) - unmatched def format_counter(counter: collections.Counter[Any], limit: int | None = None, digits: int | None = None) -> str: items = counter.most_common(limit) parts = [] for key, value in items: if digits is None: parts.append(f"{key}:{value}") else: parts.append(f"{key}:{value:.{digits}f}") return "{" + ", ".join(parts) + "}" def print_per_route( path: Path, submits: dict[int, SubmitInfo], route_results: list[RouteResult], lane_lookup: dict[int, dict[str, Any]], ) -> None: print(" per-route results:") for ordinal, route in enumerate(route_results, start=1): submit = submits.get(route.submit_call) if route.submit_call is not None else None summary, matched = summarize_route_result(route, lane_lookup) span = route.fields.get("gpsResult_u20") submit_fields = submit.fields if submit else {} ret_rva = route.submit_ret_rva ret_text = f"0x{ret_rva:x}" if ret_rva is not None else "?" submit_ms = submit.elapsed_ms if submit else None result_ms = route.elapsed_ms duration_ms = result_ms - submit_ms if submit_ms is not None and result_ms is not None else None submit_marker = f"+{submit_ms}ms" if submit_ms is not None else "?" result_marker = f"+{result_ms}ms" if result_ms is not None else "?" duration_marker = f"{duration_ms}ms" if duration_ms is not None else "?" print( f" route#{ordinal} qid={route.query_id} submitCall={route.submit_call} " f"submit={submit_marker} result={result_marker} duration={duration_marker} line={route.line_no} " f"ret={ret_text} records={len(route.records)} spanU20={span} matched={matched}" ) if submit_fields: print( " query " f"f08=0x{submit_fields.get('query_f08', 0):x} " f"f0c={submit_fields.get('query_f0c')} " f"fcc={submit_fields.get('query_fcc')}" ) if lane_lookup: category_lengths = summary["category_lengths"] total = summary["total_length"] print( " resource " f"segments={format_counter(summary['category_counts'])} " f"length={format_counter(category_lengths, digits=1)} " f"total={total:.1f} " f"speedLength={format_counter(summary['speed_lengths'], limit=6, digits=1)} " f"unmatched={summary['unmatched']}" ) print(f" routeClasses={format_counter(summary['class_counts'])}") def summarize(path: Path, lane_lookup: dict[int, dict[str, Any]]) -> None: submits, route_results, job_counts = iter_route_results(path) routes = 0 records = 0 first_byte = collections.Counter() byte_positions: list[collections.Counter[int]] = [collections.Counter() for _ in range(4)] patterns = collections.Counter() segments = collections.Counter() class_categories: dict[int, collections.Counter[str]] = collections.defaultdict(collections.Counter) class_flags: dict[int, collections.Counter[int]] = collections.defaultdict(collections.Counter) class_speed: dict[int, collections.Counter[int]] = collections.defaultdict(collections.Counter) matched_handles = 0 unmatched_handles = 0 for route in route_results: route_records = 0 for record in route.records: route_records += 1 records += 1 bytes_tuple = bytes_le(record.packed) class_id = bytes_tuple[0] patterns[bytes_tuple] += 1 first_byte[class_id] += 1 for index, byte in enumerate(bytes_tuple): byte_positions[index][byte] += 1 segments[(record.handle, class_id)] += 1 if lane_lookup: lane = lane_lookup.get(record.handle) if lane: matched_handles += 1 class_categories[class_id][lane["category"]] += 1 class_flags[class_id][lane["flags"]] += 1 class_speed[class_id][lane["speed"]] += 1 else: unmatched_handles += 1 if route_records: routes += 1 print(f"{path}:") print(f" routes={routes} records={records}") if job_counts: sorted_counts = sorted(job_counts) p50 = sorted_counts[len(sorted_counts) // 2] p90 = sorted_counts[min(len(sorted_counts) - 1, int(len(sorted_counts) * 0.9))] avg = sum(sorted_counts) / len(sorted_counts) print( " job_count50 " f"n={len(sorted_counts)} min={sorted_counts[0]} p50={p50} " f"p90={p90} max={sorted_counts[-1]} avg={avg:.1f}" ) print(" low-byte class counts:", dict(first_byte.most_common())) if lane_lookup: print(f" matched route records to lane hashes: {matched_handles}/{matched_handles + unmatched_handles}") print(" class -> resource categories:") for class_id, count in first_byte.most_common(): categories = dict(class_categories[class_id].most_common()) speeds = dict(class_speed[class_id].most_common(8)) flags = dict(class_flags[class_id].most_common(8)) print(f" {class_id}: categories={categories} speeds={speeds} flags={flags}") print_per_route(path, submits, route_results, lane_lookup) for index, counter in enumerate(byte_positions): print(f" byte{index} counts:", dict(counter.most_common(12))) print(" packed metadata patterns:") for pattern, count in patterns.most_common(24): print(f" {pattern}: {count}") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--traffic-json", type=Path) parser.add_argument("logs", nargs="+", type=Path) args = parser.parse_args() lane_lookup = load_lane_lookup(args.traffic_json) for path in args.logs: summarize(path, lane_lookup) return 0 if __name__ == "__main__": raise SystemExit(main())