Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Summarize ERR PERF windows from performance logs (also accepts older console logs).
|
||||
|
||||
Script wall_ms_per_s measures scoped elapsed time, not CPU utilization.
|
||||
Parent/child scopes overlap; engine values are sampled gauges, not scopes.
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
FIELDS = (
|
||||
'run', 'window', 'elapsed_ms', 'final', 'phase_start', 'phase_end',
|
||||
'metric', 'samples', 'sum', 'min', 'max', 'zero', 'over50', 'over100',
|
||||
)
|
||||
INTEGER_FIELDS = ('window', 'elapsed_ms', 'phase_start', 'phase_end',
|
||||
'samples', 'zero', 'over50', 'over100')
|
||||
SCRIPT_METRICS = {
|
||||
'mission_tick_ms', 'maintenance_ms', 'civilians_ms', 'forces_ms',
|
||||
'observers_ms', 'hq_tick_ms', 'listeners_ms', 'hq_prep_ms',
|
||||
'hq_hunters_ms', 'hq_garrisons_ms', 'hq_convoys_ms', 'hq_bodies_ms',
|
||||
'hq_radio_index_ms', 'hq_reports_ms', 'hq_planner_ms', 'hq_support_ms',
|
||||
'hq_journal_ms', 'garrison_work_ms', 'performance_flush_ms',
|
||||
}
|
||||
|
||||
|
||||
def read_rows(paths):
|
||||
seen = {}
|
||||
for path in paths:
|
||||
with path.open(encoding='utf-8', errors='replace') as stream:
|
||||
for number, line in enumerate(stream, 1):
|
||||
marker = line.find('ERR PERF run=')
|
||||
if marker < 0:
|
||||
continue
|
||||
values = dict(re.findall(r'(\w+)=([^\s]+)', line[marker:]))
|
||||
try:
|
||||
row = {key: values[key] for key in FIELDS}
|
||||
for key in INTEGER_FIELDS:
|
||||
row[key] = int(row[key])
|
||||
if row[key] < 0:
|
||||
raise ValueError(f'negative {key}')
|
||||
for key in ('sum', 'min', 'max'):
|
||||
row[key] = float(row[key])
|
||||
if not math.isfinite(row[key]) or row[key] < 0:
|
||||
raise ValueError(f'invalid {key}')
|
||||
if row['elapsed_ms'] == 0 or row['samples'] == 0:
|
||||
raise ValueError('empty window/sample count')
|
||||
if row['min'] > row['max']:
|
||||
raise ValueError('min exceeds max')
|
||||
if not 0 <= row['over100'] <= row['over50'] <= row['samples']:
|
||||
raise ValueError('invalid threshold counts')
|
||||
if row['zero'] > row['samples']:
|
||||
raise ValueError('invalid zero count')
|
||||
except (KeyError, ValueError) as error:
|
||||
raise ValueError(f'{path}:{number}: malformed performance row: {error}') from error
|
||||
key = row['run'], row['window'], row['metric']
|
||||
if key in seen:
|
||||
if seen[key] != row:
|
||||
raise ValueError(f'{path}:{number}: conflicting duplicate {key}')
|
||||
continue
|
||||
seen[key] = row
|
||||
return list(seen.values())
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('logs', nargs='+', type=Path)
|
||||
parser.add_argument('--csv', action='store_true', help='output individual window rows as CSV')
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
rows = read_rows(args.logs)
|
||||
if not rows:
|
||||
raise ValueError('no ERR PERF measurement rows found')
|
||||
except (OSError, ValueError) as error:
|
||||
parser.error(str(error))
|
||||
if args.csv:
|
||||
writer = csv.DictWriter(sys.stdout, fieldnames=FIELDS)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
return
|
||||
grouped = defaultdict(list)
|
||||
for row in rows:
|
||||
grouped[row['run'], row['metric']].append(row)
|
||||
print('Scope times overlap: do not add mission_tick, hq_tick and HQ children.')
|
||||
print('Wall timings have 1 ms resolution; zero does not prove zero cost.')
|
||||
print('Engine metrics are ~1 Hz samples of engine-reported values, NOT AI-only time.')
|
||||
previous_run = None
|
||||
for (run, metric), items in sorted(grouped.items()):
|
||||
if run != previous_run:
|
||||
print(f'\nRun {run}')
|
||||
print(f'{"metric":26s} {"samples":>8s} {"mean":>10s} {"max":>10s} {"zero":>8s} {"wall_ms/s":>10s}')
|
||||
previous_run = run
|
||||
count = sum(item['samples'] for item in items)
|
||||
total = sum(item['sum'] for item in items)
|
||||
elapsed = sum(item['elapsed_ms'] for item in items)
|
||||
maximum = max(item['max'] for item in items)
|
||||
zero = sum(item['zero'] for item in items)
|
||||
rate = f'{1000 * total / elapsed:.3f}' if metric in SCRIPT_METRICS else '-'
|
||||
print(f'{metric:26s} {count:8d} {total / count:10.3f} {maximum:10.3f} {zero:8d} {rate:>10s}')
|
||||
print('\nMeans/rates include only windows containing that metric; use --csv to compare matching windows and player counts.')
|
||||
print('No native-AI timing or predicted offload speedup is inferred.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user