feat: add performance controls for large browser ops

- Add throttled large-operation handling for tab, group, and session commands.
- Introduce performance profiles, audible-tab aware gentle mode, and job progress tracking.
- Support background session restores with status/cancel commands and lazy placeholders.
- Expose new perf and extension CLI groups plus matching Python SDK methods.
- Preserve pinned tabs during session snapshots and debounce auto-save updates.
- Bump browser-cli and extension versions to 0.10.0 and add pytest-cov to dev deps.
- Add coverage for performance controls, background jobs, lazy restores, and tab metadata.
This commit is contained in:
2026-05-20 22:13:57 +02:00
parent e1e4adbb25
commit 545abeb515
18 changed files with 1054 additions and 148 deletions
+103
View File
@@ -35,3 +35,106 @@ def test_navigation_and_tabs_report_browser_error_pages():
assert "last URL:" in tabs
assert "isBrowserErrorUrl" in navigation
assert "showing an error page while waiting for load" in navigation
def test_large_extension_operations_yield_between_batches():
core = (ROOT / "extension" / "src" / "core.ts").read_text()
tabs = (ROOT / "extension" / "src" / "commands" / "tabs.ts").read_text()
groups = (ROOT / "extension" / "src" / "commands" / "groups.ts").read_text()
session = (ROOT / "extension" / "src" / "commands" / "session.ts").read_text()
assert "yieldForLargeOperation" in core
assert "getLargeOperationThrottle" in core
assert "hasAudibleTabs" in core
assert "runLargeOperation" in core
assert "largeOperationQueue" in core
assert "updateJobProgress" in core
assert "throwIfJobCancelled" in core
assert "getPerformanceProfile" in core
assert "setPerformanceProfile" in core
assert "GENTLE_OPERATION_BATCH_SIZE" in core
assert "GENTLE_OPERATION_PAUSE_MS" in core
assert "itemCount >= 300" in core
assert "itemCount >= 100" in core
assert "chrome.tabs.query({ audible: true })" in core
assert "yieldForLargeOperation" in tabs
assert "toClose.slice" in tabs
assert "ids.slice" in tabs
assert "w.tabs.every" in tabs
assert "getLargeOperationThrottle" in tabs
assert "runLargeOperation(\"tabs.sort\"" in tabs
assert "yieldForLargeOperation" in groups
assert "tabIds.slice" in groups
assert "getLargeOperationThrottle" in groups
assert "runLargeOperation(\"group.close\"" in groups
assert "yieldForLargeOperation(createdTabs.length" in session
assert "getLargeOperationThrottle" in session
assert "runLargeOperation(\"session.load\"" in session
assert "chrome.tabs.discard" in session
assert "lazyPlaceholderUrl" in session
assert "activateLazyTab" in session
assert "lazySessionTabs" in session
assert "throwIfJobCancelled" in session
assert "updateJobProgress" in session
index = (ROOT / "extension" / "src" / "index.ts").read_text()
assert "BACKGROUND_COMMANDS" in index
assert "startBackgroundJob" in index
assert "persistJobs" in index
assert "recentJobs" in index
assert "jobs.status" in index
assert "jobs.cancel" in index
assert "perf.status" in index
assert "perf.set_profile" in index
assert "__background" in index
def test_session_autosave_is_debounced_and_non_overlapping():
session = (ROOT / "extension" / "src" / "commands" / "session.ts").read_text()
assert "autoSaveTimer" in session
assert "autoSaveInFlight" in session
assert "autoSavePending" in session
assert "scheduleAutoSave" in session
assert "autoSaveUpdatedHandler" in session
assert "saveAutoSessionIfChanged" in session
assert "sessionSignature" in session
assert "autoSaveSignature" in session
assert "chrome.tabs.onUpdated.addListener(autoSaveUpdatedHandler)" in session
assert "chrome.tabs.onCreated.addListener(autoSaveHandler)" in session
assert "chrome.tabs.onMoved.addListener(autoSaveHandler)" in session
assert "if (!(\"url\" in changeInfo)) return;" in session
assert "setTimeout(runAutoSave, delayMs)" in session
assert "clearTimeout(autoSaveTimer)" in session
def test_cli_and_sdk_expose_gentle_restore_controls():
session_cli = (ROOT / "browser_cli" / "commands" / "session.py").read_text()
tabs_cli = (ROOT / "browser_cli" / "commands" / "tabs.py").read_text()
groups_cli = (ROOT / "browser_cli" / "commands" / "groups.py").read_text()
sdk = (ROOT / "browser_cli" / "__init__.py").read_text()
assert "--gentle-mode" in session_cli
assert "--discard-background-tabs" in session_cli
assert "--background" in session_cli
assert "--lazy" in session_cli
assert "--eager-tabs" in session_cli
assert "job-status" in session_cli
assert "job-cancel" in session_cli
assert "discardBackgroundTabs" in session_cli
assert "--gentle-mode" in tabs_cli
assert "gentleMode" in tabs_cli
assert "--gentle-mode" in groups_cli
assert "discard_background_tabs" in sdk
assert "discardBackgroundTabs" in sdk
assert "session_load_background" in sdk
assert "job_status" in sdk
assert "job_cancel" in sdk
assert "perf_status" in sdk
assert "set_performance_profile" in sdk
perf_cli = (ROOT / "browser_cli" / "commands" / "perf.py").read_text()
root_cli = (ROOT / "browser_cli" / "cli.py").read_text()
assert "perf_group" in perf_cli
assert "perf.status" in perf_cli
assert "perf.set_profile" in perf_cli
assert "main.add_command(perf_group)" in root_cli
+157
View File
@@ -0,0 +1,157 @@
"""Integration tests for browser performance controls and background jobs."""
import time
import uuid
import pytest
def _wait_for_job(browser, job_id, timeout=10):
deadline = time.time() + timeout
last = None
while time.time() < deadline:
last = browser("jobs.status", {"jobId": job_id})
if last.get("status") in {"done", "error", "cancelled"}:
return last
time.sleep(0.1)
return last or {}
def _close_tabs(browser, tab_ids):
for tab_id in tab_ids:
try:
browser("tabs.close", {"tabId": tab_id})
except Exception:
pass
def _require_perf_features(browser):
try:
return browser("perf.status", {})
except RuntimeError as exc:
if "Unknown command: perf.status" in str(exc):
pytest.skip("Running browser has not reloaded the v0.10.0 extension background worker yet")
raise
def test_perf_status_and_profile_roundtrip_real_browser(browser):
initial = _require_perf_features(browser)
assert "performanceProfile" in initial
assert "audible" in initial
assert "throttle" in initial
assert "jobs" in initial
original = initial.get("performanceProfile", "auto")
try:
changed = browser("perf.set_profile", {"profile": "gentle"})
assert changed["performanceProfile"] == "gentle"
status = browser("perf.status", {})
assert status["performanceProfile"] == "gentle"
assert status["throttle"]["mode"] == "gentle"
finally:
browser("perf.set_profile", {"profile": original})
def test_background_session_load_job_reports_progress_real_browser(browser):
_require_perf_features(browser)
name = f"_pytest_perf_job_{uuid.uuid4().hex}"
marker_url = f"https://example.com/?browser-cli-job={uuid.uuid4().hex}"
marker_tab = browser("navigate.open", {"url": marker_url, "background": True})
loaded_ids = set()
try:
browser("session.save", {"name": name})
baseline_ids = {tab["id"] for tab in browser("tabs.list")}
started = browser("session.load", {
"name": name,
"__background": True,
"lazy": True,
"eagerTabs": 0,
"gentleMode": "ultra",
})
assert started["status"] == "running"
assert started["jobId"]
status = _wait_for_job(browser, started["jobId"])
assert status["status"] == "done"
assert status["command"] == "session.load"
assert status["percent"] == 100
assert status["phase"] == "done"
assert status["total"] is None or status["total"] >= 0
assert status.get("result", {}).get("lazy") is True
loaded_ids = {tab["id"] for tab in browser("tabs.list")} - baseline_ids
assert loaded_ids, "Expected lazy session load to create tabs"
finally:
_close_tabs(browser, loaded_ids)
_close_tabs(browser, [marker_tab["id"]])
try:
browser("session.remove", {"name": name})
except Exception:
pass
def test_lazy_session_load_creates_lightweight_placeholders_real_browser(browser):
_require_perf_features(browser)
name = f"_pytest_lazy_{uuid.uuid4().hex}"
marker_url = f"https://example.com/?browser-cli-lazy={uuid.uuid4().hex}"
marker_tab = browser("navigate.open", {"url": marker_url, "background": True})
loaded_ids = set()
try:
browser("session.save", {"name": name})
baseline_ids = {tab["id"] for tab in browser("tabs.list")}
result = browser("session.load", {"name": name, "lazy": True, "eagerTabs": 0, "gentleMode": "ultra"})
assert result["lazy"] is True
assert result["eagerTabs"] == 0
tabs_after = browser("tabs.list")
loaded_tabs = [tab for tab in tabs_after if tab["id"] not in baseline_ids]
loaded_ids = {tab["id"] for tab in loaded_tabs}
assert loaded_tabs
assert any((tab.get("url") or "").startswith("data:text/html") for tab in loaded_tabs)
finally:
_close_tabs(browser, loaded_ids)
_close_tabs(browser, [marker_tab["id"]])
try:
browser("session.remove", {"name": name})
except Exception:
pass
def test_session_load_restores_pinned_tabs_real_browser(browser):
_require_perf_features(browser)
name = f"_pytest_pinned_{uuid.uuid4().hex}"
marker_url = f"https://example.com/?browser-cli-pinned={uuid.uuid4().hex}"
marker_tab = browser("navigate.open", {"url": marker_url, "background": True})
loaded_ids = set()
try:
browser("tabs.pin", {"tabId": marker_tab["id"]})
browser("session.save", {"name": name})
baseline_ids = {tab["id"] for tab in browser("tabs.list")}
result = browser("session.load", {"name": name, "gentleMode": "ultra", "discardBackgroundTabs": True})
assert result["tabs"] >= 1
loaded_tabs = [tab for tab in browser("tabs.list") if tab["id"] not in baseline_ids]
loaded_ids = {tab["id"] for tab in loaded_tabs}
matching = [tab for tab in loaded_tabs if tab.get("url") == marker_url]
assert matching, "Expected session load to restore marker tab"
assert matching[0].get("pinned") is True
finally:
_close_tabs(browser, loaded_ids)
_close_tabs(browser, [marker_tab["id"]])
try:
browser("session.remove", {"name": name})
except Exception:
pass
def test_job_cancel_command_real_browser(browser):
_require_perf_features(browser)
started = browser("tabs.sort", {"by": "domain", "__background": True, "gentleMode": "ultra"})
job_id = started["jobId"]
try:
cancelled = browser("jobs.cancel", {"jobId": job_id})
assert cancelled["cancelled"] is True
except RuntimeError:
# Tiny real-browser sorts can finish before the cancel request arrives.
status = browser("jobs.status", {"jobId": job_id})
assert status["status"] in {"done", "cancelled"}
return
status = _wait_for_job(browser, job_id)
assert status["status"] in {"cancelled", "done"}
+1 -16
View File
@@ -1,7 +1,6 @@
"""Tests for tabs.* commands."""
import pytest
def test_tabs_list(browser):
tabs = browser("tabs.list")
assert isinstance(tabs, list)
@@ -12,59 +11,51 @@ def test_tabs_list(browser):
assert "url" in first
assert "title" in first
assert "muted" in first
assert "groupId" in first
def test_tabs_count(browser):
count = browser("tabs.count", {})
tabs = browser("tabs.list")
assert count == len(tabs)
def test_tabs_count_with_pattern(browser):
count = browser("tabs.count", {"pattern": "http"})
assert isinstance(count, int)
assert count >= 0
def test_tabs_filter(browser):
result = browser("tabs.filter", {"pattern": "http"})
assert isinstance(result, list)
for tab in result:
assert "http" in tab.get("url", "")
def test_tabs_query(browser):
result = browser("tabs.query", {"search": "a"})
assert isinstance(result, list)
def test_tabs_active_exists(browser):
tabs = browser("tabs.list")
active = [t for t in tabs if t.get("active")]
assert len(active) >= 1, "Expected at least one active tab"
def test_tabs_active_in_window(browser):
active = next(t for t in browser("tabs.list") if t.get("active"))
result = browser("tabs.active_in_window", {"windowId": active["windowId"]})
assert result["id"] == active["id"]
assert result["windowId"] == active["windowId"]
def test_tabs_status(browser):
result = browser("tabs.status", {})
assert isinstance(result, dict)
assert "id" in result
assert "muted" in result
def test_tabs_html(browser, http_tab):
html = browser("tabs.html", {"tabId": http_tab["id"]})
assert isinstance(html, str)
assert len(html) > 0
assert "<html" in html.lower() or "<!doctype" in html.lower()
def test_tabs_close_by_id(browser):
result = browser("navigate.open", {"url": "https://example.com", "background": True})
tab_id = result["id"]
@@ -74,7 +65,6 @@ def test_tabs_close_by_id(browser):
tabs = browser("tabs.list")
assert tab_id not in [t["id"] for t in tabs]
def test_tabs_dedupe(browser):
# Open the same URL twice
r1 = browser("navigate.open", {"url": "https://example.com", "background": True})
@@ -97,13 +87,11 @@ def test_tabs_dedupe(browser):
except Exception:
pass
def test_tabs_sort(browser):
result = browser("tabs.sort", {"by": "domain"})
# No error and at least returns something (None or dict)
assert result is None or isinstance(result, dict)
def test_tabs_move_forward(browser):
r1 = browser("navigate.open", {"url": "https://example.com", "background": True})
r2 = browser("navigate.open", {"url": "https://example.com", "background": True})
@@ -116,13 +104,11 @@ def test_tabs_move_forward(browser):
browser("tabs.close", {"tabId": id1})
browser("tabs.close", {"tabId": id2})
def test_tabs_merge_windows_no_crash(browser):
result = browser("tabs.merge_windows")
assert isinstance(result, dict)
assert "moved" in result
def test_tabs_mute_and_unmute(browser, http_tab):
muted = browser("tabs.mute", {"tabId": http_tab["id"]})
assert isinstance(muted, dict)
@@ -142,7 +128,6 @@ def test_tabs_mute_and_unmute(browser, http_tab):
status = browser("tabs.status", {"tabId": http_tab["id"]})
assert status["muted"] is False
def test_tabs_mute_requires_explicit_tab_when_multiple_tabs_open(browser):
opened = browser("navigate.open", {"url": "https://example.com", "background": True})
try: