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:
+84
-2
@@ -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 || "",
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user