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:
@@ -1,5 +1,5 @@
|
||||
// @ts-nocheck
|
||||
import { executeScript, getActiveTab, getAliases, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl, resolveTabForDirectAction, tabInfo } from '../core';
|
||||
import { executeScript, getActiveTab, getAliases, getLargeOperationThrottle, isBrowserErrorUrl, isErrorPageScriptError, isScriptableUrl, resolveTabForDirectAction, runLargeOperation, tabInfo, throwIfJobCancelled, updateJobProgress, yieldForLargeOperation } from '../core';
|
||||
export async function tabsList() {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
const aliases = await getAliases();
|
||||
@@ -17,24 +17,33 @@ export async function tabsList() {
|
||||
return tabs;
|
||||
}
|
||||
|
||||
export async function tabsClose({ tabId, inactive, duplicates }) {
|
||||
let toClose = [];
|
||||
if (duplicates) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const seen = new Set();
|
||||
for (const t of all) {
|
||||
if (!t.url) continue;
|
||||
if (seen.has(t.url)) toClose.push(t.id);
|
||||
else seen.add(t.url);
|
||||
export async function tabsClose({ tabId, inactive, duplicates, gentleMode, __job } = {}) {
|
||||
return runLargeOperation("tabs.close", async () => {
|
||||
let toClose = [];
|
||||
if (duplicates) {
|
||||
const all = await chrome.tabs.query({});
|
||||
const seen = new Set();
|
||||
for (const t of all) {
|
||||
if (!t.url) continue;
|
||||
if (seen.has(t.url)) toClose.push(t.id);
|
||||
else seen.add(t.url);
|
||||
}
|
||||
} else if (inactive) {
|
||||
const all = await chrome.tabs.query({});
|
||||
toClose = all.filter(t => !t.active).map(t => t.id);
|
||||
} else if (tabId) {
|
||||
toClose = [tabId];
|
||||
}
|
||||
} else if (inactive) {
|
||||
const all = await chrome.tabs.query({});
|
||||
toClose = all.filter(t => !t.active).map(t => t.id);
|
||||
} else if (tabId) {
|
||||
toClose = [tabId];
|
||||
}
|
||||
if (toClose.length) await chrome.tabs.remove(toClose);
|
||||
return { closed: toClose.length };
|
||||
const throttle = await getLargeOperationThrottle(toClose.length, gentleMode);
|
||||
updateJobProgress(__job, { phase: "closing tabs", current: 0, total: toClose.length });
|
||||
for (let i = 0; i < toClose.length; i += throttle.batchSize) {
|
||||
throwIfJobCancelled(__job);
|
||||
await chrome.tabs.remove(toClose.slice(i, i + throttle.batchSize));
|
||||
updateJobProgress(__job, { phase: "closing tabs", current: Math.min(i + throttle.batchSize, toClose.length), total: toClose.length });
|
||||
await yieldForLargeOperation(i + throttle.batchSize, throttle.batchSize, throttle.pauseMs);
|
||||
}
|
||||
return { closed: toClose.length, gentle: throttle.gentle, audible: throttle.audible };
|
||||
});
|
||||
}
|
||||
|
||||
export async function tabsMove({ tabId, groupId, windowId, index, forward, backward }) {
|
||||
@@ -127,41 +136,62 @@ export async function tabsHtml({ tabId }) {
|
||||
}
|
||||
}
|
||||
|
||||
export async function tabsDedupe() {
|
||||
return tabsClose({ duplicates: true });
|
||||
export async function tabsDedupe(args = {}) {
|
||||
return tabsClose({ ...args, duplicates: true });
|
||||
}
|
||||
|
||||
export async function tabsSort({ by }) {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
let moved = 0;
|
||||
for (const w of windows) {
|
||||
const sorted = [...w.tabs].sort((a, b) => {
|
||||
if (by === "title") return (a.title || "").localeCompare(b.title || "");
|
||||
if (by === "time") return a.id - b.id; // lower id = opened earlier
|
||||
// domain (default)
|
||||
const da = new URL(a.url || a.pendingUrl || "about:blank").hostname;
|
||||
const db = new URL(b.url || b.pendingUrl || "about:blank").hostname;
|
||||
return da.localeCompare(db);
|
||||
});
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
await chrome.tabs.move(sorted[i].id, { index: i });
|
||||
moved++;
|
||||
export async function tabsSort({ by, gentleMode, __job } = {}) {
|
||||
return runLargeOperation("tabs.sort", async () => {
|
||||
const windows = await chrome.windows.getAll({ populate: true });
|
||||
let moved = 0;
|
||||
const totalTabs = windows.reduce((sum, w) => sum + (w.tabs?.length || 0), 0);
|
||||
updateJobProgress(__job, { phase: "sorting tabs", current: 0, total: totalTabs });
|
||||
for (const w of windows) {
|
||||
const sorted = [...w.tabs].sort((a, b) => {
|
||||
if (by === "title") return (a.title || "").localeCompare(b.title || "");
|
||||
if (by === "time") return a.id - b.id; // lower id = opened earlier
|
||||
// domain (default)
|
||||
const da = new URL(a.url || a.pendingUrl || "about:blank").hostname;
|
||||
const db = new URL(b.url || b.pendingUrl || "about:blank").hostname;
|
||||
return da.localeCompare(db);
|
||||
});
|
||||
if (w.tabs.every((tab, index) => tab.id === sorted[index]?.id)) continue;
|
||||
const throttle = await getLargeOperationThrottle(sorted.length, gentleMode);
|
||||
const moveBatchSize = Math.max(1, Math.min(10, throttle.batchSize));
|
||||
for (let i = 0; i < sorted.length; i++) {
|
||||
throwIfJobCancelled(__job);
|
||||
await chrome.tabs.move(sorted[i].id, { index: i });
|
||||
moved++;
|
||||
updateJobProgress(__job, { phase: "sorting tabs", current: moved, total: totalTabs });
|
||||
await yieldForLargeOperation(moved, moveBatchSize, throttle.pauseMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
return { moved };
|
||||
return { moved };
|
||||
});
|
||||
}
|
||||
|
||||
export async function tabsMergeWindows() {
|
||||
const current = await chrome.windows.getCurrent();
|
||||
const all = await chrome.windows.getAll({ populate: true });
|
||||
let moved = 0;
|
||||
for (const w of all) {
|
||||
if (w.id === current.id) continue;
|
||||
const ids = w.tabs.map(t => t.id);
|
||||
await chrome.tabs.move(ids, { windowId: current.id, index: -1 });
|
||||
moved += ids.length;
|
||||
}
|
||||
return { moved };
|
||||
export async function tabsMergeWindows({ gentleMode, __job } = {}) {
|
||||
return runLargeOperation("tabs.merge_windows", async () => {
|
||||
const current = await chrome.windows.getCurrent();
|
||||
const all = await chrome.windows.getAll({ populate: true });
|
||||
let moved = 0;
|
||||
const totalTabs = all.filter(w => w.id !== current.id).reduce((sum, w) => sum + (w.tabs?.length || 0), 0);
|
||||
updateJobProgress(__job, { phase: "merging windows", current: 0, total: totalTabs });
|
||||
for (const w of all) {
|
||||
if (w.id === current.id) continue;
|
||||
const ids = w.tabs.map(t => t.id);
|
||||
const throttle = await getLargeOperationThrottle(ids.length, gentleMode);
|
||||
for (let i = 0; i < ids.length; i += throttle.batchSize) {
|
||||
throwIfJobCancelled(__job);
|
||||
const chunk = ids.slice(i, i + throttle.batchSize);
|
||||
await chrome.tabs.move(chunk, { windowId: current.id, index: -1 });
|
||||
moved += chunk.length;
|
||||
updateJobProgress(__job, { phase: "merging windows", current: moved, total: totalTabs });
|
||||
await yieldForLargeOperation(moved, throttle.batchSize, throttle.pauseMs);
|
||||
}
|
||||
}
|
||||
return { moved };
|
||||
});
|
||||
}
|
||||
|
||||
export async function tabsPin({ tabId }) {
|
||||
|
||||
Reference in New Issue
Block a user