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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user