Skip to content

Commit

Permalink
Bug 1809094 - Implement tab.autoDiscardable property r=robwu,geckovie…
Browse files Browse the repository at this point in the history
…w-reviewers,extension-reviewers,Gijs,owlish,tabbrowser-reviewers,dao

Differential Revision: https://phabricator.services.mozilla.com/D166440
  • Loading branch information
gregorypappas committed Jun 20, 2023
1 parent 9549861 commit 0289eea
Show file tree
Hide file tree
Showing 13 changed files with 306 additions and 1 deletion.
13 changes: 13 additions & 0 deletions browser/base/content/tabbrowser-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,15 @@
gBrowser._tabAttrModified(this, ["attention"]);
}

set undiscardable(val) {
if (val == this.hasAttribute("undiscardable")) {
return;
}

this.toggleAttribute("undiscardable", val);
gBrowser._tabAttrModified(this, ["undiscardable"]);
}

set _visuallySelected(val) {
if (val == (this.getAttribute("visuallyselected") == "true")) {
return;
Expand Down Expand Up @@ -224,6 +233,10 @@
return this.getAttribute("activemedia-blocked") == "true";
}

get undiscardable() {
return this.hasAttribute("undiscardable");
}

get isEmpty() {
// Determines if a tab is "empty", usually used in the context of determining
// if it's ok to close the tab.
Expand Down
4 changes: 4 additions & 0 deletions browser/base/content/tabbrowser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4547,6 +4547,10 @@
}
modifiedAttrs.push("muted");
}
if (aOtherTab.hasAttribute("undiscardable")) {
aOurTab.setAttribute("undiscardable", "true");
modifiedAttrs.push("undiscardable");
}
if (aOtherTab.hasAttribute("soundplaying")) {
aOurTab.setAttribute("soundplaying", "true");
modifiedAttrs.push("soundplaying");
Expand Down
4 changes: 4 additions & 0 deletions browser/components/extensions/parent/ext-browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -747,6 +747,10 @@ class Tab extends TabBase {
return this.nativeTab.soundPlaying;
}

get autoDiscardable() {
return !this.nativeTab.undiscardable;
}

get browser() {
return this.nativeTab.linkedBrowser;
}
Expand Down
11 changes: 11 additions & 0 deletions browser/components/extensions/parent/ext-tabs.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ const allAttrs = new Set([
"mutedInfo",
"sharingState",
"title",
"autoDiscardable",
]);
const allProperties = new Set([
"attention",
"audible",
"autoDiscardable",
"discarded",
"favIconUrl",
"hidden",
Expand Down Expand Up @@ -419,6 +421,12 @@ this.tabs = class extends ExtensionAPIPersistent {
) {
needed.push("audible");
}
if (
changed.includes("undiscardable") &&
filter.properties.has("autoDiscardable")
) {
needed.push("autoDiscardable");
}
if (changed.includes("label") && filter.properties.has("title")) {
needed.push("title");
}
Expand Down Expand Up @@ -898,6 +906,9 @@ this.tabs = class extends ExtensionAPIPersistent {
if (updateProperties.active) {
tabbrowser.selectedTab = nativeTab;
}
if (updateProperties.autoDiscardable !== null) {
nativeTab.undiscardable = !updateProperties.autoDiscardable;
}
if (updateProperties.highlighted !== null) {
if (updateProperties.highlighted) {
if (!nativeTab.selected && !nativeTab.multiselected) {
Expand Down
21 changes: 21 additions & 0 deletions browser/components/extensions/schemas/tabs.json
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@
"optional": true,
"description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."
},
"autoDiscardable": {
"type": "boolean",
"optional": true,
"description": "Whether the tab can be discarded automatically by the browser when resources are low."
},
"mutedInfo": {
"$ref": "MutedInfo",
"optional": true,
Expand Down Expand Up @@ -425,6 +430,7 @@
"enum": [
"attention",
"audible",
"autoDiscardable",
"discarded",
"favIconUrl",
"hidden",
Expand Down Expand Up @@ -741,6 +747,11 @@
"optional": true,
"description": "Whether the tabs are audible."
},
"autoDiscardable": {
"type": "boolean",
"optional": true,
"description": "Whether the tab can be discarded automatically by the browser when resources are low."
},
"muted": {
"type": "boolean",
"optional": true,
Expand Down Expand Up @@ -938,6 +949,11 @@
"optional": true,
"description": "Whether the tab should be active. Does not affect whether the window is focused (see $(ref:windows.update))."
},
"autoDiscardable": {
"type": "boolean",
"optional": true,
"description": "Whether the tab can be discarded automatically by the browser when resources are low."
},
"highlighted": {
"type": "boolean",
"optional": true,
Expand Down Expand Up @@ -1615,6 +1631,11 @@
"optional": true,
"description": "The tab's new audible state."
},
"autoDiscardable": {
"type": "boolean",
"optional": true,
"description": "The tab's new autoDiscardable state."
},
"discarded": {
"type": "boolean",
"optional": true,
Expand Down
1 change: 1 addition & 0 deletions browser/components/extensions/test/browser/browser.ini
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,7 @@ skip-if =
[browser_ext_tabs_attention.js]
https_first_disabled = true
[browser_ext_tabs_audio.js]
[browser_ext_tabs_autoDiscardable.js]
[browser_ext_tabs_containerIsolation.js]
https_first_disabled = true
[browser_ext_tabs_cookieStoreId.js]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
"use strict";

add_task(async function test_autoDiscardable() {
let files = {
"schema.json": JSON.stringify([
{
namespace: "experiments",
functions: [
{
name: "unload",
type: "function",
async: "callback",
description:
"Unload the least recently used tab using Firefox's built-in tab unloader mechanism",
parameters: [],
},
],
},
]),
"parent.js": () => {
const { TabUnloader } = ChromeUtils.importESModule(
"resource:///modules/TabUnloader.sys.mjs"
);
const { ExtensionError } = ExtensionUtils;
this.experiments = class extends ExtensionAPI {
getAPI(context) {
return {
experiments: {
async unload() {
try {
await TabUnloader.unloadLeastRecentlyUsedTab(null);
} catch (error) {
// We need to do this, otherwise failures won't bubble up to the test properly.
throw ExtensionError(error);
}
},
},
};
}
};
},
};

async function background() {
let firstTab = await browser.tabs.create({
active: false,
url: "https://example.org/",
});

// Make sure setting and getting works properly
browser.test.assertTrue(
firstTab.autoDiscardable,
"autoDiscardable should always be true by default"
);
let result = await browser.tabs.update(firstTab.id, {
autoDiscardable: false,
});
browser.test.assertFalse(
result.autoDiscardable,
"autoDiscardable should be false after setting it as such"
);
result = await browser.tabs.update(firstTab.id, {
autoDiscardable: true,
});
browser.test.assertTrue(
result.autoDiscardable,
"autoDiscardable should be true after setting it as such"
);
result = await browser.tabs.update(firstTab.id, {
autoDiscardable: false,
});
browser.test.assertFalse(
result.autoDiscardable,
"autoDiscardable should be false after setting it as such"
);

// Make sure the tab can't be unloaded when autoDiscardable is false
await browser.experiments.unload();
result = await browser.tabs.get(firstTab.id);
browser.test.assertFalse(
result.discarded,
"Tab should not unload when autoDiscardable is false"
);

// Make sure the tab CAN be unloaded when autoDiscardable is true
await browser.tabs.update(firstTab.id, {
autoDiscardable: true,
});
await browser.experiments.unload();
result = await browser.tabs.get(firstTab.id);
browser.test.assertTrue(
result.discarded,
"Tab should unload when autoDiscardable is true"
);

// Make sure filtering for discardable tabs works properly
result = await browser.tabs.query({ autoDiscardable: true });
browser.test.assertEq(
2,
result.length,
"tabs.query should return 2 when autoDiscardable is true "
);
await browser.tabs.update(firstTab.id, {
autoDiscardable: false,
});
result = await browser.tabs.query({ autoDiscardable: true });
browser.test.assertEq(
1,
result.length,
"tabs.query should return 1 when autoDiscardable is false"
);

let onUpdatedPromise = {};
onUpdatedPromise.promise = new Promise(
resolve => (onUpdatedPromise.resolve = resolve)
);

// Make sure onUpdated works
async function testOnUpdatedEvent(autoDiscardable) {
browser.test.log(`Testing autoDiscardable = ${autoDiscardable}`);
let onUpdated;
let promise = new Promise(resolve => {
onUpdated = (tabId, changeInfo, tabInfo) => {
browser.test.assertEq(
firstTab.id,
tabId,
"The updated tab's ID should match the correct tab"
);
browser.test.assertDeepEq(
{ autoDiscardable },
changeInfo,
"The updated tab's changeInfo should be correct"
);
browser.test.assertEq(
tabInfo.autoDiscardable,
autoDiscardable,
"The updated tab's tabInfo should be correct"
);
resolve();
};
});
browser.tabs.onUpdated.addListener(onUpdated, {
properties: ["autoDiscardable"],
});
await browser.tabs.update(firstTab.id, { autoDiscardable });
await promise;
browser.tabs.onUpdated.removeListener(onUpdated);
}

await testOnUpdatedEvent(true);
await testOnUpdatedEvent(false);

await browser.tabs.remove(firstTab.id); // Cleanup
browser.test.notifyPass("autoDiscardable");
}
let extension = ExtensionTestUtils.loadExtension({
isPrivileged: true,
manifest: {
permissions: ["tabs"],
experiment_apis: {
experiments: {
schema: "schema.json",
parent: {
scopes: ["addon_parent"],
script: "parent.js",
paths: [["experiments"]],
},
},
},
},
background,
files,
});
await extension.startup();
await extension.awaitFinish("autoDiscardable");
await extension.unload();
});
2 changes: 1 addition & 1 deletion browser/modules/TabUnloader.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ let CRITERIA_WEIGHT = 1;
*/
let DefaultTabUnloaderMethods = {
isNonDiscardable(tab, weight) {
if (tab.selected) {
if (tab.undiscardable || tab.selected) {
return weight;
}

Expand Down
6 changes: 6 additions & 0 deletions mobile/android/components/extensions/ext-android.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,12 @@ class Tab extends TabBase {
return false;
}

get autoDiscardable() {
// This property reflects whether the browser is allowed to auto-discard.
// Since extensions cannot do so on Android, we return true here.
return true;
}

get sharingState() {
return {
screen: undefined,
Expand Down
5 changes: 5 additions & 0 deletions mobile/android/components/extensions/schemas/tabs.json
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,11 @@
"optional": true,
"description": "Whether the tab has produced sound over the past couple of seconds (but it might not be heard if also muted). Equivalent to whether the speaker audio indicator is showing."
},
"autoDiscardable": {
"type": "boolean",
"optional": true,
"description": "Whether the tab can be discarded automatically by the browser when resources are low."
},
"mutedInfo": {
"$ref": "MutedInfo",
"optional": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ prefs =
[test_ext_all_apis.html]
[test_ext_downloads_event_page.html]
[test_ext_tab_runtimeConnect.html]
[test_ext_tabs_autoDiscardable.html]
[test_ext_tabs_create.html]
[test_ext_tabs_events.html]
skip-if =
Expand Down
Loading

0 comments on commit 0289eea

Please sign in to comment.