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
+84 -2
View File
@@ -32,13 +32,94 @@ export function isTransientScriptError(error) {
return message.includes("Frame with ID") || message.includes("No tab with id") || isErrorPageScriptError(error);
}
export function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
export const LARGE_OPERATION_BATCH_SIZE = 25;
export const LARGE_OPERATION_PAUSE_MS = 25;
export const GENTLE_OPERATION_BATCH_SIZE = 8;
export const GENTLE_OPERATION_PAUSE_MS = 100;
export async function hasAudibleTabs() {
const audibleTabs = await chrome.tabs.query({ audible: true });
return audibleTabs.some(tab => !(tab.mutedInfo && tab.mutedInfo.muted));
}
let largeOperationQueue = Promise.resolve();
export async function runLargeOperation(name, fn) {
const run = largeOperationQueue.then(async () => {
console.log(`[browser-cli] large operation start: ${name}`);
try {
return await fn();
} finally {
console.log(`[browser-cli] large operation done: ${name}`);
}
});
largeOperationQueue = run.catch(() => {});
return run;
}
export async function getPerformanceProfile() {
const { performanceProfile } = await chrome.storage.local.get("performanceProfile");
return performanceProfile || "auto";
}
export async function setPerformanceProfile(profile) {
const allowed = new Set(["auto", "normal", "gentle", "ultra"]);
const performanceProfile = allowed.has(profile) ? profile : "auto";
await chrome.storage.local.set({ performanceProfile });
return { performanceProfile };
}
export async function getLargeOperationThrottle(itemCount = 0, mode = "auto") {
const audible = await hasAudibleTabs();
const storedProfile = await getPerformanceProfile();
const configuredMode = mode && mode !== "auto" ? mode : storedProfile;
const gentle = configuredMode === "gentle" || configuredMode === "ultra" || (configuredMode === "auto" && audible);
let batchSize = gentle ? GENTLE_OPERATION_BATCH_SIZE : LARGE_OPERATION_BATCH_SIZE;
let pauseMs = gentle ? GENTLE_OPERATION_PAUSE_MS : LARGE_OPERATION_PAUSE_MS;
if (configuredMode === "ultra" || itemCount >= 300) {
batchSize = Math.max(3, Math.floor(batchSize / 2));
pauseMs *= 2;
} else if (itemCount >= 100) {
batchSize = Math.max(5, Math.floor(batchSize * 0.75));
pauseMs = Math.max(pauseMs, 75);
}
return { batchSize, pauseMs, gentle, audible, itemCount, mode: configuredMode };
}
export function updateJobProgress(job, { phase, current, total } = {}) {
if (!job) return;
if (phase) job.phase = phase;
if (total != null) job.total = total;
if (current != null) job.current = current;
if (job.total) job.percent = Math.min(100, Math.round((job.current || 0) * 100 / job.total));
job.updatedAt = Date.now();
}
export function throwIfJobCancelled(job) {
if (job?.cancelRequested) {
throw new Error(`Job '${job.id}' cancelled`);
}
}
export async function yieldForLargeOperation(processed, batchSize = LARGE_OPERATION_BATCH_SIZE, pauseMs = LARGE_OPERATION_PAUSE_MS) {
if (processed > 0 && processed % batchSize === 0) {
await sleep(pauseMs);
}
}
export async function executeScript(options, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await chrome.scripting.executeScript(options);
} catch (e) {
if (i < retries - 1 && isTransientScriptError(e)) {
await new Promise(r => setTimeout(r, 300));
await sleep(300);
continue;
}
throw e;
@@ -52,8 +133,9 @@ export function tabInfo(t) {
windowId: t.windowId,
active: t.active,
muted: Boolean(t.mutedInfo && t.mutedInfo.muted),
groupId: t.groupId >= 0 ? t.groupId : null,
title: t.title,
url: t.url,
url: t.url || t.pendingUrl || "",
};
}