chore(task): archive 09-07-api-performance-diagnosis

This commit is contained in:
yuxuanhui
2026-09-25 23:48:22 +08:00
parent 1197b78f9a
commit 62c66f02ad
8 changed files with 3 additions and 3 deletions
@@ -0,0 +1,126 @@
"""Task-local benchmark: isolated localhost PostgreSQL only; no production writes.
Run from zhixing-server with DATABASE_URL pointing to a disposable test database.
Set PYTHONPATH to the before/after source tree to compare the same persisted data.
Use --seed once, then --run before.json / --run after.json (five reads per endpoint).
"""
import argparse
import hashlib
import json
import os
import runpy
import statistics
import time
from dataclasses import replace
from datetime import timedelta
from decimal import Decimal
from pathlib import Path
from urllib.parse import urlsplit
from unittest.mock import patch
import psycopg
from alembic import command
from alembic.config import Config
from zhixing_server.bootstrap.config import Settings, sqlalchemy_database_url
from zhixing_server.modules.sector_radar.application.details import ReadRadarDetails
from zhixing_server.modules.sector_radar.application.read import ReadSectorRadar, RadarQuery
from zhixing_server.modules.sector_radar.domain.models import PublicationStatus, SectorType, MetricKind, MetricUnit
from zhixing_server.modules.sector_radar.domain.metrics import AmountNetStrategy, RatioTurnoverStrategy, SwingEqualThreeToTenStrategy
from zhixing_server.modules.sector_radar.domain.persistence import PublicationSourceGroup as Group, PublicationSourceRecord, RankingRecord
from zhixing_server.modules.sector_radar.domain.ranking import rank_metric_observations
from zhixing_server.modules.sector_radar.domain.source import build_source_snapshot
from zhixing_server.modules.sector_radar.infrastructure.postgres import PostgresSectorRadarRepository
from zhixing_server.modules.sector_radar.presentation.http import _detail_response, _history_response, _rankings_response
parser = argparse.ArgumentParser()
parser.add_argument('--seed', action='store_true')
parser.add_argument('--run', type=Path)
args = parser.parse_args()
url = os.environ['DATABASE_URL']
assert urlsplit(url).hostname in {'localhost', '127.0.0.1'}, 'disposable local database only'
samples = runpy.run_path('tests/unit/sector_radar/test_detail_reads.py')
target, now, code = samples['TARGET'], samples['NOW'], samples['CODE']
repo = PostgresSectorRadarRepository(url)
if args.seed:
config = Config('alembic.ini')
config.set_main_option('sqlalchemy.url', sqlalchemy_database_url(url).replace('%', '%%'))
config.config_file_name = None
with patch('zhixing_server.bootstrap.config.get_settings', return_value=Settings(database_url=url)):
command.upgrade(config, 'head')
samples['seed_detail'](repo)
current = repo.get_successful_publication(target)
template = repo.load_rankings(current.publication_id)[0].observation
stocks = [f'{i:06d}.SZ' for i in range(10, 3010)]
sectors = [f'PERF{i:04d}.DC' for i in range(500)]
def save(group, rows, order, day=target):
snapshot = build_source_snapshot(api_name=group.value, params={'batch': str(order)}, rows=rows,
target_trade_date=day, observed_at=now)
repo.save_source_snapshots((snapshot,))
repo.save_publication_sources((PublicationSourceRecord(current.publication_id, group, order, snapshot),))
save(Group.STOCK_BASICS, tuple({'ts_code': s, 'symbol': s[:6], 'name': s, 'exchange': 'SZSE',
'list_status': 'L', 'list_date': '20200101'} for s in stocks), 1)
save(Group.CONCEPT_INDICES, tuple({'ts_code': s, 'name': s, 'trade_date': str(target),
'pct_change': '1.1234'} for s in sectors), 1)
save(Group.MEMBERS, tuple({'ts_code': sector, 'con_code': stocks[(i*7+j)%len(stocks)],
'name': stocks[(i*7+j)%len(stocks)], 'trade_date': str(target)}
for i, sector in enumerate(sectors) for j in range(100)), 1)
for offset in range(10):
day = target-timedelta(days=offset)
for group in (Group.DAILY, Group.MONEYFLOW_DC, Group.MONEYFLOW):
rows = tuple({'ts_code': stock, 'trade_date': str(day), 'name': stock,
'pct_chg': str(Decimal(i % 123)/100), 'amount': str(i*12),
'net_amount': str(i-1500), 'net_mf_amount': str(i-500),
'close': str(Decimal(i%1000)/10+1), 'vol': str(i*33)}
for i, stock in enumerate(stocks))
save(group, rows, offset+1, day)
versions = ((MetricKind.AMOUNT, AmountNetStrategy.metric_version, MetricUnit.CNY_100M),
(MetricKind.RATIO, RatioTurnoverStrategy.metric_version, MetricUnit.RATIO),
(MetricKind.SWING, SwingEqualThreeToTenStrategy.metric_version, MetricUnit.RATIO))
for offset in range(30):
day = target-timedelta(days=offset)
if offset < 2:
publication = repo.get_successful_publication(day)
else:
running = replace(current, publication_id=f'bench-{offset}', target_trade_date=day,
status=PublicationStatus.RUNNING, input_hash=None, finished_at=None)
repo.create_publication(running)
publication = replace(running, status=PublicationStatus.SUCCESS, input_hash='a'*64,
finished_at=now+timedelta(seconds=1))
repo.finish_publication(publication)
rankings = []
for kind, version, unit in versions:
observations = tuple(replace(template, trade_date=day, sector_code=sector, sector_name=sector,
metric_kind=kind, metric_version=version, unit=unit,
value=Decimal(i+1)) for i, sector in enumerate(sectors))
rankings.extend(RankingRecord(publication.publication_id, row)
for row in rank_metric_observations(observations))
repo.save_rankings(rankings)
with psycopg.connect(url) as connection:
connection.execute('ANALYZE')
print('dataset', connection.execute('SELECT count(*), sum(row_count), sum(octet_length(payload::text)) FROM sector_radar_source_snapshot').fetchone(),
'rankings', connection.execute('SELECT count(*) FROM sector_radar_ranking').fetchone())
if args.run:
repo.open()
reads = {
'detail': lambda: _detail_response(ReadRadarDetails(repo).detail(target, SectorType.CONCEPT, code)),
'history': lambda: _history_response(ReadRadarDetails(repo).history(target, SectorType.CONCEPT, code)),
'rankings': lambda: _rankings_response(ReadSectorRadar(repo).query(RadarQuery(trade_date=target, page_size=20))),
}
report = {}
for label, read in reads.items():
timings = []
responses = []
for _ in range(5):
start = time.perf_counter()
response = read().model_dump(mode='json')
timings.append(time.perf_counter()-start)
responses.append(response)
assert all(item == responses[0] for item in responses)
fingerprint = hashlib.sha256(json.dumps(responses[0], sort_keys=True).encode()).hexdigest()
report[label] = {'seconds': timings, 'median_seconds': statistics.median(timings),
'response_sha256': fingerprint, 'response': responses[0]}
print(label, 'median', report[label]['median_seconds'], 'sha256', fingerprint, flush=True)
args.run.write_text(json.dumps(report, ensure_ascii=False, indent=2))
repo.close()
@@ -0,0 +1,100 @@
{
"environment": "isolated local PostgreSQL 16-alpine, Python 3.12.11, psycopg 3.3.4; same data, sequential requests, warm database/connection pool, no application cache",
"baseline_commit": "e567e5f",
"dataset": {
"snapshot_count": 41,
"source_rows": 143521,
"source_json_bytes": 22785765,
"ranking_rows": 45003
},
"samples_per_endpoint": 5,
"measurements": {
"detail": {
"before_seconds": [
1.7919143750004878,
1.7288594170004217,
1.7106771250000747,
1.7057415830004174,
1.6912810410012753
],
"after_seconds": [
0.6212627920012892,
0.4883682920008141,
0.44150824999996985,
0.43969708400072705,
0.4841277909999917
],
"before_median_seconds": 1.7106771250000747,
"after_median_seconds": 0.4841277909999917,
"reduction_percent": 71.7,
"responses_identical": true,
"response_sha256": "d2d5693152e949767a2db8e807852fe6fd2cb6ec175664a5dab9f615cdda4467"
},
"history": {
"before_seconds": [
0.8467425830003776,
0.8580366249989311,
0.8756502500000352,
0.8701953330000833,
0.8388115420002578
],
"after_seconds": [
0.026230833000226994,
0.024217250000219792,
0.025365250001414097,
0.028609290999156656,
0.026671499999793014
],
"before_median_seconds": 0.8580366249989311,
"after_median_seconds": 0.026230833000226994,
"reduction_percent": 96.9,
"responses_identical": true,
"response_sha256": "edf098e1f124a6d5526d9f202b67c6e97986095a6cb18fad3d0d6ba07d10d387"
},
"rankings": {
"before_seconds": [
1.3065002089988411,
1.2318050410012802,
1.2226990420003858,
1.3149193330009439,
1.2414632910004002
],
"after_seconds": [
0.06522883400066348,
0.06445087500105728,
0.06437025000013818,
0.06701250000151049,
0.06281666699942434
],
"before_median_seconds": 1.2414632910004002,
"after_median_seconds": 0.06445087500105728,
"reduction_percent": 94.8,
"responses_identical": true,
"response_sha256": "dbe90217c0086e22a570990062e08583c44d72ca1b4cf87b39208b033ce12e62"
}
},
"query_plan_samples": [
{
"query": "source_rows",
"execution_ms": 31.641,
"planning_ms": 1.534,
"rows": 3
},
{
"query": "rank_history",
"execution_ms": 23.911,
"planning_ms": 0.664,
"rows": 90
}
],
"validation": {
"pytest_passed": 226,
"pytest_failed_baseline": 1,
"baseline_failure": "test_market_data_repository_pool.py:65 Connection.executemany AttributeError, reproduced before edits",
"pyright_baseline_errors": 14,
"pyright_new_errors": 0,
"ruff": "passed",
"review_agent": "/root/read_path_review complete: no blocking SQL/data correctness findings",
"caveat": "Default integration-first ordering also disables caplog loggers through pre-existing Alembic fileConfig. Full tests were run unit/HTTP/integration order with both DB environment variables set; one baseline integration failure remained."
}
}