"""Воспроизводимый замер скорости формирования отчёта (без Streamlit и OpenCloud). Генерирует N синтетических XML-файлов по M операций с двумя участниками (ФЛИП с адресом и документом, ЮЛ с ЕИО и бенефициаром), прогоняет их через parse_xml_content + StreamingReportWriter и печатает время по стадиям и стоимость одной строки отчёта. Это тот же путь, что и в run_batch, минус распаковка вложений и загрузка результата. Пример: python scripts/bench_report.py # 230 файлов × 20 операций python scripts/bench_report.py --files 50 --ops 100 python scripts/bench_report.py --profile # топ функций по cProfile Ориентир (Apple M-серия, Python 3.13): ~0.25 мс на строку отчёта. На сервере с квотой 0.25 CPU (raisa_config.yml, профиль «Малый») ожидайте в 8–12 раз медленнее. """ from __future__ import annotations import argparse import cProfile import pstats import sys import tempfile import time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) from app.pipeline.parser import parse_xml_content # noqa: E402 from app.pipeline.report import ReportRow, StreamingReportWriter # noqa: E402 def _operation(index: int) -> str: return f""" <Операция> <ИдентификаторЗаписи>OP{index} <КодОперации>6001 <ДатаОперации>2026-06-16 <СуммаОперации>12345.67 <ВалютаОперации>643 <СуммаОперацииРуб>12345.67 <НазначениеПлатежа>Оплата по договору №{index} от 01.06.2026 <ХарактеристикаОперации>Перевод денежных средств клиента <ОснованиеОп><КодДок>21<НомДок>{index}<ДатаДок>2026-06-01 <ОснованиеОп><КодДок>22<НомДок>{index}-b<ДатаДок>2026-06-02 <СведПереводДС> <ВидПеревода>1 <СведБанкПлательщик><БИККО>044525111<НаимБанк>АО Банк<НомерСчета>40702810000000000001 <СведБанкПолучатель><БИККО>044525222<НаимБанк>ПАО Банк2<НомерСчета>40702810000000000002 <УчастникОп> <ТипУчастника>2 <УчастникФЛИП> <СведФЛИП> <ФИО><Фамилия>Иванов<Имя>Иван<Отчество>Иванович <ИННФЛИП>123456789012 <ДатаРождения>1980-01-01 <КодОКСМ>643 <СведДокУдЛичн><КодДок>21<СерияДок>4510<НомДок>123456<ДатаДок>2010-01-01<КемВыдан>ОУФМС России по г. Москве <АдрРег Индекс="674500" КодОКСМ="643" КодСубъектаПоОКАТО="45" Район="р-н" Пункт="Москва г" Улица="Ленина ул" Дом="1" Корпус="2" Квартира="3"/> <УчастникОп> <ТипУчастника>1 <УчастникЮЛ> <СведЮЛ> <НаимЮЛ>ООО Ромашка<ИННЮЛ>7701234567<КППЮЛ>770101001<ОГРН>1027700000001 <АдрРегЮЛ Индекс="675520" КодОКСМ="643" КодСубъектаПоОКАТО="10" Район="Благовещенский р-н" Пункт="Чигири с" Улица="Зеленая ул" Дом="1"/> <СведЕИО><ФЛЕИО><ФИО><Фамилия>Петров<Имя>Петр<ИННФЛИП>111111111111 <БенефициарЮЛ><ФЛЕИО><ФИО><Фамилия>Сидоров<Имя>Сидор<ИННФЛИП>222222222222 """ def make_xml(operations: int) -> bytes: body = "".join(_operation(index) for index in range(operations)) return f""" <СообщОперКО xmlns="urn:test"> <СлужЧасть><ИдФайл>ID1<ДатаФайла>2026-06-16 <ИнформЧасть> <ИнфБанк><БИК>044525111<НаимКО>АО Банк <СведКО> <ИнфФилиал><НомерФилиала>01 {body} """.encode( "utf-8" ) def run(files: int, operations: int) -> dict[str, float]: xml = make_xml(operations) started = time.perf_counter() writer = StreamingReportWriter() rows = 0 parse_seconds = 0.0 append_seconds = 0.0 for number in range(files): file_name = f"SKO115FZ_01_044525111_20260616_{number:06d}.xml" stage = time.perf_counter() result = parse_xml_content(file_name=file_name, xml_content=xml) parse_seconds += time.perf_counter() - stage stage = time.perf_counter() for row in result.rows: writer.append_row( ReportRow( file_name=row.file_name, record_id=row.record_id, operation_index=row.operation_index, operation_fields=row.operation_fields, participant_fields=row.participant_fields, is_continuation=row.is_continuation, ) ) append_seconds += time.perf_counter() - stage rows += len(result.rows) with tempfile.TemporaryDirectory() as temp_dir: stage = time.perf_counter() report_path = Path(temp_dir) / "report.xlsx" writer.save(report_path) save_seconds = time.perf_counter() - stage report_size_mb = report_path.stat().st_size / 2**20 return { "files": files, "operations_per_file": operations, "xml_kb": len(xml) / 1024, "rows": rows, "total_seconds": time.perf_counter() - started, "parse_seconds": parse_seconds, "append_seconds": append_seconds, "save_seconds": save_seconds, "ms_per_row": append_seconds / rows * 1000 if rows else 0.0, "report_mb": report_size_mb, } def main() -> int: parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) parser.add_argument("--files", type=int, default=230) parser.add_argument("--ops", type=int, default=20, help="операций в файле") parser.add_argument("--profile", action="store_true", help="печатать cProfile") args = parser.parse_args() profiler = cProfile.Profile() if args.profile else None if profiler is not None: profiler.enable() stats = run(args.files, args.ops) if profiler is not None: profiler.disable() print( f"files={stats['files']} ops/file={stats['operations_per_file']} " f"xml={stats['xml_kb']:.0f} KB rows={stats['rows']}" ) print( f"total={stats['total_seconds']:.1f}s parse={stats['parse_seconds']:.1f}s " f"append={stats['append_seconds']:.1f}s save={stats['save_seconds']:.1f}s " f"report={stats['report_mb']:.1f} MB" ) print(f"per row (parse excluded): {stats['ms_per_row']:.3f} ms") if profiler is not None: pstats.Stats(profiler).sort_stats("cumulative").print_stats(25) return 0 if __name__ == "__main__": raise SystemExit(main())