feat: render pipeline optimization (Phase 0)
- Persistent Chromium browser instance with auto-reconnect (WP-02) - FastAPI lifespan hooks for browser startup/shutdown (WP-03) - Self-hosted WOFF2 fonts via base64 embedding, remove Google Fonts CDN (WP-01) - Fix pre-existing lint issues (unused imports, f-string placeholders) Eliminates ~1.5s browser spawn overhead and ~0.4s font CDN round-trip per card render. Target: per-card render from ~3s to <1s. Refs: #88, #89, #90 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
41c50dac4f
commit
74294c38ce
59
app/main.py
59
app/main.py
@ -1,3 +1,5 @@
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.openapi.docs import get_swagger_ui_html
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
@ -5,16 +7,55 @@ from fastapi.openapi.utils import get_openapi
|
||||
# from fastapi.staticfiles import StaticFiles
|
||||
# from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .routers_v2.players import get_browser, shutdown_browser
|
||||
from .routers_v2 import (
|
||||
current, awards, teams, rarity, cardsets, players, packtypes, packs, cards, events, results, rewards, decisions,
|
||||
batstats, pitstats, notifications, paperdex, gamerewards, gauntletrewards, gauntletruns, battingcards,
|
||||
battingcardratings, pitchingcards, pitchingcardratings, cardpositions, scouting, mlbplayers, stratgame, stratplays)
|
||||
current,
|
||||
awards,
|
||||
teams,
|
||||
rarity,
|
||||
cardsets,
|
||||
players,
|
||||
packtypes,
|
||||
packs,
|
||||
cards,
|
||||
events,
|
||||
results,
|
||||
rewards,
|
||||
decisions,
|
||||
batstats,
|
||||
pitstats,
|
||||
notifications,
|
||||
paperdex,
|
||||
gamerewards,
|
||||
gauntletrewards,
|
||||
gauntletruns,
|
||||
battingcards,
|
||||
battingcardratings,
|
||||
pitchingcards,
|
||||
pitchingcardratings,
|
||||
cardpositions,
|
||||
scouting,
|
||||
mlbplayers,
|
||||
stratgame,
|
||||
stratplays,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup: warm up the persistent Chromium browser
|
||||
await get_browser()
|
||||
yield
|
||||
# Shutdown: clean up browser and playwright
|
||||
await shutdown_browser()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
# root_path='/api',
|
||||
responses={404: {'description': 'Not found'}},
|
||||
docs_url='/api/docs',
|
||||
redoc_url='/api/redoc'
|
||||
lifespan=lifespan,
|
||||
responses={404: {"description": "Not found"}},
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
)
|
||||
|
||||
# app.mount("/static", StaticFiles(directory="storage/static"), name="static")
|
||||
@ -54,9 +95,11 @@ app.include_router(decisions.router)
|
||||
@app.get("/api/docs", include_in_schema=False)
|
||||
async def get_docs(req: Request):
|
||||
print(req.scope)
|
||||
return get_swagger_ui_html(openapi_url=req.scope.get('root_path')+'/openapi.json', title='Swagger')
|
||||
return get_swagger_ui_html(
|
||||
openapi_url=req.scope.get("root_path") + "/openapi.json", title="Swagger"
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/openapi.json", include_in_schema=False)
|
||||
async def openapi():
|
||||
return get_openapi(title='Paper Dynasty API', version=f'0.1.1', routes=app.routes)
|
||||
return get_openapi(title="Paper Dynasty API", version="0.1.1", routes=app.routes)
|
||||
|
||||
@ -1,17 +1,15 @@
|
||||
import datetime
|
||||
import os.path
|
||||
import base64
|
||||
|
||||
import pandas as pd
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request, Response, Query
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from html2image import Html2Image
|
||||
from typing import Optional, List, Literal
|
||||
import logging
|
||||
import pydantic
|
||||
from pandas import DataFrame
|
||||
from playwright.async_api import async_playwright
|
||||
from playwright.async_api import async_playwright, Browser, Playwright
|
||||
|
||||
from ..card_creation import get_batter_card_data, get_pitcher_card_data
|
||||
from ..db_engine import (
|
||||
@ -19,7 +17,6 @@ from ..db_engine import (
|
||||
Player,
|
||||
model_to_dict,
|
||||
fn,
|
||||
chunked,
|
||||
Paperdex,
|
||||
Cardset,
|
||||
Rarity,
|
||||
@ -33,6 +30,57 @@ from ..db_engine import (
|
||||
from ..db_helpers import upsert_players
|
||||
from ..dependencies import oauth2_scheme, valid_token, LOG_DATA
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Persistent browser instance (WP-02)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_browser: Browser | None = None
|
||||
_playwright: Playwright | None = None
|
||||
|
||||
|
||||
async def get_browser() -> Browser:
|
||||
"""Get or create persistent Chromium browser instance.
|
||||
|
||||
Reuses a single browser across all card renders, eliminating the ~1-1.5s
|
||||
per-request launch/teardown overhead. Automatically reconnects if the
|
||||
browser process has died.
|
||||
"""
|
||||
global _browser, _playwright
|
||||
if _browser is None or not _browser.is_connected():
|
||||
if _playwright is not None:
|
||||
try:
|
||||
await _playwright.stop()
|
||||
except Exception:
|
||||
pass
|
||||
_playwright = await async_playwright().start()
|
||||
_browser = await _playwright.chromium.launch(
|
||||
args=["--no-sandbox", "--disable-dev-shm-usage"]
|
||||
)
|
||||
return _browser
|
||||
|
||||
|
||||
async def shutdown_browser():
|
||||
"""Clean shutdown of the persistent browser.
|
||||
|
||||
Called by the FastAPI lifespan handler on application exit so the
|
||||
Chromium process is not left orphaned.
|
||||
"""
|
||||
global _browser, _playwright
|
||||
if _browser:
|
||||
try:
|
||||
await _browser.close()
|
||||
except Exception:
|
||||
pass
|
||||
_browser = None
|
||||
if _playwright:
|
||||
try:
|
||||
await _playwright.stop()
|
||||
except Exception:
|
||||
pass
|
||||
_playwright = None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Franchise normalization: Convert city+team names to city-agnostic team names
|
||||
# This enables cross-era player matching (e.g., 'Oakland Athletics' -> 'Athletics')
|
||||
FRANCHISE_NORMALIZE = {
|
||||
@ -153,7 +201,7 @@ async def get_players(
|
||||
all_players = Player.select()
|
||||
if all_players.count() == 0:
|
||||
db.close()
|
||||
raise HTTPException(status_code=404, detail=f"There are no players to filter")
|
||||
raise HTTPException(status_code=404, detail="There are no players to filter")
|
||||
|
||||
if name is not None:
|
||||
all_players = all_players.where(fn.Lower(Player.p_name) == name.lower())
|
||||
@ -692,7 +740,7 @@ async def get_batter_card(
|
||||
)
|
||||
|
||||
headers = {"Cache-Control": "public, max-age=86400"}
|
||||
filename = (
|
||||
_filename = (
|
||||
f"{this_player.description} {this_player.p_name} {card_type} {d}-v{variant}"
|
||||
)
|
||||
if (
|
||||
@ -819,16 +867,17 @@ async def get_batter_card(
|
||||
logging.debug(f"body:\n{html_response.body.decode('UTF-8')}")
|
||||
|
||||
file_path = f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
page = await browser.new_page()
|
||||
browser = await get_browser()
|
||||
page = await browser.new_page(viewport={"width": 1280, "height": 720})
|
||||
try:
|
||||
await page.set_content(html_response.body.decode("UTF-8"))
|
||||
await page.screenshot(
|
||||
path=file_path,
|
||||
type="png",
|
||||
clip={"x": 0.0, "y": 0, "width": 1200, "height": 600},
|
||||
)
|
||||
await browser.close()
|
||||
finally:
|
||||
await page.close()
|
||||
|
||||
# hti = Html2Image(
|
||||
# browser='chrome',
|
||||
|
||||
@ -2,9 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% include 'style.html' %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700&family=Source+Sans+3:wght@400;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="fullCard" style="width: 1200px; height: 600px;">
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user