Skip to content

Commit 5ad7a2b

Browse files
authored
Merge branch 'main' into andrew/use_process_group
2 parents de8c143 + 4e6a8f7 commit 5ad7a2b

19 files changed

+602
-172
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ repos:
6666
language: python
6767
additional_dependencies: ["./gitlint-core[trusted-deps]"]
6868
entry: gitlint
69-
args: [--staged, --msg-filename]
69+
args: [--staged, -c, "general.ignore=B6,T3", --msg-filename]
7070
stages: [commit-msg]
7171
- repo: https://github.com/crate-ci/typos
7272
rev: v1

CHANGELOG.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
11
- Change to process group for better killing of multi-process chrome
2+
- Add argument to Session/Target send_command with_perf to return
3+
timing information about browser write/read.
4+
- Update default chrome from 135.0.7011.0/1418433 to 144.0.7527.0/1544685
5+
- Fix: New chrome takes longer/doesn't populate targets right away, so add a
6+
retry loop to populate targets
7+
- Alter `get_chrome` verbose to print whole JSON
8+
- Change chrome download path to use XDG cache dir
9+
- Don't download chrome if we already have that version: add force argument
10+
- Remove unused system inspection code
11+
- Add a set of helper functions to await for tab loading and send javascript
212
v1.2.1
313
- Use custom threadpool for functions that could be running during shutdown:
414
Python's stdlib threadpool isn't available during interpreter shutdown, nor

ROADMAP.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Roadmap
22

3+
- [ ] Fix up browser deps error (eliminate in-package analysis)
4+
- [ ] Switch to process group and kill that to catch all chromium children
5+
- [ ] Add helpers for running javascript
36
- [ ] Working on better diagnostic information
47
- [ ] Explain to user when their system has security restrictions
58
- [ ] Eliminate synchronous API: it's unused, hard to maintain, and nearly
@@ -15,4 +18,3 @@
1518
- [ ] Do documentation
1619
- [ ] Browser Open/Close Status/PipeCheck status should happen at broker level
1720
- [ ] Broker should probably be opening browser and running watchdog...
18-
- [ ] Add a connect only for websockets

pyproject.toml

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ maintainers = [
3030
]
3131
dependencies = [
3232
"logistro>=2.0.1",
33+
"platformdirs>=4.3.6",
3334
"simplejson>=3.19.3",
3435
]
3536

@@ -43,17 +44,17 @@ choreo_get_chrome = "choreographer.cli._cli_utils:get_chrome_cli"
4344

4445
[dependency-groups]
4546
dev = [
46-
"pytest",
47-
"pytest-asyncio; python_version < '3.14'",
48-
"pytest-asyncio>=1.2.0; python_version >= '3.14'",
49-
"pytest-xdist",
5047
"async-timeout",
5148
"numpy; python_version < '3.11'",
5249
"numpy>=2.3.3; python_version >= '3.11'",
5350
"mypy>=1.14.1",
5451
"types-simplejson>=3.19.0.20241221",
5552
"poethepoet>=0.30.0",
5653
"pyright>=1.1.406",
54+
"pytest",
55+
"pytest-asyncio; python_version < '3.14'",
56+
"pytest-asyncio>=1.2.0; python_version >= '3.14'",
57+
"pytest-xdist",
5758
]
5859

5960
# uv doens't allow dependency groups to have separate python requirements
@@ -105,12 +106,14 @@ ignore = [
105106
]
106107

107108
[tool.pytest.ini_options]
109+
asyncio_mode = "auto"
108110
asyncio_default_fixture_loop_scope = "function"
109111
log_cli = false
110112
addopts = "--import-mode=append"
111113

114+
# tell poe to use the env we give it, otherwise it detects uv and overrides flags
112115
[tool.poe]
113-
executor.type = "virtualenv"
116+
executor.type = "simple"
114117

115118
[tool.poe.tasks]
116119
test_proc = "pytest --log-level=1 -W error -n auto -v -rfE --capture=fd tests/test_process.py"
@@ -123,10 +126,12 @@ sequence = ["test_proc", "test_fn"]
123126
help = "Run all tests quickly"
124127

125128
[tool.poe.tasks.debug-test]
129+
env = { CHOREO_ENABLE_DEBUG="1" }
126130
sequence = ["debug-test_proc", "debug-test_fn"]
127131
help = "Run test by test, slowly, quitting after first error"
128132

129133
[tool.poe.tasks.filter-test]
134+
env = { CHOREO_ENABLE_DEBUG="1" }
130135
cmd = "pytest --log-level=1 -W error -vvvx -rA --capture=no --show-capture=no"
131136
help = "Run any/all tests one by one with basic settings: can include filename and -k filters"
132137

src/choreographer/_brokers/_async.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import asyncio
4+
import time
45
import warnings
56
from functools import partial
67
from typing import TYPE_CHECKING
@@ -22,6 +23,9 @@
2223

2324
_logger = logistro.getLogger(__name__)
2425

26+
PERFS_MAX = 5000 # maximum number of entries in the perf dicts
27+
TRIM_SIZE = 500 # what to save after trimming it
28+
2529

2630
class UnhandledMessageWarning(UserWarning):
2731
pass
@@ -49,6 +53,9 @@ class Broker:
4953
]
5054
"""A mapping of session id: subscription: list[futures]"""
5155

56+
write_perfs: MutableMapping[protocol.MessageKey, tuple[float, float]]
57+
read_perfs: MutableMapping[protocol.MessageKey, float]
58+
5259
def __init__(self, browser: Browser, channel: ChannelInterface) -> None:
5360
"""
5461
Construct a broker for a synchronous arragenment w/ both ends.
@@ -66,6 +73,8 @@ def __init__(self, browser: Browser, channel: ChannelInterface) -> None:
6673
# if its a user task, can cancel
6774
self._current_read_task: asyncio.Task[Any] | None = None
6875
self.futures = {}
76+
self.write_perfs = {}
77+
self.read_perfs = {}
6978
self._subscriptions_futures = {}
7079

7180
self._write_lock = asyncio.Lock()
@@ -223,6 +232,14 @@ async def read_loop() -> None: # noqa: PLR0912, PLR0915, C901
223232
raise RuntimeError(f"Couldn't find a future for key: {key}")
224233
if not future.done():
225234
future.set_result(response)
235+
self.read_perfs[key] = time.perf_counter()
236+
if len(self.write_perfs) > PERFS_MAX:
237+
self.write_perfs = dict(
238+
list(self.write_perfs.items())[TRIM_SIZE:],
239+
)
240+
self.read_perfs = dict(
241+
list(self.read_perfs.items())[TRIM_SIZE:],
242+
)
226243
else:
227244
warnings.warn(
228245
f"Unhandled message type:{response!s}",
@@ -237,6 +254,16 @@ async def read_loop() -> None: # noqa: PLR0912, PLR0915, C901
237254
read_task.add_done_callback(check_read_loop_error)
238255
self._current_read_task = read_task
239256

257+
def get_perf(
258+
self,
259+
obj: protocol.BrowserCommand,
260+
) -> tuple[float, float, float]:
261+
"""Get the performance tuple for a certain BrowserCommand."""
262+
key = protocol.calculate_message_key(obj)
263+
if not key:
264+
return (0, 0, 0)
265+
return (*self.write_perfs[key], self.read_perfs[key])
266+
240267
async def write_json(
241268
self,
242269
obj: protocol.BrowserCommand,
@@ -254,13 +281,15 @@ async def write_json(
254281
self.futures[key] = future
255282
_logger.debug(f"Created future: {key} {future}")
256283
try:
284+
perf_start = time.perf_counter()
257285
async with self._write_lock: # this should be a queue not a lock
258286
loop = asyncio.get_running_loop()
259287
await loop.run_in_executor(
260288
self._executor,
261289
self._channel.write_json,
262290
obj,
263291
)
292+
self.write_perfs[key] = (perf_start, time.perf_counter())
264293
except (_manual_thread_pool.ExecutorClosedError, asyncio.CancelledError) as e:
265294
if not future.cancel() or not future.cancelled():
266295
await future # it wasn't canceled, so listen to it before raising

src/choreographer/browser_async.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from choreographer import protocol
1717

1818
from ._brokers import Broker
19-
from .browsers import BrowserClosedError, BrowserDepsError, BrowserFailedError, Chromium
19+
from .browsers import BrowserClosedError, BrowserFailedError, Chromium
2020
from .channels import ChannelClosedError, Pipe
2121
from .protocol.devtools_async import Session, Target
2222
from .utils import TmpDirWarning, _manual_thread_pool
@@ -32,6 +32,9 @@
3232
from .browsers._interface_type import BrowserImplInterface
3333
from .channels._interface_type import ChannelInterface
3434

35+
_N = MAX_POPULATE_LOOPS = 20
36+
37+
3538
_logger = logistro.getLogger(__name__)
3639

3740
# Since I added locks to pipes, do we need locks here?
@@ -164,15 +167,16 @@ def run() -> subprocess.Popen[bytes] | subprocess.Popen[str]: # depends on args
164167
self._channel.open() # should this and below be in a broker run
165168
_logger.debug("Running read loop")
166169
self._broker.run_read_loop()
167-
_logger.debug("Populating Targets")
168-
await asyncio.sleep(0) # let watchdog start
169-
await self.populate_targets()
170+
await asyncio.sleep(0) # let watchdog start before populate
171+
counter = 0
172+
while not self.get_tab():
173+
_logger.debug("Populating Targets")
174+
await self.populate_targets()
175+
await asyncio.sleep(0.1)
176+
counter += 1
177+
if counter == MAX_POPULATE_LOOPS:
178+
break
170179
except (BrowserClosedError, BrowserFailedError, asyncio.CancelledError) as e:
171-
if (
172-
hasattr(self._browser_impl, "missing_libs")
173-
and self._browser_impl.missing_libs # type: ignore[reportAttributeAccessIssue]
174-
):
175-
raise BrowserDepsError from e
176180
raise BrowserFailedError(
177181
"The browser seemed to close immediately after starting.",
178182
"You can set the `logging.Logger` level lower to see more output.",
@@ -352,7 +356,6 @@ async def populate_targets(self) -> None:
352356
raise RuntimeError("Could not get targets") from Exception(
353357
response["error"],
354358
)
355-
356359
for json_response in response["result"]["targetInfos"]:
357360
if (
358361
json_response["type"] == "page"

src/choreographer/browsers/_errors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class BrowserFailedError(RuntimeError):
66
"""An error for when the browser fails to launch."""
77

88

9+
# not currently used but keeping copy + not breaking API
910
class BrowserDepsError(BrowserFailedError):
1011
"""An error for when the browser is closed because of missing libs."""
1112

src/choreographer/browsers/chromium.py

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -115,46 +115,6 @@ def logger_parser(
115115

116116
return True
117117

118-
def _libs_ok(self) -> bool:
119-
"""Return true if libs ok."""
120-
if self.skip_local:
121-
_logger.debug(
122-
"If we HAVE to skip local.",
123-
)
124-
return True
125-
_logger.debug("Checking for libs needed.")
126-
if platform.system() != "Linux":
127-
_logger.debug("We're not in linux, so no need for check.")
128-
return True
129-
p = None
130-
try:
131-
_logger.debug(f"Trying ldd {self.path}")
132-
p = subprocess.run( # noqa: S603, validating run with variables
133-
[ # noqa: S607 path is all we have
134-
"ldd",
135-
str(self.path),
136-
],
137-
capture_output=True,
138-
timeout=5,
139-
check=True,
140-
)
141-
except Exception as e: # noqa: BLE001
142-
msg = "ldd failed."
143-
stderr = p.stderr.decode() if p and p.stderr else None
144-
# Log failure as INFO rather than WARNING so that it's hidden by default,
145-
# since browser may succeed even if ldd fails
146-
_logger.info(
147-
msg # noqa: G003 + in log
148-
+ f" e: {e}, stderr: {stderr}",
149-
)
150-
return False
151-
if b"not found" in p.stdout:
152-
msg = "Found deps missing in chrome"
153-
_logger.debug2(msg + f" {p.stdout.decode()}")
154-
return False
155-
_logger.debug("No problems found with dependencies")
156-
return True
157-
158118
def __init__(
159119
self,
160120
channel: ChannelInterface,
@@ -220,7 +180,6 @@ def pre_open(self) -> None:
220180
path=self._tmp_dir_path,
221181
sneak=self._is_isolated,
222182
)
223-
self.missing_libs = not self._libs_ok()
224183
_logger.info(f"Temporary directory at: {self.tmp_dir.path}")
225184

226185
def is_isolated(self) -> bool:

0 commit comments

Comments
 (0)