Skip to content

Commit dd41371

Browse files
perf(boot): lazy-import curl_cffi off the settings boot path
Defer the GestdownLookup import into the suggestion workers' execute() so curl_cffi no longer loads during window boot (~170ms off the import path). Add tools/boot_timer.py to profile settings-mode boot (pyinstrument call tree, viztracer timeline, and cold-FS runs).
1 parent ea8dd57 commit dd41371

10 files changed

Lines changed: 387 additions & 14 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,4 @@ memory/**
183183
.agents/**
184184
.github/workflows/release_v2.yml
185185
AGENTS.md
186+
tools/_boot_profiles/

pyproject.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,13 @@
8484
"tox==4.56.1",
8585
"python-dotenv==1.2.2",
8686
]
87-
tools = ["commitizen==4.16.4", "pre-commit==4.6.0", "pillow==12.2.0"]
87+
tools = [
88+
"commitizen==4.16.4",
89+
"pre-commit==4.6.0",
90+
"pillow==12.2.0",
91+
"pyinstrument==5.1.2",
92+
"viztracer==1.1.1",
93+
]
8894
type = ["mypy==2.1.0"]
8995

9096

src/subsearch/io/app_updater.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22
from pathlib import Path
33
from typing import Callable, NamedTuple
44

5-
from curl_cffi.requests import exceptions
6-
75
from subsearch.io import file_system
86
from subsearch.io.http import get_session
97
from subsearch.runtime.config import APP_PATHS
@@ -25,6 +23,8 @@ class ReleasePage(NamedTuple):
2523

2624

2725
def fetch_latest_release_page() -> ReleasePage:
26+
from curl_cffi.requests import exceptions
27+
2828
try:
2929
response = get_session().get(LATEST_RELEASE_PAGE)
3030
except exceptions.RequestException as error:
@@ -41,6 +41,8 @@ def installer_url(version: str) -> str:
4141
def download_installer(version: str, on_progress: Callable[[float], None] | None = None) -> Path:
4242
APP_PATHS.tmp_dir.mkdir(parents=True, exist_ok=True)
4343
destination = APP_PATHS.tmp_dir / INSTALLER_NAME.format(version=version)
44+
from curl_cffi.requests import exceptions
45+
4446
try:
4547
response = get_session().get(installer_url(version), stream=True)
4648
except exceptions.RequestException as error:

src/subsearch/io/http.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,24 @@
1-
from curl_cffi import requests
2-
from curl_cffi.requests import Response, exceptions
1+
from typing import TYPE_CHECKING
2+
33
from selectolax.lexbor import LexborHTMLParser
44

55
from subsearch.parsing.html_parser import parse_html_response
66
from subsearch.runtime.recorder import LogLevel, capture
77

8+
if TYPE_CHECKING:
9+
from curl_cffi import requests
10+
from curl_cffi.requests import Response
11+
12+
13+
def get_session() -> "requests.Session":
14+
from curl_cffi import requests
815

9-
def get_session() -> requests.Session:
1016
return requests.Session(impersonate="chrome")
1117

1218

1319
def send_request(
14-
url: str, session: requests.Session, timeout: tuple[int, int], header: dict[str, str] | None = None
15-
) -> Response:
20+
url: str, session: "requests.Session", timeout: tuple[int, int], header: dict[str, str] | None = None
21+
) -> "Response":
1622
if header is None:
1723
return session.get(url, timeout=timeout)
1824
return session.get(url, timeout=timeout, headers=header)
@@ -21,6 +27,8 @@ def send_request(
2127
def request_parsed_response(
2228
url: str, timeout: tuple[int, int], header: dict[str, str] | None = None
2329
) -> LexborHTMLParser | None:
30+
from curl_cffi.requests import exceptions
31+
2432
session = get_session()
2533
try:
2634
response = send_request(url, session, timeout=timeout, header=header)
@@ -34,6 +42,8 @@ def request_parsed_response(
3442

3543

3644
def request_parsed_post(url: str, data: dict[str, str], timeout: tuple[int, int]) -> LexborHTMLParser | None:
45+
from curl_cffi.requests import exceptions
46+
3747
session = get_session()
3848
try:
3949
response = session.post(url, data=data, timeout=timeout)
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
from curl_cffi.requests import Response
1+
from typing import TYPE_CHECKING
2+
23
from selectolax.lexbor import LexborHTMLParser
34

5+
if TYPE_CHECKING:
6+
from curl_cffi.requests import Response
7+
48

5-
def parse_html_response(response: Response) -> LexborHTMLParser:
9+
def parse_html_response(response: "Response") -> LexborHTMLParser:
610
return LexborHTMLParser(response.text)

src/subsearch/parsing/imdb_lookup.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,27 @@
11
from dataclasses import dataclass
2-
3-
import imdbinfo
4-
from imdbinfo.exceptions import ImdbinfoError
2+
from types import ModuleType
53

64
from subsearch.runtime.models import ProviderDiagnosticStatus
75
from subsearch.runtime.recorder import LogLevel, capture
86

97
SUGGESTION_LIMIT = 5
108
_SUGGESTION_KINDS = {"movie", "tvSeries", "tvMiniSeries", "tvMovie"}
119

10+
imdbinfo: ModuleType
11+
ImdbinfoError: type[Exception]
12+
13+
14+
def _load_imdbinfo() -> None:
15+
# Deferred so settings-mode boot, which never runs a lookup, skips the ~0.8s import.
16+
global imdbinfo, ImdbinfoError
17+
if "imdbinfo" in globals():
18+
return
19+
import imdbinfo as imdbinfo_module
20+
from imdbinfo.exceptions import ImdbinfoError as ImdbinfoErrorType
21+
22+
imdbinfo = imdbinfo_module
23+
ImdbinfoError = ImdbinfoErrorType
24+
1225

1326
@dataclass(frozen=True)
1427
class TitleSuggestion:
@@ -48,6 +61,7 @@ def display_text(self) -> str:
4861

4962

5063
def find_season_suggestions(imdb_id: str) -> list[SeasonSuggestion]:
64+
_load_imdbinfo()
5165
try:
5266
season_episodes = imdbinfo.get_episodes(imdb_id, season=1)
5367
except ImdbinfoError:
@@ -60,6 +74,7 @@ def find_season_suggestions(imdb_id: str) -> list[SeasonSuggestion]:
6074

6175

6276
def find_episode_suggestions(imdb_id: str, season: int) -> list[EpisodeSuggestion]:
77+
_load_imdbinfo()
6378
try:
6479
season_episodes = imdbinfo.get_episodes(imdb_id, season=season)
6580
except ImdbinfoError:
@@ -78,6 +93,7 @@ def find_episode_suggestions(imdb_id: str, season: int) -> list[EpisodeSuggestio
7893

7994

8095
def find_title_suggestions(typed_term: str, limit: int = SUGGESTION_LIMIT) -> list[TitleSuggestion]:
96+
_load_imdbinfo()
8197
capture(f"Fuzzy matching {typed_term!r}")
8298
try:
8399
search_result = imdbinfo.search_title(typed_term)
@@ -115,6 +131,7 @@ def __init__(self, title: str, year: int, tvseries: bool) -> None:
115131
self.imdb_id = ""
116132
self.diagnostic_status = ProviderDiagnosticStatus.OK
117133

134+
_load_imdbinfo()
118135
try:
119136
search_result = imdbinfo.search_title(title)
120137
except ImdbinfoError:

src/subsearch/ui/services/season_episode_suggestions.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from PySide6.QtCore import QObject, Signal
22

3-
from subsearch.parsing.gestdown_lookup import GestdownLookup
43
from subsearch.parsing.imdb_lookup import (
54
EpisodeSuggestion,
65
SeasonSuggestion,
@@ -17,6 +16,8 @@ def __init__(self, title: str, imdb_id: str) -> None:
1716
self.imdb_id = imdb_id
1817

1918
def execute(self) -> list[SeasonSuggestion]:
19+
from subsearch.parsing.gestdown_lookup import GestdownLookup
20+
2021
return GestdownLookup().find_season_suggestions(self.title) or find_season_suggestions(self.imdb_id)
2122

2223

@@ -29,6 +30,8 @@ def __init__(self, title: str, imdb_id: str, season: int, language_name: str) ->
2930
self.language_name = language_name
3031

3132
def execute(self) -> list[EpisodeSuggestion]:
33+
from subsearch.parsing.gestdown_lookup import GestdownLookup
34+
3235
gestdown = GestdownLookup().find_episode_suggestions(self.title, self.season, self.language_name)
3336
return gestdown or find_episode_suggestions(self.imdb_id, self.season)
3437

tests/test_imdb_episode_lookup.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from dataclasses import dataclass
22

3+
import pytest
34
from imdbinfo.exceptions import ImdbinfoError
45

56
from subsearch.parsing import imdb_lookup
@@ -9,6 +10,11 @@
910
)
1011

1112

13+
@pytest.fixture(autouse=True)
14+
def _load_imdbinfo() -> None:
15+
imdb_lookup._load_imdbinfo()
16+
17+
1218
@dataclass
1319
class _FakeEpisode:
1420
season: int

tests/test_title_suggestions.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
from types import SimpleNamespace
22

3+
import pytest
4+
35
from subsearch.parsing import imdb_lookup
46

57

8+
@pytest.fixture(autouse=True)
9+
def _load_imdbinfo() -> None:
10+
imdb_lookup._load_imdbinfo()
11+
12+
613
def _fake_search_result(titles: list[SimpleNamespace]) -> SimpleNamespace:
714
return SimpleNamespace(titles=titles)
815

0 commit comments

Comments
 (0)