commit c51c942660865435e250efffb9160dc5867d142e
parent 2c60938c4a62298a29b06e6da3d802439c4dff92
Author: Vasish Baungally <vbaungally@mozilla.com>
Date: Wed, 10 Dec 2025 22:06:10 +0000
Bug 1970500 - Limit Number of Tabs to Process for AI Tab Grouping to First 300. r=tabbrowser-reviewers,sthompson
Differential Revision: https://phabricator.services.mozilla.com/D273947
Diffstat:
3 files changed, 295 insertions(+), 16 deletions(-)
diff --git a/browser/components/tabbrowser/SmartTabGrouping.sys.mjs b/browser/components/tabbrowser/SmartTabGrouping.sys.mjs
@@ -84,8 +84,10 @@ const MAX_NON_SUMMARIZED_SEARCH_LENGTH = 26;
export const DIM_REDUCTION_METHODS = {};
const MISSING_ANCHOR_IN_CLUSTER_PENALTY = 0.2;
-const MAX_NN_GROUPED_TABS = 3;
+const MAX_GROUPED_TABS = 3;
const MAX_SUGGESTED_TABS = 10;
+// limit number of tabs to be processed so inference process doesn't crash
+const MAX_TABS_TO_PROCESS = 300;
const DISSIMILAR_TAB_LABEL = "none";
const ADULT_TAB_LABEL = "adult content";
@@ -260,17 +262,24 @@ export class SmartTabGroupingManager {
}
/**
- * Generates suggested tabs for an existing or provisional group
+ * Generates tabs to process with a limit. First MAX_GROUPED_TABS are tabs that are
+ * present in the group of the anchor tab. The remaining "ungrouped" tabs fill the
+ * slots up to MAX_TABS_TO_PROCESS
*
- * @param {object} group active group we are adding tabs to
- * @param {Array} tabs list of tabs from gbrowser, some of which may be grouped in other groups
+ * @param {Array} tabsInGroup active tabs in anchor group we are adding tabs to
+ * @param {Array} allTabs list of tabs from gbrowser, some of which may be grouped in other groups
+ * @param {number} max_limit_to_process max number of tabs we want to process as part of the flow
* @returns a list of suggested new tabs. If no new tabs are suggested an empty list is returned.
*/
- async smartTabGroupingForGroup(group, tabs) {
- // Add tabs to suggested group
- const groupTabs = group.tabs;
- const allTabs = tabs.filter(tab => {
- // Don't include tabs already pinned
+ getTabsToProcess(
+ tabsInGroup,
+ allTabs,
+ max_limit_to_process = MAX_TABS_TO_PROCESS
+ ) {
+ const seen = new Set();
+ let tabsToProcess = [];
+
+ const shouldInclude = tab => {
if (tab.pinned) {
return false;
}
@@ -278,12 +287,58 @@ export class SmartTabGroupingManager {
return false;
}
return true;
- });
+ };
- // find tabs that are part of the group
- const groupIndices = groupTabs
- .map(a => allTabs.indexOf(a))
- .filter(a => a >= 0);
+ // include tabs in the anchor group first
+ for (const tab of tabsInGroup) {
+ if (!shouldInclude(tab)) {
+ continue;
+ }
+ if (!seen.has(tab)) {
+ // make sure we have "seen" all the
+ // tabs already in the current group
+ seen.add(tab);
+ tabsToProcess.push(tab);
+ }
+ }
+
+ // when generating embeddings, we only look at the first MAX_GROUPED_TABS
+ // so use that limit here
+ tabsToProcess = tabsToProcess.slice(0, MAX_GROUPED_TABS);
+ // fill remaining slots with ungrouped tabs from the window
+ for (const tab of allTabs) {
+ if (tabsToProcess.length >= max_limit_to_process) {
+ break;
+ }
+ if (!shouldInclude(tab)) {
+ continue;
+ }
+ if (!seen.has(tab)) {
+ seen.add(tab);
+ tabsToProcess.push(tab);
+ }
+ }
+ return tabsToProcess;
+ }
+
+ /**
+ * Generates suggested tabs for an existing or provisional group
+ *
+ * @param {object} group active group we are adding tabs to
+ * @param {Array} tabs list of tabs from gbrowser, some of which may be grouped in other groups
+ * @returns a list of suggested new tabs. If no new tabs are suggested an empty list is returned.
+ */
+ async smartTabGroupingForGroup(group, tabs) {
+ // Add tabs to suggested group
+ const groupTabs = group.tabs;
+ const allTabs = this.getTabsToProcess(groupTabs, tabs, MAX_TABS_TO_PROCESS);
+ // first (1 up to MAX_GROUPED_TABS) are tabs in the group
+ const groupIndices = [];
+ for (let i = 0; i < MAX_GROUPED_TABS; i++) {
+ if (groupTabs.includes(allTabs[i])) {
+ groupIndices.push(i);
+ }
+ }
// find tabs that are part of other groups
const alreadyGroupedIndices = allTabs
@@ -409,7 +464,7 @@ export class SmartTabGroupingManager {
let closestScore = null;
for (
let j = 0;
- j < Math.min(groupedIndices.length, MAX_NN_GROUPED_TABS);
+ j < Math.min(groupedIndices.length, MAX_GROUPED_TABS);
j++
) {
const cosineSim = cosSim(
@@ -654,7 +709,7 @@ export class SmartTabGroupingManager {
const anchorTabsPrep = groupedIndices
.map(gi => tabData[gi])
- .slice(0, MAX_NN_GROUPED_TABS);
+ .slice(0, MAX_GROUPED_TABS);
// generate embeddings for both anchor and candidate titles
const titleEmbeddings = await this._generateEmbeddings(
diff --git a/browser/components/tabbrowser/test/xpcshell/smarttabgrouping/test_tabs_to_process.js b/browser/components/tabbrowser/test/xpcshell/smarttabgrouping/test_tabs_to_process.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { SmartTabGroupingManager } = ChromeUtils.importESModule(
+ "moz-src:///browser/components/tabbrowser/SmartTabGrouping.sys.mjs"
+);
+
+// Simple helper to construct tab-like objects for tests.
+function makeTab(id, { pinned = false, hasURL = true } = {}) {
+ return {
+ id,
+ label: `Tab ${id}`,
+ pinned,
+ linkedBrowser: hasURL
+ ? { currentURI: { spec: `https://example.com/${id}` } }
+ : null,
+ };
+}
+
+add_task(
+ function test_tabs_to_process_includes_group_first_and_respects_limit() {
+ const manager = new SmartTabGroupingManager();
+
+ const groupTab1 = makeTab("group-1");
+ const groupTab2 = makeTab("group-2");
+ const other1 = makeTab("other-1");
+ const other2 = makeTab("other-2");
+ const other3 = makeTab("other-3");
+
+ const tabsInGroup = [groupTab1, groupTab2];
+ const allTabs = [groupTab1, other1, groupTab2, other2, other3];
+
+ // Limit 4: should be [group tabs] first, then fill with window tabs.
+ let resultIds = manager
+ .getTabsToProcess(tabsInGroup, allTabs, 4)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["group-1", "group-2", "other-1", "other-2"],
+ "Group tabs should come first, then window tabs, up to the limit"
+ );
+
+ // Limit 2: should only include the group tabs.
+ resultIds = manager
+ .getTabsToProcess(tabsInGroup, allTabs, 2)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["group-1", "group-2"],
+ "When limit equals number of group tabs, only group tabs are returned"
+ );
+ }
+);
+
+add_task(
+ function test_tabs_to_process_limits_anchor_tabs_to_max_nn_grouped_tabs() {
+ const manager = new SmartTabGroupingManager();
+
+ // More than MAX_NN_GROUPED_TABS (4) tabs in the group.
+ const groupTabs = [];
+ for (let i = 0; i < 6; i++) {
+ groupTabs.push(makeTab(`group-${i}`));
+ }
+ const extraTab = makeTab("other");
+ const allTabs = [...groupTabs, extraTab];
+
+ const resultIds = manager
+ .getTabsToProcess(groupTabs, allTabs, 10)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["group-0", "group-1", "group-2", "other"],
+ "Only the first MAX_NN_GROUPED_TABS group tabs should be kept, then window tabs"
+ );
+ }
+);
+
+add_task(function test_tabs_to_process_deduplicates_group_tabs_in_all_tabs() {
+ const manager = new SmartTabGroupingManager();
+
+ const groupTab1 = makeTab("group-1");
+ const groupTab2 = makeTab("group-2");
+ const other1 = makeTab("other-1");
+
+ // group tabs also appear in allTabs
+ const tabsInGroup = [groupTab1, groupTab2];
+ const allTabs = [groupTab1, groupTab2, other1];
+
+ const resultIds = manager
+ .getTabsToProcess(tabsInGroup, allTabs, 10)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["group-1", "group-2", "other-1"],
+ "Tabs already in the group should not be duplicated when iterating allTabs"
+ );
+});
+
+add_task(function test_tabs_to_process_more_non_group_tabs_than_limit() {
+ const manager = new SmartTabGroupingManager();
+
+ const groupTab1 = makeTab("group-1");
+ const groupTab2 = makeTab("group-2");
+
+ const tabsInGroup = [groupTab1, groupTab2];
+
+ // 10 non-group tabs
+ const nonGroupTabs = [];
+ for (let i = 0; i < 10; i++) {
+ nonGroupTabs.push(makeTab(`other-${i}`));
+ }
+
+ const allTabs = [groupTab1, ...nonGroupTabs, groupTab2];
+
+ // Limit 6 => 2 group tabs + first 4 non-group tabs (in allTabs order)
+ const resultIds = manager
+ .getTabsToProcess(tabsInGroup, allTabs, 6)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["group-1", "group-2", "other-0", "other-1", "other-2", "other-3"],
+ "When there are more non-group tabs than the limit, only the first ones (by window order) are included after the group tabs"
+ );
+});
+
+add_task(function test_tabs_to_process_with_no_group_tabs() {
+ const manager = new SmartTabGroupingManager();
+
+ const allTabs = [];
+ for (let i = 0; i < 8; i++) {
+ allTabs.push(makeTab(`tab-${i}`));
+ }
+
+ const tabsInGroup = [];
+
+ const resultIds = manager
+ .getTabsToProcess(tabsInGroup, allTabs, 5)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["tab-0", "tab-1", "tab-2", "tab-3", "tab-4"],
+ "When there are no group tabs, we should just take up to the limit from window tabs"
+ );
+});
+
+add_task(function test_tabs_to_process_uses_default_max_limit() {
+ const manager = new SmartTabGroupingManager();
+
+ // 350 non-group tabs, all valid
+ const allTabs = [];
+ for (let i = 0; i < 350; i++) {
+ allTabs.push(makeTab(`tab-${i}`));
+ }
+
+ const tabsInGroup = [];
+
+ // Use default max_limit_to_process (should be 100)
+ const result = manager.getTabsToProcess(tabsInGroup, allTabs);
+ const resultIds = result.map(t => t.id);
+
+ Assert.equal(
+ resultIds.length,
+ 300,
+ "Default max limit should cap the number of processed tabs to 300"
+ );
+
+ Assert.deepEqual(
+ resultIds.slice(0, 5),
+ ["tab-0", "tab-1", "tab-2", "tab-3", "tab-4"],
+ "Default behavior should preserve window order for the first tabs"
+ );
+});
+
+add_task(function test_tabs_to_process_all_tabs_filtered_out() {
+ const manager = new SmartTabGroupingManager();
+
+ // All pinned
+ const pinned1 = makeTab("pinned-1", { pinned: true });
+ const pinned2 = makeTab("pinned-2", { pinned: true });
+
+ // All missing URL
+ const noUrl1 = makeTab("no-url-1", { hasURL: false });
+ const noUrl2 = makeTab("no-url-2", { hasURL: false });
+
+ const tabsInGroup = [pinned1];
+ const allTabs = [pinned1, pinned2, noUrl1, noUrl2];
+
+ const result = manager.getTabsToProcess(tabsInGroup, allTabs, 10);
+
+ Assert.deepEqual(
+ result,
+ [],
+ "If all tabs are filtered out (pinned or no URL), we should return an empty list"
+ );
+});
+
+add_task(function test_tabs_to_process_group_tabs_not_in_allTabs() {
+ const manager = new SmartTabGroupingManager();
+
+ const externalGroupTab = makeTab("external-group"); // not in allTabs
+ const other1 = makeTab("other-1");
+ const other2 = makeTab("other-2");
+
+ const tabsInGroup = [externalGroupTab];
+ const allTabs = [other1, other2];
+
+ const resultIds = manager
+ .getTabsToProcess(tabsInGroup, allTabs, 3)
+ .map(t => t.id);
+
+ Assert.deepEqual(
+ resultIds,
+ ["external-group", "other-1", "other-2"],
+ "Tabs in the group should still be included even if they don't appear in allTabs"
+ );
+});
diff --git a/browser/components/tabbrowser/test/xpcshell/smarttabgrouping/xpcshell.toml b/browser/components/tabbrowser/test/xpcshell/smarttabgrouping/xpcshell.toml
@@ -10,4 +10,6 @@ firefox-appdir = "browser"
["test_logistic_regression_utils.js"]
+["test_tabs_to_process.js"]
+
["test_text_preprocessing.js"]