Compare commits
19 Commits
main
...
next-relea
| Author | SHA1 | Date | |
|---|---|---|---|
| 57ef9d7f09 | |||
| 14218de02f | |||
| b9faf278f6 | |||
| 3d7ff5a86c | |||
| 6caa24e1be | |||
| dc163e0ddd | |||
|
|
0953a45b9f | ||
| 87f46a1bfd | |||
|
|
8733fd45ad | ||
| 57d8a929fd | |||
| a81bde004b | |||
|
|
383fb2bc3f | ||
|
|
9c19120444 | ||
|
|
3fc6721d4d | ||
| cf0b1d1d1c | |||
| 3ff3fd3d14 | |||
|
|
6f10c8775c | ||
|
|
2ab6e71735 | ||
|
|
4f2a66b67e |
3
.gitignore
vendored
3
.gitignore
vendored
@ -83,3 +83,6 @@ postgres_data/
|
|||||||
README_GAUNTLET_CLEANUP.md
|
README_GAUNTLET_CLEANUP.md
|
||||||
wipe_gauntlet_team.py
|
wipe_gauntlet_team.py
|
||||||
SCHEMA.md
|
SCHEMA.md
|
||||||
|
|
||||||
|
# Benchmark output files
|
||||||
|
benchmarks/render_timings.txt
|
||||||
|
|||||||
@ -8,35 +8,33 @@ from pandas import DataFrame
|
|||||||
from ..db_engine import Paperdex, model_to_dict, Player, Cardset, Team, DoesNotExist
|
from ..db_engine import Paperdex, model_to_dict, Player, Cardset, Team, DoesNotExist
|
||||||
from ..dependencies import oauth2_scheme, valid_token
|
from ..dependencies import oauth2_scheme, valid_token
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/api/v2/paperdex", tags=["paperdex"])
|
||||||
router = APIRouter(
|
|
||||||
prefix='/api/v2/paperdex',
|
|
||||||
tags=['paperdex']
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class PaperdexModel(pydantic.BaseModel):
|
class PaperdexModel(pydantic.BaseModel):
|
||||||
team_id: int
|
team_id: int
|
||||||
player_id: int
|
player_id: int
|
||||||
created: Optional[int] = int(datetime.timestamp(datetime.now())*1000)
|
created: Optional[int] = int(datetime.timestamp(datetime.now()) * 1000)
|
||||||
|
|
||||||
|
|
||||||
@router.get('')
|
@router.get("")
|
||||||
async def get_paperdex(
|
async def get_paperdex(
|
||||||
team_id: Optional[int] = None, player_id: Optional[int] = None, created_after: Optional[int] = None,
|
team_id: Optional[int] = None,
|
||||||
cardset_id: Optional[int] = None, created_before: Optional[int] = None, flat: Optional[bool] = False,
|
player_id: Optional[int] = None,
|
||||||
csv: Optional[bool] = None):
|
created_after: Optional[int] = None,
|
||||||
|
cardset_id: Optional[int] = None,
|
||||||
|
created_before: Optional[int] = None,
|
||||||
|
flat: Optional[bool] = False,
|
||||||
|
csv: Optional[bool] = None,
|
||||||
|
limit: Optional[int] = 500,
|
||||||
|
):
|
||||||
all_dex = Paperdex.select().join(Player).join(Cardset).order_by(Paperdex.id)
|
all_dex = Paperdex.select().join(Player).join(Cardset).order_by(Paperdex.id)
|
||||||
|
|
||||||
if all_dex.count() == 0:
|
|
||||||
raise HTTPException(status_code=404, detail=f'There are no paperdex to filter')
|
|
||||||
|
|
||||||
if team_id is not None:
|
if team_id is not None:
|
||||||
all_dex = all_dex.where(Paperdex.team_id == team_id)
|
all_dex = all_dex.where(Paperdex.team_id == team_id)
|
||||||
if player_id is not None:
|
if player_id is not None:
|
||||||
all_dex = all_dex.where(Paperdex.player_id == player_id)
|
all_dex = all_dex.where(Paperdex.player_id == player_id)
|
||||||
if cardset_id is not None:
|
if cardset_id is not None:
|
||||||
all_sets = Cardset.select().where(Cardset.id == cardset_id)
|
|
||||||
all_dex = all_dex.where(Paperdex.player.cardset.id == cardset_id)
|
all_dex = all_dex.where(Paperdex.player.cardset.id == cardset_id)
|
||||||
if created_after is not None:
|
if created_after is not None:
|
||||||
# Convert milliseconds timestamp to datetime for PostgreSQL comparison
|
# Convert milliseconds timestamp to datetime for PostgreSQL comparison
|
||||||
@ -47,61 +45,63 @@ async def get_paperdex(
|
|||||||
created_before_dt = datetime.fromtimestamp(created_before / 1000)
|
created_before_dt = datetime.fromtimestamp(created_before / 1000)
|
||||||
all_dex = all_dex.where(Paperdex.created <= created_before_dt)
|
all_dex = all_dex.where(Paperdex.created <= created_before_dt)
|
||||||
|
|
||||||
# if all_dex.count() == 0:
|
if limit is not None:
|
||||||
# db.close()
|
all_dex = all_dex.limit(limit)
|
||||||
# raise HTTPException(status_code=404, detail=f'No paperdex found')
|
|
||||||
|
|
||||||
if csv:
|
if csv:
|
||||||
data_list = [['id', 'team_id', 'player_id', 'created']]
|
data_list = [["id", "team_id", "player_id", "created"]]
|
||||||
for line in all_dex:
|
for line in all_dex:
|
||||||
data_list.append(
|
data_list.append(
|
||||||
[
|
[line.id, line.team.id, line.player.player_id, line.created]
|
||||||
line.id, line.team.id, line.player.player_id, line.created
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||||
|
|
||||||
return Response(content=return_val, media_type='text/csv')
|
return Response(content=return_val, media_type="text/csv")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return_val = {'count': all_dex.count(), 'paperdex': []}
|
items = list(all_dex)
|
||||||
for x in all_dex:
|
return_val = {"count": len(items), "paperdex": []}
|
||||||
return_val['paperdex'].append(model_to_dict(x, recurse=not flat))
|
for x in items:
|
||||||
|
return_val["paperdex"].append(model_to_dict(x, recurse=not flat))
|
||||||
|
|
||||||
return return_val
|
return return_val
|
||||||
|
|
||||||
|
|
||||||
@router.get('/{paperdex_id}')
|
@router.get("/{paperdex_id}")
|
||||||
async def get_one_paperdex(paperdex_id, csv: Optional[bool] = False):
|
async def get_one_paperdex(paperdex_id, csv: Optional[bool] = False):
|
||||||
try:
|
try:
|
||||||
this_dex = Paperdex.get_by_id(paperdex_id)
|
this_dex = Paperdex.get_by_id(paperdex_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No paperdex found with id {paperdex_id}"
|
||||||
|
)
|
||||||
|
|
||||||
if csv:
|
if csv:
|
||||||
data_list = [
|
data_list = [
|
||||||
['id', 'team_id', 'player_id', 'created'],
|
["id", "team_id", "player_id", "created"],
|
||||||
[this_dex.id, this_dex.team.id, this_dex.player.id, this_dex.created]
|
[this_dex.id, this_dex.team.id, this_dex.player.id, this_dex.created],
|
||||||
]
|
]
|
||||||
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
return_val = DataFrame(data_list).to_csv(header=False, index=False)
|
||||||
|
|
||||||
return Response(content=return_val, media_type='text/csv')
|
return Response(content=return_val, media_type="text/csv")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return_val = model_to_dict(this_dex)
|
return_val = model_to_dict(this_dex)
|
||||||
return return_val
|
return return_val
|
||||||
|
|
||||||
|
|
||||||
@router.post('')
|
@router.post("")
|
||||||
async def post_paperdex(paperdex: PaperdexModel, token: str = Depends(oauth2_scheme)):
|
async def post_paperdex(paperdex: PaperdexModel, token: str = Depends(oauth2_scheme)):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to post paperdex. This event has been logged.'
|
detail="You are not authorized to post paperdex. This event has been logged.",
|
||||||
)
|
)
|
||||||
|
|
||||||
dupe_dex = Paperdex.get_or_none(Paperdex.team_id == paperdex.team_id, Paperdex.player_id == paperdex.player_id)
|
dupe_dex = Paperdex.get_or_none(
|
||||||
|
Paperdex.team_id == paperdex.team_id, Paperdex.player_id == paperdex.player_id
|
||||||
|
)
|
||||||
if dupe_dex:
|
if dupe_dex:
|
||||||
return_val = model_to_dict(dupe_dex)
|
return_val = model_to_dict(dupe_dex)
|
||||||
return return_val
|
return return_val
|
||||||
@ -109,7 +109,7 @@ async def post_paperdex(paperdex: PaperdexModel, token: str = Depends(oauth2_sch
|
|||||||
this_dex = Paperdex(
|
this_dex = Paperdex(
|
||||||
team_id=paperdex.team_id,
|
team_id=paperdex.team_id,
|
||||||
player_id=paperdex.player_id,
|
player_id=paperdex.player_id,
|
||||||
created=datetime.fromtimestamp(paperdex.created / 1000)
|
created=datetime.fromtimestamp(paperdex.created / 1000),
|
||||||
)
|
)
|
||||||
|
|
||||||
saved = this_dex.save()
|
saved = this_dex.save()
|
||||||
@ -119,24 +119,30 @@ async def post_paperdex(paperdex: PaperdexModel, token: str = Depends(oauth2_sch
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=418,
|
status_code=418,
|
||||||
detail='Well slap my ass and call me a teapot; I could not save that dex'
|
detail="Well slap my ass and call me a teapot; I could not save that dex",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.patch('/{paperdex_id}')
|
@router.patch("/{paperdex_id}")
|
||||||
async def patch_paperdex(
|
async def patch_paperdex(
|
||||||
paperdex_id, team_id: Optional[int] = None, player_id: Optional[int] = None, created: Optional[int] = None,
|
paperdex_id,
|
||||||
token: str = Depends(oauth2_scheme)):
|
team_id: Optional[int] = None,
|
||||||
|
player_id: Optional[int] = None,
|
||||||
|
created: Optional[int] = None,
|
||||||
|
token: str = Depends(oauth2_scheme),
|
||||||
|
):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to patch paperdex. This event has been logged.'
|
detail="You are not authorized to patch paperdex. This event has been logged.",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
this_dex = Paperdex.get_by_id(paperdex_id)
|
this_dex = Paperdex.get_by_id(paperdex_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No paperdex found with id {paperdex_id}"
|
||||||
|
)
|
||||||
|
|
||||||
if team_id is not None:
|
if team_id is not None:
|
||||||
this_dex.team_id = team_id
|
this_dex.team_id = team_id
|
||||||
@ -151,40 +157,43 @@ async def patch_paperdex(
|
|||||||
else:
|
else:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=418,
|
status_code=418,
|
||||||
detail='Well slap my ass and call me a teapot; I could not save that rarity'
|
detail="Well slap my ass and call me a teapot; I could not save that rarity",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.delete('/{paperdex_id}')
|
@router.delete("/{paperdex_id}")
|
||||||
async def delete_paperdex(paperdex_id, token: str = Depends(oauth2_scheme)):
|
async def delete_paperdex(paperdex_id, token: str = Depends(oauth2_scheme)):
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=401,
|
||||||
detail='You are not authorized to delete rewards. This event has been logged.'
|
detail="You are not authorized to delete rewards. This event has been logged.",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
this_dex = Paperdex.get_by_id(paperdex_id)
|
this_dex = Paperdex.get_by_id(paperdex_id)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
raise HTTPException(status_code=404, detail=f'No paperdex found with id {paperdex_id}')
|
raise HTTPException(
|
||||||
|
status_code=404, detail=f"No paperdex found with id {paperdex_id}"
|
||||||
|
)
|
||||||
|
|
||||||
count = this_dex.delete_instance()
|
count = this_dex.delete_instance()
|
||||||
|
|
||||||
if count == 1:
|
if count == 1:
|
||||||
raise HTTPException(status_code=200, detail=f'Paperdex {this_dex} has been deleted')
|
|
||||||
else:
|
|
||||||
raise HTTPException(status_code=500, detail=f'Paperdex {this_dex} was not deleted')
|
|
||||||
|
|
||||||
|
|
||||||
@router.post('/wipe-ai')
|
|
||||||
async def wipe_ai_paperdex(token: str = Depends(oauth2_scheme)):
|
|
||||||
if not valid_token(token):
|
|
||||||
logging.warning('Bad Token: [REDACTED]')
|
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=401,
|
status_code=200, detail=f"Paperdex {this_dex} has been deleted"
|
||||||
detail='Unauthorized'
|
)
|
||||||
|
else:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=500, detail=f"Paperdex {this_dex} was not deleted"
|
||||||
)
|
)
|
||||||
|
|
||||||
g_teams = Team.select().where(Team.abbrev.contains('Gauntlet'))
|
|
||||||
|
@router.post("/wipe-ai")
|
||||||
|
async def wipe_ai_paperdex(token: str = Depends(oauth2_scheme)):
|
||||||
|
if not valid_token(token):
|
||||||
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
g_teams = Team.select().where(Team.abbrev.contains("Gauntlet"))
|
||||||
count = Paperdex.delete().where(Paperdex.team << g_teams).execute()
|
count = Paperdex.delete().where(Paperdex.team << g_teams).execute()
|
||||||
return f'Deleted {count} records'
|
return f"Deleted {count} records"
|
||||||
|
|||||||
@ -52,9 +52,12 @@ async def update_game_season_stats(
|
|||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
from ..services.season_stats import update_season_stats
|
from ..services.season_stats import update_season_stats
|
||||||
|
from ..db_engine import DoesNotExist
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = update_season_stats(game_id, force=force)
|
result = update_season_stats(game_id, force=force)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Game {game_id} not found")
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("update-game/%d failed: %s", game_id, exc, exc_info=True)
|
logger.error("update-game/%d failed: %s", game_id, exc, exc_info=True)
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
|
|||||||
@ -1049,7 +1049,6 @@ async def team_buy_players(team_id: int, ids: str, ts: str):
|
|||||||
detail=f"You are not authorized to buy {this_team.abbrev} cards. This event has been logged.",
|
detail=f"You are not authorized to buy {this_team.abbrev} cards. This event has been logged.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
all_ids = ids.split(",")
|
all_ids = ids.split(",")
|
||||||
conf_message = ""
|
conf_message = ""
|
||||||
total_cost = 0
|
total_cost = 0
|
||||||
@ -1542,9 +1541,11 @@ async def list_team_evolutions(
|
|||||||
):
|
):
|
||||||
"""List all EvolutionCardState rows for a team, with optional filters.
|
"""List all EvolutionCardState rows for a team, with optional filters.
|
||||||
|
|
||||||
Joins EvolutionCardState to EvolutionTrack so that card_type filtering
|
Joins EvolutionCardState → EvolutionTrack (for card_type filtering and
|
||||||
works without a second query. Results are paginated via page/per_page
|
threshold context) and EvolutionCardState → Player (for player_name),
|
||||||
(1-indexed pages); items are ordered by player_id for stable ordering.
|
both eager-loaded in a single query. Results are paginated via
|
||||||
|
page/per_page (1-indexed pages); items are ordered by current_tier DESC,
|
||||||
|
current_value DESC so the most-progressed cards appear first.
|
||||||
|
|
||||||
Query parameters:
|
Query parameters:
|
||||||
card_type -- filter to states whose track.card_type matches (e.g. 'batter', 'sp')
|
card_type -- filter to states whose track.card_type matches (e.g. 'batter', 'sp')
|
||||||
@ -1555,20 +1556,26 @@ async def list_team_evolutions(
|
|||||||
Response shape:
|
Response shape:
|
||||||
{"count": N, "items": [card_state_with_threshold_context, ...]}
|
{"count": N, "items": [card_state_with_threshold_context, ...]}
|
||||||
|
|
||||||
Each item in 'items' has the same shape as GET /evolution/cards/{card_id}.
|
Each item in 'items' has the same shape as GET /evolution/cards/{card_id},
|
||||||
|
plus a ``player_name`` field sourced from the Player table.
|
||||||
"""
|
"""
|
||||||
if not valid_token(token):
|
if not valid_token(token):
|
||||||
logging.warning("Bad Token: [REDACTED]")
|
logging.warning("Bad Token: [REDACTED]")
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
from ..db_engine import EvolutionCardState, EvolutionTrack
|
from ..db_engine import EvolutionCardState, EvolutionTrack, Player
|
||||||
from ..routers_v2.evolution import _build_card_state_response
|
from ..routers_v2.evolution import _build_card_state_response
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
EvolutionCardState.select(EvolutionCardState, EvolutionTrack)
|
EvolutionCardState.select(EvolutionCardState, EvolutionTrack, Player)
|
||||||
.join(EvolutionTrack)
|
.join(EvolutionTrack)
|
||||||
|
.switch(EvolutionCardState)
|
||||||
|
.join(Player)
|
||||||
.where(EvolutionCardState.team == team_id)
|
.where(EvolutionCardState.team == team_id)
|
||||||
.order_by(EvolutionCardState.player_id)
|
.order_by(
|
||||||
|
EvolutionCardState.current_tier.desc(),
|
||||||
|
EvolutionCardState.current_value.desc(),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if card_type is not None:
|
if card_type is not None:
|
||||||
@ -1581,5 +1588,9 @@ async def list_team_evolutions(
|
|||||||
offset = (page - 1) * per_page
|
offset = (page - 1) * per_page
|
||||||
page_query = query.offset(offset).limit(per_page)
|
page_query = query.offset(offset).limit(per_page)
|
||||||
|
|
||||||
items = [_build_card_state_response(state) for state in page_query]
|
items = []
|
||||||
|
for state in page_query:
|
||||||
|
item = _build_card_state_response(state)
|
||||||
|
item["player_name"] = state.player.p_name
|
||||||
|
items.append(item)
|
||||||
return {"count": total, "items": items}
|
return {"count": total, "items": items}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ and WP-09 (formula engine). Models and formula functions are imported lazily so
|
|||||||
this module can be imported before those PRs merge.
|
this module can be imported before those PRs merge.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime, UTC
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ def evaluate_card(
|
|||||||
new_tier = _tier_from_value_fn(value, track)
|
new_tier = _tier_from_value_fn(value, track)
|
||||||
|
|
||||||
# 5–8. Update card state (no tier regression)
|
# 5–8. Update card state (no tier regression)
|
||||||
now = datetime.utcnow()
|
now = datetime.now(UTC)
|
||||||
card_state.current_value = value
|
card_state.current_value = value
|
||||||
card_state.current_tier = max(card_state.current_tier, new_tier)
|
card_state.current_tier = max(card_state.current_tier, new_tier)
|
||||||
card_state.fully_evolved = card_state.current_tier >= 4
|
card_state.fully_evolved = card_state.current_tier >= 4
|
||||||
|
|||||||
92
benchmarks/BASELINE.md
Normal file
92
benchmarks/BASELINE.md
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
# Phase 0 Baseline Benchmarks — WP-00
|
||||||
|
|
||||||
|
Captured before any Phase 0 render-pipeline optimizations (WP-01 through WP-04).
|
||||||
|
Run these benchmarks again after each work package lands to measure improvement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Per-Card Render Time
|
||||||
|
|
||||||
|
**What is measured:** Time from HTTP request to full PNG response for a single card image.
|
||||||
|
Each render triggers a full Playwright Chromium launch, page load, screenshot, and teardown.
|
||||||
|
|
||||||
|
### Method
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set API_BASE to the environment under test
|
||||||
|
export API_BASE=http://pddev.manticorum.com:816
|
||||||
|
|
||||||
|
# Run against 10 batting cards (auto-fetches player IDs)
|
||||||
|
./benchmarks/benchmark_renders.sh
|
||||||
|
|
||||||
|
# Or supply explicit player IDs:
|
||||||
|
./benchmarks/benchmark_renders.sh 101 102 103 104 105 106 107 108 109 110
|
||||||
|
|
||||||
|
# For pitching cards:
|
||||||
|
CARD_TYPE=pitching ./benchmarks/benchmark_renders.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Prerequisites: `curl`, `jq`, `bc`
|
||||||
|
|
||||||
|
Results are appended to `benchmarks/render_timings.txt`.
|
||||||
|
|
||||||
|
### Baseline Results — 2026-03-13
|
||||||
|
|
||||||
|
| Environment | Card type | N | Min (s) | Max (s) | Avg (s) |
|
||||||
|
|-------------|-----------|---|---------|---------|---------|
|
||||||
|
| dev (pddev.manticorum.com:816) | batting | 10 | _TBD_ | _TBD_ | _TBD_ |
|
||||||
|
| dev (pddev.manticorum.com:816) | pitching | 10 | _TBD_ | _TBD_ | _TBD_ |
|
||||||
|
|
||||||
|
> **Note:** Run `./benchmarks/benchmark_renders.sh` against the dev API and paste
|
||||||
|
> the per-render timings from `render_timings.txt` into the table above.
|
||||||
|
|
||||||
|
**Expected baseline (pre-optimization):** ~2.0–3.0s per render
|
||||||
|
(Chromium spawn ~1.0–1.5s + Google Fonts fetch ~0.3–0.5s + render ~0.3s)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Batch Upload Time
|
||||||
|
|
||||||
|
**What is measured:** Wall-clock time to render and upload N card images to S3
|
||||||
|
using the `pd-cards upload` CLI (in the `card-creation` repo).
|
||||||
|
|
||||||
|
### Method
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# In the card-creation repo:
|
||||||
|
time pd-cards upload --cardset 24 --limit 20
|
||||||
|
```
|
||||||
|
|
||||||
|
Or to capture more detail:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
START=$(date +%s%3N)
|
||||||
|
pd-cards upload --cardset 24 --limit 20
|
||||||
|
END=$(date +%s%3N)
|
||||||
|
echo "Elapsed: $(( (END - START) / 1000 )).$(( (END - START) % 1000 ))s"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Baseline Results — 2026-03-13
|
||||||
|
|
||||||
|
| Environment | Cards | Elapsed (s) | Per-card avg (s) |
|
||||||
|
|-------------|-------|-------------|-----------------|
|
||||||
|
| dev (batting) | 20 | _TBD_ | _TBD_ |
|
||||||
|
| dev (pitching) | 20 | _TBD_ | _TBD_ |
|
||||||
|
|
||||||
|
> **Note:** Run the upload command in the `card-creation` repo and record timings here.
|
||||||
|
|
||||||
|
**Expected baseline (pre-optimization):** ~40–60s for 20 cards (~2–3s each sequential)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Re-run After Each Work Package
|
||||||
|
|
||||||
|
| Milestone | Per-card avg (s) | 20-card upload (s) | Notes |
|
||||||
|
|-----------|-----------------|-------------------|-------|
|
||||||
|
| Baseline (pre-WP-01/02) | _TBD_ | _TBD_ | This document |
|
||||||
|
| After WP-01 (self-hosted fonts) | — | — | |
|
||||||
|
| After WP-02 (persistent browser) | — | — | |
|
||||||
|
| After WP-01 + WP-02 combined | — | — | |
|
||||||
|
| After WP-04 (concurrent upload) | — | — | |
|
||||||
|
|
||||||
|
Target: <1.0s per render, <5 min for 800-card upload (with WP-01 + WP-02 deployed).
|
||||||
75
benchmarks/benchmark_renders.sh
Executable file
75
benchmarks/benchmark_renders.sh
Executable file
@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# WP-00: Baseline benchmark — sequential card render timing
|
||||||
|
#
|
||||||
|
# Measures per-card render time for 10 cards by calling the card image
|
||||||
|
# endpoint sequentially and recording curl's time_total for each request.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# API_BASE=http://pddev.manticorum.com:816 ./benchmarks/benchmark_renders.sh
|
||||||
|
# API_BASE=http://localhost:8000 CARD_TYPE=pitching ./benchmarks/benchmark_renders.sh
|
||||||
|
# API_BASE=http://pddev.manticorum.com:816 ./benchmarks/benchmark_renders.sh 101 102 103
|
||||||
|
#
|
||||||
|
# Arguments (optional): explicit player IDs to render. If omitted, the script
|
||||||
|
# queries the API for the first 10 players in the live cardset.
|
||||||
|
#
|
||||||
|
# Output: results are printed to stdout and appended to benchmarks/render_timings.txt
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
API_BASE="${API_BASE:-http://localhost:8000}"
|
||||||
|
CARD_TYPE="${CARD_TYPE:-batting}"
|
||||||
|
OUTFILE="$(dirname "$0")/render_timings.txt"
|
||||||
|
|
||||||
|
echo "=== Card Render Benchmark ===" | tee -a "$OUTFILE"
|
||||||
|
echo "Date: $(date -u +%Y-%m-%dT%H:%M:%SZ)" | tee -a "$OUTFILE"
|
||||||
|
echo "API: $API_BASE" | tee -a "$OUTFILE"
|
||||||
|
echo "Type: $CARD_TYPE" | tee -a "$OUTFILE"
|
||||||
|
|
||||||
|
# --- Resolve player IDs ---
|
||||||
|
if [ "$#" -gt 0 ]; then
|
||||||
|
PLAYER_IDS=("$@")
|
||||||
|
echo "Mode: explicit IDs (${#PLAYER_IDS[@]} players)" | tee -a "$OUTFILE"
|
||||||
|
else
|
||||||
|
echo "Mode: auto-fetch first 10 players from live cardset" | tee -a "$OUTFILE"
|
||||||
|
# Fetch player list and extract IDs; requires jq
|
||||||
|
RAW=$(curl -sf "$API_BASE/api/v2/players?page_size=10")
|
||||||
|
PLAYER_IDS=($(echo "$RAW" | jq -r '.players[].id // .[]?.id // .[]' 2>/dev/null | head -10))
|
||||||
|
if [ "${#PLAYER_IDS[@]}" -eq 0 ]; then
|
||||||
|
echo "ERROR: Could not fetch player IDs from $API_BASE/api/v2/players" | tee -a "$OUTFILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Players: ${PLAYER_IDS[*]}" | tee -a "$OUTFILE"
|
||||||
|
echo "" | tee -a "$OUTFILE"
|
||||||
|
|
||||||
|
# --- Run renders ---
|
||||||
|
TOTAL=0
|
||||||
|
COUNT=0
|
||||||
|
|
||||||
|
for player_id in "${PLAYER_IDS[@]}"; do
|
||||||
|
# Bypass cached PNG files; remove ?nocache=1 after baseline is captured to test cache-hit performance.
|
||||||
|
URL="$API_BASE/api/v2/players/$player_id/${CARD_TYPE}card?nocache=1"
|
||||||
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code} %{time_total}" "$URL" 2>&1)
|
||||||
|
STATUS=$(echo "$HTTP_CODE" | awk '{print $1}')
|
||||||
|
TIMING=$(echo "$HTTP_CODE" | awk '{print $2}')
|
||||||
|
echo " player_id=$player_id http=$STATUS time=${TIMING}s" | tee -a "$OUTFILE"
|
||||||
|
if [ "$STATUS" = "200" ]; then
|
||||||
|
TOTAL=$(echo "$TOTAL + $TIMING" | bc -l)
|
||||||
|
COUNT=$((COUNT + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# --- Summary ---
|
||||||
|
echo "" | tee -a "$OUTFILE"
|
||||||
|
if [ "$COUNT" -gt 0 ]; then
|
||||||
|
AVG=$(echo "scale=3; $TOTAL / $COUNT" | bc -l)
|
||||||
|
echo "Successful renders: $COUNT / ${#PLAYER_IDS[@]}" | tee -a "$OUTFILE"
|
||||||
|
echo "Total time: ${TOTAL}s" | tee -a "$OUTFILE"
|
||||||
|
echo "Average: ${AVG}s per render" | tee -a "$OUTFILE"
|
||||||
|
else
|
||||||
|
echo "No successful renders — check API_BASE and player IDs" | tee -a "$OUTFILE"
|
||||||
|
fi
|
||||||
|
echo "---" | tee -a "$OUTFILE"
|
||||||
|
echo "" | tee -a "$OUTFILE"
|
||||||
|
echo "Results appended to $OUTFILE"
|
||||||
Loading…
Reference in New Issue
Block a user