commit e16e498bd0b7998d903a4832835450598316c1f6 parent c4f2c793384af2f0a03ab8eef49040a67463bbe9 Author: Lando <lando@lando.test> Date: Wed, 12 Nov 2025 00:14:55 +0000 Merge autoland to mozilla-central Diffstat:
206 files changed, 6245 insertions(+), 4111 deletions(-)
diff --git a/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js b/browser/base/content/test/webrtc/browser_devices_get_user_media_paused.js @@ -245,10 +245,14 @@ var gTests = [ // Clone audio and video, their state will be enabled await cloneTracks(true, true); + observerPromise = expectObserverCalled("recording-device-events", 2); + // Disable both audio and video. await setTrackEnabled(false, false); - observerPromise = expectObserverCalled("recording-device-events", 2); + await observerPromise; + + observerPromise = expectObserverCalled("recording-device-events"); // Stop the clones. This should disable the sharing indicators. await stopClonedTracks(true, true); diff --git a/browser/components/preferences/home.inc.xhtml b/browser/components/preferences/home.inc.xhtml @@ -92,6 +92,7 @@ <groupbox id="homeContentsGroup" data-category="paneHome" data-subcategory="contents" + data-srd-groupid="home" hidden="true"> <label><html:h2 data-l10n-id="home-prefs-content-header2" /></label> <description class="description-deemphasized" data-l10n-id="home-prefs-content-description2" /> @@ -112,4 +113,11 @@ <vbox id="highlights" /> </groupbox> + +<html:setting-group + groupid="home" + hidden="true" + data-category="paneHome" + data-subcategory="contents" /> + </html:template> diff --git a/browser/components/preferences/home.js b/browser/components/preferences/home.js @@ -38,6 +38,77 @@ const NEW_TAB_KEY = "newTabURL"; const BLANK_HOMEPAGE_URL = "chrome://browser/content/blanktab.html"; +// New Prefs UI: we need to check for this setting before registering prefs +// so that old-style prefs continue working +if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) { + Preferences.addAll([ + { id: "browser.newtabpage.activity-stream.showSearch", type: "bool" }, + { + id: "browser.newtabpage.activity-stream.system.showWeather", + type: "bool", + }, + { id: "browser.newtabpage.activity-stream.showWeather", type: "bool" }, + { + id: "browser.newtabpage.activity-stream.widgets.system.lists.enabled", + type: "bool", + }, + { + id: "browser.newtabpage.activity-stream.widgets.lists.enabled", + type: "bool", + }, + { + id: "browser.newtabpage.activity-stream.widgets.system.focusTimer.enabled", + type: "bool", + }, + { + id: "browser.newtabpage.activity-stream.widgets.focusTimer.enabled", + type: "bool", + }, + ]); + + // Search + Preferences.addSetting({ + id: "webSearch", + pref: "browser.newtabpage.activity-stream.showSearch", + }); + + // Weather + Preferences.addSetting({ + id: "showWeather", + pref: "browser.newtabpage.activity-stream.system.showWeather", + }); + Preferences.addSetting({ + id: "weather", + pref: "browser.newtabpage.activity-stream.showWeather", + deps: ["showWeather"], + visible: ({ showWeather }) => showWeather.value, + }); + + // Widgets: lists + Preferences.addSetting({ + id: "listsEnabled", + pref: "browser.newtabpage.activity-stream.widgets.system.lists.enabled", + }); + Preferences.addSetting({ + id: "lists", + pref: "browser.newtabpage.activity-stream.widgets.lists.enabled", + deps: ["listsEnabled"], + visible: ({ listsEnabled }) => listsEnabled.value, + }); + + // Widgets: timer + Preferences.addSetting({ + id: "timerEnabled", + pref: "browser.newtabpage.activity-stream.widgets.system.focusTimer.enabled", + }); + Preferences.addSetting({ + id: "timer", + pref: "browser.newtabpage.activity-stream.widgets.focusTimer.enabled", + deps: ["timerEnabled"], + visible: ({ timerEnabled }) => timerEnabled.value, + }); +} + var gHomePane = { HOME_MODE_FIREFOX_HOME: "0", HOME_MODE_BLANK: "1", @@ -666,6 +737,8 @@ var gHomePane = { }, init() { + initSettingGroup("home"); + // Event Listeners document .getElementById("homePageUrl") diff --git a/browser/components/preferences/main.js b/browser/components/preferences/main.js @@ -1177,6 +1177,35 @@ let SETTINGS_CONFIG = { }, ], }, + home: { + inProgress: true, + headingLevel: 2, + l10nId: "home-prefs-content-header", + // Icons are not ready to be used yet. + // iconSrc: "chrome://browser/skin/home.svg", + items: [ + { + id: "webSearch", + l10nId: "home-prefs-search-header2", + control: "moz-toggle", + }, + { + id: "weather", + l10nId: "home-prefs-weather-header", + control: "moz-toggle", + }, + { + id: "lists", + l10nId: "home-prefs-lists-header", + control: "moz-toggle", + }, + { + id: "timer", + l10nId: "home-prefs-timer-header", + control: "moz-toggle", + }, + ], + }, zoom: { // This section is marked as in progress for testing purposes inProgress: true, diff --git a/browser/config/mozconfigs/allowlist b/browser/config/mozconfigs/allowlist @@ -14,9 +14,6 @@ for platform in all_platforms: allowlist['nightly']['macosx64'] += [ 'ac_add_options --enable-instruments', - 'ac_add_options --enable-dtrace', - 'if test `uname -s` != Linux; then', - 'fi', ] allowlist['nightly']['win64'] += [ diff --git a/browser/config/mozconfigs/macosx64-aarch64/devedition b/browser/config/mozconfigs/macosx64-aarch64/devedition @@ -5,11 +5,6 @@ MOZ_REQUIRE_SIGNING= ac_add_options --enable-instruments -# Cross-compiled builds fail when dtrace is enabled -if test `uname -s` != Linux; then - ac_add_options --enable-dtrace -fi - if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then ac_add_options --with-macbundlename-prefix=Firefox fi diff --git a/browser/config/mozconfigs/macosx64-aarch64/nightly b/browser/config/mozconfigs/macosx64-aarch64/nightly @@ -2,11 +2,6 @@ ac_add_options --enable-instruments -# Cross-compiled builds fail when dtrace is enabled -if test `uname -s` != Linux; then - ac_add_options --enable-dtrace -fi - ac_add_options --with-branding=browser/branding/nightly . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/config/mozconfigs/macosx64/devedition b/browser/config/mozconfigs/macosx64/devedition @@ -5,11 +5,6 @@ MOZ_REQUIRE_SIGNING= ac_add_options --enable-instruments -# Cross-compiled builds fail when dtrace is enabled -if test `uname -s` != Linux; then - ac_add_options --enable-dtrace -fi - if test "${MOZ_UPDATE_CHANNEL}" = "nightly"; then ac_add_options --with-macbundlename-prefix=Firefox fi diff --git a/browser/config/mozconfigs/macosx64/nightly b/browser/config/mozconfigs/macosx64/nightly @@ -2,11 +2,6 @@ ac_add_options --enable-instruments -# Cross-compiled builds fail when dtrace is enabled -if test `uname -s` != Linux; then - ac_add_options --enable-dtrace -fi - ac_add_options --with-branding=browser/branding/nightly . "$topsrcdir/build/mozconfig.common.override" diff --git a/browser/extensions/newtab/lib/AboutPreferences.sys.mjs b/browser/extensions/newtab/lib/AboutPreferences.sys.mjs @@ -205,6 +205,13 @@ export class AboutPreferences { * We have to potentially re-assign the `id` if it is `web-search`. * We should restore `id` back to a const after Fx146+. */ + + /* Do not render old-style settings if new settings UI is enabled - this is needed to avoid + * registering prefs twice and ensuing errors */ + if (Services.prefs.getBoolPref("browser.settings-redesign.enabled")) { + return; + } + let { id } = sectionData; const { pref: prefData, diff --git a/browser/installer/linux/app/debian/distribution.ini b/browser/installer/linux/app/debian/distribution.ini @@ -5,5 +5,4 @@ about=Mozilla Firefox Debian Package [Preferences] intl.locale.requested="" -dom.ipc.forkserver.enable=true browser.gnome-search-provider.enabled=true diff --git a/browser/installer/linux/app/rpm/distribution.ini b/browser/installer/linux/app/rpm/distribution.ini @@ -0,0 +1,7 @@ +[Global] +id=mozilla-rpm +version=1.0 +about=Mozilla Firefox RPM Package + +[Preferences] +intl.locale.requested="" diff --git a/browser/installer/linux/app/rpm/firefox.spec.j2 b/browser/installer/linux/app/rpm/firefox.spec.j2 @@ -27,6 +27,7 @@ Source{{ loop.index + 2 }}: {{ metadata.extension_id }}.xpi %{__install} -m 0644 %{SOURCE2} %{buildroot}%{_mandir}/man1/%{name}.1 %{__ln_s} %{mozappdir}/firefox %{buildroot}%{_bindir}/%{name} %{__mkdir_p} %{buildroot}/%{mozappdir}/distribution/extensions +%{__install} -m 0644 %{_sourcedir}/firefox/distribution/distribution.ini %{buildroot}%{mozappdir}/distribution/distribution.ini {%- for codename, metadata in LANGUAGES.items() %} %{__install} -m 0644 %{SOURCE{{ loop.index + 2 }}} %{buildroot}%{mozappdir}/distribution/extensions/{{ metadata.extension_id }}.xpi {%- endfor %} diff --git a/browser/locales/en-US/browser/preferences/preferences.ftl b/browser/locales/en-US/browser/preferences/preferences.ftl @@ -744,8 +744,9 @@ choose-bookmark = ## Home Section - Firefox Home Content Customization +home-prefs-content-header = + .label = { -firefox-home-brand-name } home-prefs-content-header2 = { -firefox-home-brand-name } Content -home-prefs-content-header3 = { -firefox-home-brand-name } home-prefs-content-description2 = Choose what content you want on your { -firefox-home-brand-name } screen. home-prefs-search-header = @@ -791,6 +792,14 @@ home-prefs-trending-search-header = .label = Trending searches home-prefs-trending-search-description = Popular and frequently searched topics +# Lists is a widget on New Tab, similar to a to-do widget +home-prefs-lists-header = + .label = Lists + +# Timer is a widget on New Tab, similar to the Pomodoro timer. +home-prefs-timer-header = + .label = Timer + # "Support" here means to help sustain or contribute to something, especially through funding or sponsorship. home-prefs-support-firefox-header = .label = Support { -brand-product-name } diff --git a/browser/locales/l10n-changesets.json b/browser/locales/l10n-changesets.json @@ -15,7 +15,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "af": { "pin": false, @@ -33,7 +33,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "an": { "pin": false, @@ -51,7 +51,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ar": { "pin": false, @@ -69,7 +69,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ast": { "pin": false, @@ -87,7 +87,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "az": { "pin": false, @@ -105,7 +105,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "be": { "pin": false, @@ -123,7 +123,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bg": { "pin": false, @@ -141,7 +141,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bn": { "pin": false, @@ -159,7 +159,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bo": { "pin": false, @@ -177,7 +177,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "br": { "pin": false, @@ -195,7 +195,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "brx": { "pin": false, @@ -213,7 +213,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bs": { "pin": false, @@ -231,7 +231,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ca": { "pin": false, @@ -249,7 +249,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ca-valencia": { "pin": false, @@ -267,7 +267,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "cak": { "pin": false, @@ -285,7 +285,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ckb": { "pin": false, @@ -303,7 +303,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "cs": { "pin": false, @@ -321,7 +321,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "cy": { "pin": false, @@ -339,7 +339,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "da": { "pin": false, @@ -357,7 +357,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "de": { "pin": false, @@ -375,7 +375,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "dsb": { "pin": false, @@ -393,7 +393,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "el": { "pin": false, @@ -411,7 +411,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "en-CA": { "pin": false, @@ -429,7 +429,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "en-GB": { "pin": false, @@ -447,7 +447,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "eo": { "pin": false, @@ -465,7 +465,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-AR": { "pin": false, @@ -483,7 +483,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-CL": { "pin": false, @@ -501,7 +501,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-ES": { "pin": false, @@ -519,7 +519,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-MX": { "pin": false, @@ -537,7 +537,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "et": { "pin": false, @@ -555,7 +555,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "eu": { "pin": false, @@ -573,7 +573,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fa": { "pin": false, @@ -591,7 +591,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ff": { "pin": false, @@ -609,7 +609,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fi": { "pin": false, @@ -627,7 +627,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fr": { "pin": false, @@ -645,7 +645,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fur": { "pin": false, @@ -663,7 +663,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fy-NL": { "pin": false, @@ -681,7 +681,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ga-IE": { "pin": false, @@ -699,7 +699,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gd": { "pin": false, @@ -717,7 +717,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gl": { "pin": false, @@ -735,7 +735,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gn": { "pin": false, @@ -753,7 +753,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gu-IN": { "pin": false, @@ -771,7 +771,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "he": { "pin": false, @@ -789,7 +789,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hi-IN": { "pin": false, @@ -807,7 +807,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hr": { "pin": false, @@ -825,7 +825,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hsb": { "pin": false, @@ -843,7 +843,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hu": { "pin": false, @@ -861,7 +861,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hy-AM": { "pin": false, @@ -879,7 +879,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hye": { "pin": false, @@ -897,7 +897,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ia": { "pin": false, @@ -915,7 +915,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "id": { "pin": false, @@ -933,7 +933,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "is": { "pin": false, @@ -951,7 +951,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "it": { "pin": false, @@ -969,7 +969,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ja": { "pin": false, @@ -985,7 +985,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ja-JP-mac": { "pin": false, @@ -993,7 +993,7 @@ "macosx64", "macosx64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ka": { "pin": false, @@ -1011,7 +1011,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "kab": { "pin": false, @@ -1029,7 +1029,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "kk": { "pin": false, @@ -1047,7 +1047,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "km": { "pin": false, @@ -1065,7 +1065,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "kn": { "pin": false, @@ -1083,7 +1083,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ko": { "pin": false, @@ -1101,7 +1101,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lij": { "pin": false, @@ -1119,7 +1119,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lo": { "pin": false, @@ -1137,7 +1137,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lt": { "pin": false, @@ -1155,7 +1155,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ltg": { "pin": false, @@ -1173,7 +1173,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lv": { "pin": false, @@ -1191,7 +1191,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "meh": { "pin": false, @@ -1209,7 +1209,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "mk": { "pin": false, @@ -1227,7 +1227,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ml": { "pin": false, @@ -1245,7 +1245,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "mr": { "pin": false, @@ -1263,7 +1263,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ms": { "pin": false, @@ -1281,7 +1281,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "my": { "pin": false, @@ -1299,7 +1299,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "nb-NO": { "pin": false, @@ -1317,7 +1317,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ne-NP": { "pin": false, @@ -1335,7 +1335,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "nl": { "pin": false, @@ -1353,7 +1353,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "nn-NO": { "pin": false, @@ -1371,7 +1371,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "oc": { "pin": false, @@ -1389,7 +1389,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pa-IN": { "pin": false, @@ -1407,7 +1407,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pl": { "pin": false, @@ -1425,7 +1425,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pt-BR": { "pin": false, @@ -1443,7 +1443,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pt-PT": { "pin": false, @@ -1461,7 +1461,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "rm": { "pin": false, @@ -1479,7 +1479,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ro": { "pin": false, @@ -1497,7 +1497,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ru": { "pin": false, @@ -1515,7 +1515,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sat": { "pin": false, @@ -1533,7 +1533,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sc": { "pin": false, @@ -1551,7 +1551,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "scn": { "pin": false, @@ -1569,7 +1569,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sco": { "pin": false, @@ -1587,7 +1587,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "si": { "pin": false, @@ -1605,7 +1605,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sk": { "pin": false, @@ -1623,7 +1623,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "skr": { "pin": false, @@ -1641,7 +1641,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sl": { "pin": false, @@ -1659,7 +1659,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "son": { "pin": false, @@ -1677,7 +1677,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sq": { "pin": false, @@ -1695,7 +1695,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sr": { "pin": false, @@ -1713,7 +1713,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sv-SE": { "pin": false, @@ -1731,7 +1731,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "szl": { "pin": false, @@ -1749,7 +1749,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ta": { "pin": false, @@ -1767,7 +1767,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "te": { "pin": false, @@ -1785,7 +1785,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "tg": { "pin": false, @@ -1803,7 +1803,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "th": { "pin": false, @@ -1821,7 +1821,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "tl": { "pin": false, @@ -1839,7 +1839,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "tr": { "pin": false, @@ -1857,7 +1857,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "trs": { "pin": false, @@ -1875,7 +1875,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "uk": { "pin": false, @@ -1893,7 +1893,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ur": { "pin": false, @@ -1911,7 +1911,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "uz": { "pin": false, @@ -1929,7 +1929,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "vi": { "pin": false, @@ -1947,7 +1947,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "wo": { "pin": false, @@ -1965,7 +1965,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "xh": { "pin": false, @@ -1983,7 +1983,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "zh-CN": { "pin": false, @@ -2001,7 +2001,7 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "zh-TW": { "pin": false, @@ -2019,6 +2019,6 @@ "win64-aarch64-devedition", "win64-devedition" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" } } \ No newline at end of file diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp @@ -7480,10 +7480,7 @@ nsresult nsDocShell::RestoreFromHistory() { // In cases where we use a transient about:blank viewer between loads, // we never show the transient viewer, so _its_ previous viewer is never - // unhooked from the view hierarchy. Destroy any such previous viewer now, - // before we grab the root view sibling, so that we don't grab a view - // that's about to go away. - + // destroyed. Destroy any such previous viewer now. if (mDocumentViewer) { // Make sure to hold a strong ref to previousViewer here while we // drop the reference to it from mDocumentViewer. @@ -7495,38 +7492,12 @@ nsresult nsDocShell::RestoreFromHistory() { } } - // Save off the root view's parent and sibling so that we can insert the - // new content viewer's root view at the same position. Also save the - // bounds of the root view's widget. - - nsView* rootViewSibling = nullptr; - nsView* rootViewParent = nullptr; + // Save the bounds of the root view's widget. LayoutDeviceIntRect newBounds(0, 0, 0, 0); PresShell* oldPresShell = GetPresShell(); if (oldPresShell) { - nsViewManager* vm = oldPresShell->GetViewManager(); - if (vm) { - nsView* oldRootView = vm->GetRootView(); - - if (oldRootView) { - rootViewSibling = oldRootView->GetNextSibling(); - rootViewParent = oldRootView->GetParent(); - - mDocumentViewer->GetBounds(newBounds); - } - } - } - - nsCOMPtr<nsIContent> container; - RefPtr<Document> sibling; - if (rootViewParent && rootViewParent->GetParent()) { - nsIFrame* frame = rootViewParent->GetParent()->GetFrame(); - container = frame ? frame->GetContent() : nullptr; - } - if (rootViewSibling) { - nsIFrame* frame = rootViewSibling->GetFrame(); - sibling = frame ? frame->PresShell()->GetDocument() : nullptr; + mDocumentViewer->GetBounds(newBounds); } // Transfer ownership to mDocumentViewer. By ensuring that either the @@ -7729,39 +7700,6 @@ nsresult nsDocShell::RestoreFromHistory() { nsViewManager* newVM = presShell ? presShell->GetViewManager() : nullptr; nsView* newRootView = newVM ? newVM->GetRootView() : nullptr; - // Insert the new root view at the correct location in the view tree. - if (container) { - nsSubDocumentFrame* subDocFrame = - do_QueryFrame(container->GetPrimaryFrame()); - rootViewParent = subDocFrame ? subDocFrame->EnsureInnerView() : nullptr; - } else { - rootViewParent = nullptr; - } - if (sibling && sibling->GetPresShell() && - sibling->GetPresShell()->GetViewManager()) { - rootViewSibling = sibling->GetPresShell()->GetViewManager()->GetRootView(); - } else { - rootViewSibling = nullptr; - } - if (rootViewParent && newRootView && - newRootView->GetParent() != rootViewParent) { - nsViewManager* parentVM = rootViewParent->GetViewManager(); - if (parentVM) { - // InsertChild(parent, child, sib, true) inserts the child after - // sib in content order, which is before sib in view order. BUT - // when sib is null it inserts at the end of the the document - // order, i.e., first in view order. But when oldRootSibling is - // null, the old root as at the end of the view list --- last in - // content order --- and we want to call InsertChild(parent, child, - // nullptr, false) in that case. - parentVM->InsertChild(rootViewParent, newRootView, rootViewSibling, - rootViewSibling ? true : false); - - NS_ASSERTION(newRootView->GetNextSibling() == rootViewSibling, - "error in InsertChild"); - } - } - nsCOMPtr<nsPIDOMWindowInner> privWinInner = privWin->GetCurrentInnerWindow(); // If parent is suspended, increase suspension count. @@ -7798,15 +7736,6 @@ nsresult nsDocShell::RestoreFromHistory() { // presentation. If this is not the same size we showed it at last time, // then we need to resize the widget. - // XXXbryner This interacts poorly with Firefox's infobar. If the old - // presentation had the infobar visible, then we will resize the new - // presentation to that smaller size. However, firing the locationchanged - // event will hide the infobar, which will immediately resize the window - // back to the larger size. A future optimization might be to restore - // the presentation at the "wrong" size, then fire the locationchanged - // event and check whether the docshell's new size is the same as the - // cached viewer size (skipping the resize if they are equal). - if (newRootView) { if (!newBounds.IsEmpty() && !newBounds.ToUnknownRect().IsEqualEdges(oldBounds)) { @@ -7822,7 +7751,7 @@ nsresult nsDocShell::RestoreFromHistory() { // The FinishRestore call below can kill these, null them out so we don't // have invalid pointer lying around. - newRootView = rootViewSibling = rootViewParent = nullptr; + newRootView = nullptr; newVM = nullptr; // If the IsUnderHiddenEmbedderElement() state has been changed, we need to @@ -13822,8 +13751,21 @@ nsresult nsDocShell::OnOverLink(nsIContent* aContent, nsIURI* aURI, NS_ConvertUTF8toUTF16 uStr(spec); - PredictorPredict(aURI, mCurrentURI, nsINetworkPredictor::PREDICT_LINK, - aContent->NodePrincipal()->OriginAttributesRef(), nullptr); + // The speculative connect used to go through the predictor, but we don't + // need all that just to initiate a speculative connect. + if ((StaticPrefs::network_predictor_enable_hover_on_ssl() && + mCurrentURI->SchemeIs("https")) || + mCurrentURI->SchemeIs("http")) { + if (nsCOMPtr<nsISpeculativeConnect> specService = + mozilla::components::IO::Service()) { + // This would be a navigation, so if this is cross origin the speculative + // connection needs to have the origin of the URL not the current page. + nsCOMPtr<nsIPrincipal> principal = BasePrincipal::CreateContentPrincipal( + aURI, aContent->NodePrincipal()->OriginAttributesRef()); + + specService->SpeculativeConnect(aURI, principal, nullptr, false); + } + } rv = browserChrome->SetLinkStatus(uStr); return rv; diff --git a/docshell/base/nsIDocumentViewer.idl b/docshell/base/nsIDocumentViewer.idl @@ -18,7 +18,7 @@ webidl Node; class nsIWidget; class nsPresContext; -class nsView; +class nsSubDocumentFrame; class nsDOMNavigationTiming; namespace mozilla { class Encoding; @@ -35,7 +35,7 @@ class RemotePrintJobChild; [ptr] native nsIWidgetPtr(nsIWidget); [ref] native LayoutDeviceIntRectRef(mozilla::LayoutDeviceIntRect); [ptr] native nsPresContextPtr(nsPresContext); -[ptr] native nsViewPtr(nsView); +[ptr] native nsSubDocumentFramePtr(nsSubDocumentFrame); [ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming); [ptr] native Encoding(const mozilla::Encoding); [ptr] native PresShellPtr(mozilla::PresShell); @@ -251,7 +251,7 @@ interface nsIDocumentViewer : nsISupports * case, if mParentWidget is null then this document should not even * be displayed. */ - [noscript,notxpcom,nostdcall] nsViewPtr findContainerView(); + [noscript,notxpcom,nostdcall] nsSubDocumentFramePtr findContainerFrame(); /** * Set collector for navigation timing data (load, unload events). */ diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp @@ -7509,7 +7509,8 @@ static inline void AssertNoStaleServoDataIn(nsINode& aSubtreeRoot) { } already_AddRefed<PresShell> Document::CreatePresShell( - nsPresContext* aContext, nsViewManager* aViewManager) { + nsPresContext* aContext, nsViewManager* aViewManager, + nsSubDocumentFrame* aEmbedderFrame) { MOZ_DIAGNOSTIC_ASSERT(!mPresShell, "We have a presshell already!"); NS_ENSURE_FALSE(GetBFCacheEntry(), nullptr); @@ -7520,6 +7521,12 @@ already_AddRefed<PresShell> Document::CreatePresShell( // Note: we don't hold a ref to the shell (it holds a ref to us) mPresShell = presShell; + if (aEmbedderFrame) { + // It's important to do this as soon as possible so that + // GetRootPresContext() and so on do the right thing from the get go. + aEmbedderFrame->AddEmbeddingPresShell(presShell); + } + if (!mStyleSetFilled) { FillStyleSet(); } diff --git a/dom/base/Document.h b/dom/base/Document.h @@ -1175,7 +1175,8 @@ class Document : public nsINode, * presshell if the presshell should observe document mutations. */ MOZ_CAN_RUN_SCRIPT already_AddRefed<PresShell> CreatePresShell( - nsPresContext* aContext, nsViewManager* aViewManager); + nsPresContext* aContext, nsViewManager* aViewManager, + nsSubDocumentFrame* aEmbedderFrame); void DeletePresShell(); PresShell* GetPresShell() const { diff --git a/dom/base/nsContentUtils.h b/dom/base/nsContentUtils.h @@ -118,7 +118,6 @@ class nsNodeInfoManager; class nsParser; class nsPIWindowRoot; class nsPresContext; -class nsView; class nsWrapperCache; enum class WindowMediatorFilter : uint8_t; diff --git a/dom/base/nsDeprecatedOperationList.h b/dom/base/nsDeprecatedOperationList.h @@ -47,6 +47,9 @@ DEPRECATED_OPERATION(InitMouseEvent) DEPRECATED_OPERATION(InitNSMouseEvent) DEPRECATED_OPERATION(MathML_DeprecatedMathSpaceValue2) DEPRECATED_OPERATION(MathML_DeprecatedMathVariant) +DEPRECATED_OPERATION(MathML_DeprecatedMoExplicitAccent) +DEPRECATED_OPERATION(MathML_DeprecatedMoverNonExplicitAccent) +DEPRECATED_OPERATION(MathML_DeprecatedMunderNonExplicitAccentunder) DEPRECATED_OPERATION(FormSubmissionUntrustedEvent) DEPRECATED_OPERATION(ElementSetCapture) DEPRECATED_OPERATION(ElementReleaseCapture) diff --git a/dom/base/nsFrameLoader.cpp b/dom/base/nsFrameLoader.cpp @@ -117,7 +117,6 @@ #include "nsSubDocumentFrame.h" #include "nsThreadUtils.h" #include "nsUnicharUtils.h" -#include "nsView.h" #include "nsViewManager.h" #include "nsXPCOMPrivate.h" // for XUL_DLL #include "nsXULPopupManager.h" @@ -168,7 +167,6 @@ nsFrameLoader::nsFrameLoader(Element* aOwner, BrowsingContext* aBrowsingContext, bool aIsRemoteFrame, bool aNetworkCreated) : mPendingBrowsingContext(aBrowsingContext), mOwnerContent(aOwner), - mDetachedSubdocFrame(nullptr), mPendingSwitchID(0), mChildID(0), mRemoteType(NOT_REMOTE_TYPE), @@ -971,13 +969,8 @@ bool nsFrameLoader::Show(nsSubDocumentFrame* aFrame) { const bool marginsChanged = ds->UpdateFrameMargins(GetMarginAttributes(mOwnerContent)); - nsView* view = aFrame->EnsureInnerView(); - if (!view) { - return false; - } - // If we already have a pres shell (which can happen with <object> / <embed>) - // then hook it up in the view tree. + // then hook it up to the frame. if (PresShell* presShell = ds->GetPresShell()) { // Ensure root scroll frame is reflowed in case margins have changed. if (marginsChanged) { @@ -987,24 +980,12 @@ bool nsFrameLoader::Show(nsSubDocumentFrame* aFrame) { IntrinsicDirty::None, NS_FRAME_IS_DIRTY); } } - nsView* childView = presShell->GetViewManager()->GetRootView(); - MOZ_DIAGNOSTIC_ASSERT(childView); - if (childView->GetParent() == view) { - // We were probably doing a docshell swap and succeeded before getting - // here, hooray, nothing else to do. - return true; - } - - // We did layout before due to <object> or <embed> and now we need to fix - // up our stuff and initialize our docshell below too. - MOZ_DIAGNOSTIC_ASSERT(!view->GetFirstChild()); - MOZ_DIAGNOSTIC_ASSERT(!childView->GetParent(), "Stale view?"); - nsSubDocumentFrame::InsertViewsInReverseOrder(childView, view); - nsSubDocumentFrame::EndSwapDocShellsForViews(view->GetFirstChild()); + aFrame->EnsureEmbeddingPresShell(presShell); + MOZ_DIAGNOSTIC_ASSERT(presShell->GetViewManager()->GetRootView()); } RefPtr<nsDocShell> baseWindow = GetDocShell(); - baseWindow->InitWindow(view->GetWidget(), 0, 0, size.width, size.height); + baseWindow->InitWindow(nullptr, 0, 0, size.width, size.height); baseWindow->SetVisibility(true); NS_ENSURE_TRUE(GetDocShell(), false); @@ -3041,16 +3022,12 @@ nsFrameLoader::GetLazyLoadFrameResumptionState() { return sEmpty; } -void nsFrameLoader::SetDetachedSubdocFrame(nsIFrame* aDetachedFrame) { - mDetachedSubdocFrame = aDetachedFrame; - mHadDetachedFrame = !!aDetachedFrame; +void nsFrameLoader::SetDetachedSubdocs(WeakPresShellArray&& aDocs) { + mDetachedSubdocs = std::move(aDocs); } -nsIFrame* nsFrameLoader::GetDetachedSubdocFrame(bool* aOutIsSet) const { - if (aOutIsSet) { - *aOutIsSet = mHadDetachedFrame; - } - return mDetachedSubdocFrame.GetFrame(); +auto nsFrameLoader::TakeDetachedSubdocs() -> WeakPresShellArray { + return std::move(mDetachedSubdocs); } void nsFrameLoader::ApplySandboxFlags(uint32_t sandboxFlags) { diff --git a/dom/base/nsFrameLoader.h b/dom/base/nsFrameLoader.h @@ -365,18 +365,17 @@ class nsFrameLoader final : public nsStubMutationObserver, mozilla::dom::Element* GetOwnerContent() { return mOwnerContent; } /** - * Stashes a detached nsIFrame on the frame loader. We do this when we're - * destroying the nsSubDocumentFrame. If the nsSubdocumentFrame is - * being reframed we'll restore the detached nsIFrame when it's recreated, - * otherwise we'll discard the old presentation and set the detached - * subdoc nsIFrame to null. + * Stashes a list of detached pres shells on the frame loader. We do this when + * we're destroying the nsSubDocumentFrame. If the nsSubdocumentFrame is being + * reframed we'll restore the detached shells when they're recreated, + * otherwise we'll discard the old presentation and clear these. */ - void SetDetachedSubdocFrame(nsIFrame* aDetachedFrame); - - /** - * Retrieves the detached nsIFrame as set by SetDetachedSubdocFrame(). - */ - nsIFrame* GetDetachedSubdocFrame(bool* aOutIsSet = nullptr) const; + using WeakPresShellArray = nsTArray<nsWeakPtr>; + void SetDetachedSubdocs(WeakPresShellArray&&); + WeakPresShellArray TakeDetachedSubdocs(); + const WeakPresShellArray& GetDetachedSubdocs() const { + return mDetachedSubdocs; + } /** * Applies a new set of sandbox flags. These are merged with the sandbox @@ -508,9 +507,9 @@ class nsFrameLoader final : public nsStubMutationObserver, // our <browser> element. RefPtr<mozilla::dom::Element> mOwnerContentStrong; - // Stores the root frame of the subdocument while the subdocument is being - // reframed. Used to restore the presentation after reframing. - WeakFrame mDetachedSubdocFrame; + // Stores the detached pres shells of subdocuments. + // Used to restore the presentation after reframing. + WeakPresShellArray mDetachedSubdocs; // When performing a process switch, this value is used rather than mURIToLoad // to identify the process-switching load which should be resumed in the diff --git a/dom/base/test/browser_script_loader_js_cache_dyn_import.js b/dom/base/test/browser_script_loader_js_cache_dyn_import.js @@ -21,12 +21,26 @@ add_task(async function testDiskCache_dynamicImport() { ev("load:source", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), ev("diskcache:disabled", "file_js_cache_dyn_importer.mjs"), - ev("load:source", "file_js_cache_dyn_imported1.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported1.mjs", false), - ev("load:source", "file_js_cache_dyn_imported2.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported2.mjs", false), - ev("load:source", "file_js_cache_dyn_imported3.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_dyn_imported1.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported1.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported2.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported2.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported3.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported3.mjs", + false + ), + ]), ], }, { @@ -35,12 +49,26 @@ add_task(async function testDiskCache_dynamicImport() { ev("load:source", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), ev("diskcache:disabled", "file_js_cache_dyn_importer.mjs"), - ev("load:source", "file_js_cache_dyn_imported1.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported1.mjs", false), - ev("load:source", "file_js_cache_dyn_imported2.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported2.mjs", false), - ev("load:source", "file_js_cache_dyn_imported3.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_dyn_imported1.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported1.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported2.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported2.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported3.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported3.mjs", + false + ), + ]), ], }, { @@ -49,12 +77,26 @@ add_task(async function testDiskCache_dynamicImport() { ev("load:source", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), ev("diskcache:disabled", "file_js_cache_dyn_importer.mjs"), - ev("load:source", "file_js_cache_dyn_imported1.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported1.mjs", false), - ev("load:source", "file_js_cache_dyn_imported2.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported2.mjs", false), - ev("load:source", "file_js_cache_dyn_imported3.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_dyn_imported1.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported1.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported2.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported2.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported3.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported3.mjs", + false + ), + ]), ], }, { @@ -63,16 +105,32 @@ add_task(async function testDiskCache_dynamicImport() { ev("load:source", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), ev("diskcache:register", "file_js_cache_dyn_importer.mjs"), - ev("load:source", "file_js_cache_dyn_imported1.mjs", false), - ev("diskcache:register", "file_js_cache_dyn_imported1.mjs", false), - ev("load:source", "file_js_cache_dyn_imported2.mjs", false), - ev("diskcache:register", "file_js_cache_dyn_imported2.mjs", false), - ev("load:source", "file_js_cache_dyn_imported3.mjs", false), - ev("diskcache:register", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_dyn_imported1.mjs", false), + ev( + "diskcache:register", + "file_js_cache_dyn_imported1.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported2.mjs", false), + ev( + "diskcache:register", + "file_js_cache_dyn_imported2.mjs", + false + ), + ev("load:source", "file_js_cache_dyn_imported3.mjs", false), + ev( + "diskcache:register", + "file_js_cache_dyn_imported3.mjs", + false + ), + ]), ev("diskcache:saved", "file_js_cache_dyn_importer.mjs", false), - ev("diskcache:saved", "file_js_cache_dyn_imported1.mjs", false), - ev("diskcache:saved", "file_js_cache_dyn_imported2.mjs", false), - ev("diskcache:saved", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("diskcache:saved", "file_js_cache_dyn_imported1.mjs", false), + ev("diskcache:saved", "file_js_cache_dyn_imported2.mjs", false), + ev("diskcache:saved", "file_js_cache_dyn_imported3.mjs", false), + ]), ], }, { @@ -81,12 +139,26 @@ add_task(async function testDiskCache_dynamicImport() { ev("load:diskcache", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), ev("diskcache:disabled", "file_js_cache_dyn_importer.mjs"), - ev("load:diskcache", "file_js_cache_dyn_imported1.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported1.mjs", false), - ev("load:diskcache", "file_js_cache_dyn_imported2.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported2.mjs", false), - ev("load:diskcache", "file_js_cache_dyn_imported3.mjs", false), - ev("diskcache:disabled", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:diskcache", "file_js_cache_dyn_imported1.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported1.mjs", + false + ), + ev("load:diskcache", "file_js_cache_dyn_imported2.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported2.mjs", + false + ), + ev("load:diskcache", "file_js_cache_dyn_imported3.mjs", false), + ev( + "diskcache:disabled", + "file_js_cache_dyn_imported3.mjs", + false + ), + ]), ], }, ], @@ -122,12 +194,14 @@ add_task(async function testMemoryCache_dynamicImport() { ev("load:source", "file_js_cache_dyn_importer.mjs"), ev("memorycache:saved", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), - ev("load:source", "file_js_cache_dyn_imported1.mjs", false), - ev("memorycache:saved", "file_js_cache_dyn_imported1.mjs", false), - ev("load:source", "file_js_cache_dyn_imported2.mjs", false), - ev("memorycache:saved", "file_js_cache_dyn_imported2.mjs", false), - ev("load:source", "file_js_cache_dyn_imported3.mjs", false), - ev("memorycache:saved", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_dyn_imported1.mjs", false), + ev("memorycache:saved", "file_js_cache_dyn_imported1.mjs", false), + ev("load:source", "file_js_cache_dyn_imported2.mjs", false), + ev("memorycache:saved", "file_js_cache_dyn_imported2.mjs", false), + ev("load:source", "file_js_cache_dyn_imported3.mjs", false), + ev("memorycache:saved", "file_js_cache_dyn_imported3.mjs", false), + ]), ev("diskcache:noschedule"), ], }, @@ -136,9 +210,11 @@ add_task(async function testMemoryCache_dynamicImport() { events: [ ev("load:memorycache", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), - ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + ]), ev("diskcache:noschedule"), ], }, @@ -147,9 +223,11 @@ add_task(async function testMemoryCache_dynamicImport() { events: [ ev("load:memorycache", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), - ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + ]), ev("diskcache:noschedule"), ], }, @@ -158,9 +236,11 @@ add_task(async function testMemoryCache_dynamicImport() { events: [ ev("load:memorycache", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), - ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + ]), unordered([ ev("diskcache:saved", "file_js_cache_dyn_importer.mjs", false), ev("diskcache:saved", "file_js_cache_dyn_imported1.mjs", false), @@ -174,9 +254,11 @@ add_task(async function testMemoryCache_dynamicImport() { events: [ ev("load:memorycache", "file_js_cache_dyn_importer.mjs"), ev("evaluate:module", "file_js_cache_dyn_importer.mjs"), - ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_dyn_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_dyn_imported3.mjs", false), + ]), ev("diskcache:noschedule"), ], }, diff --git a/dom/base/test/browser_script_loader_js_cache_module.js b/dom/base/test/browser_script_loader_js_cache_module.js @@ -101,9 +101,11 @@ add_task(async function testDiskCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:source", "file_js_cache_importer.mjs"), - ev("load:source", "file_js_cache_imported1.mjs", false), - ev("load:source", "file_js_cache_imported2.mjs", false), - ev("load:source", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_imported1.mjs", false), + ev("load:source", "file_js_cache_imported2.mjs", false), + ev("load:source", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:disabled", "file_js_cache_importer.mjs"), // non-top-level modules that don't pass the condition @@ -114,9 +116,11 @@ add_task(async function testDiskCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:source", "file_js_cache_importer.mjs"), - ev("load:source", "file_js_cache_imported1.mjs", false), - ev("load:source", "file_js_cache_imported2.mjs", false), - ev("load:source", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_imported1.mjs", false), + ev("load:source", "file_js_cache_imported2.mjs", false), + ev("load:source", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:disabled", "file_js_cache_importer.mjs"), ], @@ -125,9 +129,11 @@ add_task(async function testDiskCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:source", "file_js_cache_importer.mjs"), - ev("load:source", "file_js_cache_imported1.mjs", false), - ev("load:source", "file_js_cache_imported2.mjs", false), - ev("load:source", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_imported1.mjs", false), + ev("load:source", "file_js_cache_imported2.mjs", false), + ev("load:source", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:disabled", "file_js_cache_importer.mjs"), ], @@ -136,27 +142,35 @@ add_task(async function testDiskCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:source", "file_js_cache_importer.mjs"), - ev("load:source", "file_js_cache_imported1.mjs", false), - ev("load:source", "file_js_cache_imported2.mjs", false), - ev("load:source", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_imported1.mjs", false), + ev("load:source", "file_js_cache_imported2.mjs", false), + ev("load:source", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:register", "file_js_cache_importer.mjs"), - ev("diskcache:register", "file_js_cache_imported1.mjs", false), - ev("diskcache:register", "file_js_cache_imported2.mjs", false), - ev("diskcache:register", "file_js_cache_imported3.mjs", false), + unordered([ + ev("diskcache:register", "file_js_cache_imported1.mjs", false), + ev("diskcache:register", "file_js_cache_imported2.mjs", false), + ev("diskcache:register", "file_js_cache_imported3.mjs", false), + ]), ev("diskcache:saved", "file_js_cache_importer.mjs", false), - ev("diskcache:saved", "file_js_cache_imported1.mjs", false), - ev("diskcache:saved", "file_js_cache_imported2.mjs", false), - ev("diskcache:saved", "file_js_cache_imported3.mjs", false), + unordered([ + ev("diskcache:saved", "file_js_cache_imported1.mjs", false), + ev("diskcache:saved", "file_js_cache_imported2.mjs", false), + ev("diskcache:saved", "file_js_cache_imported3.mjs", false), + ]), ], }, { file: "file_js_cache_importer.mjs", events: [ ev("load:diskcache", "file_js_cache_importer.mjs"), - ev("load:diskcache", "file_js_cache_imported1.mjs", false), - ev("load:diskcache", "file_js_cache_imported2.mjs", false), - ev("load:diskcache", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:diskcache", "file_js_cache_imported1.mjs", false), + ev("load:diskcache", "file_js_cache_imported2.mjs", false), + ev("load:diskcache", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:disabled", "file_js_cache_importer.mjs"), ], @@ -284,12 +298,14 @@ add_task(async function testMemoryCache_modules() { events: [ ev("load:source", "file_js_cache_importer.mjs"), ev("memorycache:saved", "file_js_cache_importer.mjs"), - ev("load:source", "file_js_cache_imported1.mjs", false), - ev("memorycache:saved", "file_js_cache_imported1.mjs", false), - ev("load:source", "file_js_cache_imported2.mjs", false), - ev("memorycache:saved", "file_js_cache_imported2.mjs", false), - ev("load:source", "file_js_cache_imported3.mjs", false), - ev("memorycache:saved", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:source", "file_js_cache_imported1.mjs", false), + ev("memorycache:saved", "file_js_cache_imported1.mjs", false), + ev("load:source", "file_js_cache_imported2.mjs", false), + ev("memorycache:saved", "file_js_cache_imported2.mjs", false), + ev("load:source", "file_js_cache_imported3.mjs", false), + ev("memorycache:saved", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:noschedule"), ], @@ -298,9 +314,11 @@ add_task(async function testMemoryCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:memorycache", "file_js_cache_importer.mjs"), - ev("load:memorycache", "file_js_cache_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:noschedule"), ], @@ -309,9 +327,11 @@ add_task(async function testMemoryCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:memorycache", "file_js_cache_importer.mjs"), - ev("load:memorycache", "file_js_cache_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:noschedule"), ], @@ -320,9 +340,11 @@ add_task(async function testMemoryCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:memorycache", "file_js_cache_importer.mjs"), - ev("load:memorycache", "file_js_cache_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), // SharedScriptCache iterates over unordered hashmap while // saving. @@ -338,9 +360,11 @@ add_task(async function testMemoryCache_modules() { file: "file_js_cache_importer.mjs", events: [ ev("load:memorycache", "file_js_cache_importer.mjs"), - ev("load:memorycache", "file_js_cache_imported1.mjs", false), - ev("load:memorycache", "file_js_cache_imported2.mjs", false), - ev("load:memorycache", "file_js_cache_imported3.mjs", false), + unordered([ + ev("load:memorycache", "file_js_cache_imported1.mjs", false), + ev("load:memorycache", "file_js_cache_imported2.mjs", false), + ev("load:memorycache", "file_js_cache_imported3.mjs", false), + ]), ev("evaluate:module", "file_js_cache_importer.mjs"), ev("diskcache:noschedule"), ], diff --git a/dom/base/test/jsmodules/importmaps/chrome.toml b/dom/base/test/jsmodules/importmaps/chrome.toml @@ -2,18 +2,25 @@ support-files = [ "external_importMap.js", "insert_a_base_element.js", + "module_1979050.mjs", "module_simpleImportMap.mjs", "module_simpleImportMap_dir.mjs", "module_simpleImportMap_remap.mjs", "module_simpleImportMap_remap_https.mjs", "module_simpleExport.mjs", "module_sortedImportMap.mjs", + "scope1/module_1979050.mjs", "scope1/module_simpleExport.mjs", "scope1/module_simpleImportMap.mjs", + "scope1/scope2/module_1979050.mjs", "scope1/scope2/module_simpleExport.mjs", "scope1/scope2/module_simpleImportMap.mjs", ] +["test_1979050.html"] + +["test_1979050_2.html"] + ["test_dynamic_import_reject_importMap.html"] ["test_externalImportMap.html"] diff --git a/dom/base/test/jsmodules/importmaps/module_1979050.mjs b/dom/base/test/jsmodules/importmaps/module_1979050.mjs @@ -0,0 +1,3 @@ +import { x } from "./module_simpleExport.mjs"; + +result = x; diff --git a/dom/base/test/jsmodules/importmaps/scope1/module_1979050.mjs b/dom/base/test/jsmodules/importmaps/scope1/module_1979050.mjs @@ -0,0 +1,5 @@ +import { x } from "./module_simpleExport.mjs"; + +result2 = x; + +export let y = x; diff --git a/dom/base/test/jsmodules/importmaps/scope1/scope2/module_1979050.mjs b/dom/base/test/jsmodules/importmaps/scope1/scope2/module_1979050.mjs @@ -0,0 +1,5 @@ +import { x } from "./module_simpleExport.mjs"; + +result3 = x; + +export let z = x; diff --git a/dom/base/test/jsmodules/importmaps/test_1979050.html b/dom/base/test/jsmodules/importmaps/test_1979050.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test bug 1979050</title> +<script> +Object.prototype.imports = { + "./module_simpleExport.mjs": "./scope1/module_simpleExport.mjs", +}; +Object.prototype.scopes = { + "chrome://mochitests/content/chrome/dom/base/test/jsmodules/importmaps/scope1/": { + "./scope1/module_simpleExport.mjs": "/content/chrome/dom/base/test/jsmodules/importmaps/module_simpleExport.mjs" + } +}; +Object.prototype.integrity = { + "./scope1/scope2/module_simpleExport.mjs": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC" +}; + +</script> +<script type="importmap"> +{} +</script> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script type="module" src="module_1979050.mjs"></script> +<script type="module" src="./scope1/module_1979050.mjs"></script> +<script type="module" src="./scope1/scope2/module_1979050.mjs"></script> +<script> + var result, result2, result3; + + SimpleTest.waitForExplicitFinish(); + + // eslint-disable-next-line no-unused-vars + function testLoaded() { + ok(result == 42, 'Check static imported value result: ' + result); + ok(result2 == 84, 'Check static imported value result2: ' + result2); + ok(result3 == 126, 'Check static imported value result3: ' + result3); + + import("./module_simpleExport.mjs").then((ns) => { + ok(ns.x == 42, 'Check dynamic imported value result: ' + ns.x); + return import("./scope1/module_1979050.mjs"); + }).then((ns) => { + ok(ns.y == 84, 'Check dynamic imported value ns.y: ' + ns.y); + return import("./scope1/scope2/module_1979050.mjs"); + }).then((ns) => { + ok(ns.z == 126, 'Check dynamic imported value ns.z: ' + ns.z); + SimpleTest.finish(); + }); + } +</script> +<body onload='testLoaded()'></body> diff --git a/dom/base/test/jsmodules/importmaps/test_1979050_2.html b/dom/base/test/jsmodules/importmaps/test_1979050_2.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>Test bug 1979050</title> +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> +<script> +Object.defineProperty(Object.prototype, "imports", + { + get() { + ok(false, "imports getter"); + return { + "./module_simpleExport.mjs": "./scope1/module_simpleExport.mjs", + }; + } + }); + +Object.defineProperty(Object.prototype, "scopes", + { + get() { + ok(false, "scopes getter"); + return { + "chrome://mochitests/content/chrome/dom/base/test/jsmodules/importmaps/scope1/": { + "./scope1/module_simpleExport.mjs": "/content/chrome/dom/base/test/jsmodules/importmaps/module_simpleExport.mjs" + } + }; + } + }); + +Object.defineProperty(Object.prototype, "integrity ", + { + get() { + ok(false, "scopes getter"); + return { + "./scope1/scope2/module_simpleExport.mjs": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC" + }; + } + }); + +</script> +<script type="importmap"> +{} +</script> + +<script type="module" src="module_1979050.mjs"></script> +<script type="module" src="./scope1/module_1979050.mjs"></script> +<script type="module" src="./scope1/scope2/module_1979050.mjs"></script> +<script> + var result, result2, result3; + + SimpleTest.waitForExplicitFinish(); + + // eslint-disable-next-line no-unused-vars + function testLoaded() { + ok(result == 42, 'Check static imported value result: ' + result); + ok(result2 == 84, 'Check static imported value result2: ' + result2); + ok(result3 == 126, 'Check static imported value result3: ' + result3); + + import("./module_simpleExport.mjs").then((ns) => { + ok(ns.x == 42, 'Check dynamic imported value result: ' + ns.x); + return import("./scope1/module_1979050.mjs"); + }).then((ns) => { + ok(ns.y == 84, 'Check dynamic imported value ns.y: ' + ns.y); + return import("./scope1/scope2/module_1979050.mjs"); + }).then((ns) => { + ok(ns.z == 126, 'Check dynamic imported value ns.z: ' + ns.z); + SimpleTest.finish(); + }); + } +</script> +<body onload='testLoaded()'></body> diff --git a/dom/base/use_counter_metrics.yaml b/dom/base/use_counter_metrics.yaml @@ -15642,6 +15642,57 @@ use.counter.deprecated_ops.page: send_in_pings: - use-counters + math_ml__deprecated_mo_explicit_accent: + type: counter + description: > + Whether a page used MathML_DeprecatedMoExplicitAccent. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + + math_ml__deprecated_mover_non_explicit_accent: + type: counter + description: > + Whether a page used MathML_DeprecatedMoverNonExplicitAccent. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + + math_ml__deprecated_munder_non_explicit_accentunder: + type: counter + description: > + Whether a page used MathML_DeprecatedMunderNonExplicitAccentunder. + Compare against `use.counter.top_level_content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + form_submission_untrusted_event: type: counter description: > @@ -16409,6 +16460,57 @@ use.counter.deprecated_ops.doc: send_in_pings: - use-counters + math_ml__deprecated_mo_explicit_accent: + type: counter + description: > + Whether a document used MathML_DeprecateMoExplicitAccent. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + + math_ml__deprecated_mover_non_explicit_accent: + type: counter + description: > + Whether a document used MathML_DeprecatedMoverNonExplicitAccent. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + + math_ml__deprecated_munder_non_explicit_accentunder: + type: counter + description: > + Whether a document used MathML_DeprecatedMunderNonExplicitAccentunder. + Compare against `use.counter.content_documents_destroyed` + to calculate the rate. + bugs: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + data_reviews: + - https://bugzilla.mozilla.org/show_bug.cgi?id=1852098 + notification_emails: + - dom-core@mozilla.com + - emilio@mozilla.com + expires: never + send_in_pings: + - use-counters + form_submission_untrusted_event: type: counter description: > diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp @@ -1126,7 +1126,7 @@ void BrowserParent::UpdateDimensions(const LayoutDeviceIntRect& rect, LayoutDeviceIntPoint clientOffset = GetClientOffset(); LayoutDeviceIntPoint chromeOffset = !GetBrowserBridgeParent() - ? -GetChildProcessOffset() + ? GetChildProcessOffset() : LayoutDeviceIntPoint(); if (!mUpdatedDimensions || mDimensions != size || !mRect.IsEqualEdges(rect) || @@ -2717,7 +2717,7 @@ BrowserParent::GetChildToParentConversionMatrix() { if (mChildToParentConversionMatrix) { return *mChildToParentConversionMatrix; } - LayoutDevicePoint offset(-GetChildProcessOffset()); + LayoutDevicePoint offset(GetChildProcessOffset()); return LayoutDeviceToLayoutDeviceMatrix4x4::Translation(offset); } @@ -2741,34 +2741,20 @@ void BrowserParent::SetChildToParentConversionMatrix( LayoutDeviceIntPoint BrowserParent::GetChildProcessOffset() { // The "toplevel widget" in child processes is always at position // 0,0. Map the event coordinates to match that. - - LayoutDeviceIntPoint offset(0, 0); RefPtr<nsFrameLoader> frameLoader = GetFrameLoader(); if (!frameLoader) { - return offset; + return {}; } nsIFrame* targetFrame = frameLoader->GetPrimaryFrameOfOwningContent(); if (!targetFrame) { - return offset; + return {}; } nsCOMPtr<nsIWidget> widget = GetWidget(); if (!widget) { - return offset; - } - - nsPresContext* presContext = targetFrame->PresContext(); - nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame(); - nsView* rootView = rootFrame ? rootFrame->GetView() : nullptr; - if (!rootView) { - return offset; + return {}; } - // Note that we don't want to take into account transforms here: -#if 0 - nsPoint pt(0, 0); - nsLayoutUtils::TransformPoint(targetFrame, rootFrame, pt); -#endif // In practice, when transforms are applied to this frameLoader, we currently // get the wrong results whether we take transforms into account here or not. // But applying transforms here gives us the wrong results in all @@ -2780,15 +2766,13 @@ LayoutDeviceIntPoint BrowserParent::GetChildProcessOffset() { // What we actually need to do is apply the transforms to the coordinates of // any events we send to the child, and reverse them for any screen // coordinates that we retrieve from the child. - - // TODO: Once we take into account transforms here, set viewportType - // correctly. For now we use Visual as this means we don't apply - // the layout-to-visual transform in TranslateViewToWidget(). - ViewportType viewportType = ViewportType::Visual; - - nsPoint pt = targetFrame->GetOffsetTo(rootFrame); - return -nsLayoutUtils::TranslateViewToWidget(presContext, rootView, pt, - viewportType, widget); + auto point = nsLayoutUtils::FrameToWidgetOffset(targetFrame, widget); + if (!point) { + return {}; + } + nsPresContext* pc = targetFrame->PresContext(); + return LayoutDeviceIntPoint::FromAppUnitsRounded(*point, + pc->AppUnitsPerDevPixel()); } LayoutDeviceIntPoint BrowserParent::GetClientOffset() { diff --git a/dom/locales/en-US/chrome/dom/dom.properties b/dom/locales/en-US/chrome/dom/dom.properties @@ -404,6 +404,12 @@ InitNSMouseEventWarning=initNSMouseEvent() is deprecated. Use the MouseEvent() c MathML_DeprecatedMathSpaceValue2Warning=MathML length value “%S” is deprecated and will be removed at a future date. # LOCALIZATION NOTE: Do not translate mathvariant or MathML. %S is the deprecated value of the mathvariant attribute. MathML_DeprecatedMathVariantWarning=“mathvariant='%S'” on MathML elements is deprecated and will be removed at a future date. +# LOCALIZATION NOTE: Do not translate accent and mo (it's the name of a MathML element). %1$S is either accent or accentunder. %2$S is the name of the parent frame. +MathML_DeprecatedMoExplicitAccentWarning=Setting the accent attribute on mo elements is deprecated. Use the “%1$S” attribute on “%2$S” instead. +# LOCALIZATION NOTE: Do not translate accent. %S is either mover or munderover. +MathML_DeprecatedMoverNonExplicitAccentWarning=Inferring the accent property from the core operator is deprecated. Consider adding an explicit accent attribute to “%S”. +# LOCALIZATION NOTE: Do not translate accentunder. %S is either munder or munderover. +MathML_DeprecatedMunderNonExplicitAccentunderWarning=Inferring the accentunder property from the core operator is deprecated. Consider adding an explicit accentunder attribute to “%S”. FormSubmissionUntrustedEventWarning=Form submission via untrusted submit event is deprecated and will be removed at a future date. WebShareAPI_Failed=The share operation has failed. diff --git a/dom/media/AudioStreamTrack.cpp b/dom/media/AudioStreamTrack.cpp @@ -40,9 +40,8 @@ void AudioStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType) { MediaStreamTrack::GetLabel(aLabel, aCallerType); } -already_AddRefed<MediaStreamTrack> AudioStreamTrack::CloneInternal() { - return do_AddRef(new AudioStreamTrack(mWindow, mInputTrack, mSource, - ReadyState(), Muted(), mConstraints)); +already_AddRefed<MediaStreamTrack> AudioStreamTrack::Clone() { + return MediaStreamTrack::CloneInternal<AudioStreamTrack>(); } } // namespace mozilla::dom diff --git a/dom/media/AudioStreamTrack.h b/dom/media/AudioStreamTrack.h @@ -24,6 +24,8 @@ class AudioStreamTrack : public MediaStreamTrack { : MediaStreamTrack(aWindow, aInputTrack, aSource, aReadyState, aMuted, aConstraints) {} + already_AddRefed<MediaStreamTrack> Clone() override; + AudioStreamTrack* AsAudioStreamTrack() override { return this; } const AudioStreamTrack* AsAudioStreamTrack() const override { return this; } @@ -38,9 +40,6 @@ class AudioStreamTrack : public MediaStreamTrack { void GetKind(nsAString& aKind) override { aKind.AssignLiteral("audio"); } void GetLabel(nsAString& aLabel, CallerType aCallerType) override; - - protected: - already_AddRefed<MediaStreamTrack> CloneInternal() override; }; } // namespace mozilla::dom diff --git a/dom/media/CubebInputStream.cpp b/dom/media/CubebInputStream.cpp @@ -112,18 +112,17 @@ UniquePtr<CubebInputStream> CubebInputStream::Create(cubeb_devid aDeviceId, LOG("Create a cubeb stream %p successfully", inputStream.get()); - UniquePtr<CubebInputStream> stream( - new CubebInputStream(listener.forget(), std::move(inputStream))); + UniquePtr<CubebInputStream> stream(new CubebInputStream( + listener.forget(), handle.forget(), std::move(inputStream))); stream->Init(); return stream; } CubebInputStream::CubebInputStream( already_AddRefed<Listener>&& aListener, + already_AddRefed<CubebUtils::CubebHandle>&& aCubeb, UniquePtr<cubeb_stream, CubebDestroyPolicy>&& aStream) - : mListener(aListener), - mCubeb(CubebUtils::GetCubeb()), - mStream(std::move(aStream)) { + : mListener(aListener), mCubeb(aCubeb), mStream(std::move(aStream)) { MOZ_ASSERT(mListener); MOZ_ASSERT(mStream); } diff --git a/dom/media/CubebInputStream.h b/dom/media/CubebInputStream.h @@ -61,6 +61,7 @@ class CubebInputStream final { void operator()(cubeb_stream* aStream) const; }; CubebInputStream(already_AddRefed<Listener>&& aListener, + already_AddRefed<CubebUtils::CubebHandle>&& aCubeb, UniquePtr<cubeb_stream, CubebDestroyPolicy>&& aStream); void Init(); diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp @@ -3493,12 +3493,12 @@ void MediaDecoderStateMachine::AudioAudibleChanged(bool aAudible) { MediaSink* MediaDecoderStateMachine::CreateAudioSink() { if (mOutputCaptureState != MediaDecoder::OutputCaptureState::None) { DecodedStream* stream = new DecodedStream( - this, + OwnerThread(), mOutputCaptureState == MediaDecoder::OutputCaptureState::Capture ? mOutputDummyTrack.Ref() : nullptr, - mOutputTracks, mVolume, mPlaybackRate, mPreservesPitch, mAudioQueue, - mVideoQueue, mSinkDevice.Ref()); + mOutputTracks, CanonicalOutputPrincipal(), mVolume, mPlaybackRate, + mPreservesPitch, mAudioQueue, mVideoQueue); mAudibleListener.DisconnectIfExists(); mAudibleListener = stream->AudibleEvent().Connect( OwnerThread(), this, &MediaDecoderStateMachine::AudioAudibleChanged); diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp @@ -210,6 +210,11 @@ struct DeviceState { MOZ_ASSERT(mTrackSource); } + // true if we have allocated mDevice. When not allocated, we may not stop or + // deallocate. + // MainThread only. + bool mAllocated = false; + // true if we have stopped mDevice, this is a terminal state. // MainThread only. bool mStopped = false; @@ -389,13 +394,29 @@ class DeviceListener : public SupportsWeakPtr { * Marks this listener as active and creates the internal device state. */ void Activate(RefPtr<LocalMediaDevice> aDevice, - RefPtr<LocalTrackSource> aTrackSource, bool aStartMuted); + RefPtr<LocalTrackSource> aTrackSource, bool aStartMuted, + bool aIsAllocated); /** * Posts a task to initialize and start the associated device. */ RefPtr<DeviceListenerPromise> InitializeAsync(); + private: + /** + * Initializes synchronously. Must be called on the media thread. + */ + nsresult Initialize(PrincipalHandle aPrincipal, LocalMediaDevice* aDevice, + MediaTrack* aTrack, bool aStartDevice); + + public: + /** + * Synchronously clones this device listener, setting up the device to match + * our current device state asynchronously. Settings, constraints and other + * main thread state starts applying immediately. + */ + already_AddRefed<DeviceListener> Clone() const; + /** * Posts a task to stop the device associated with this DeviceListener and * notifies the associated window listener that a track was stopped. @@ -466,6 +487,10 @@ class DeviceListener : public SupportsWeakPtr { return mDeviceState ? mDeviceState->mDevice.get() : nullptr; } + LocalTrackSource* GetTrackSource() const { + return mDeviceState ? mDeviceState->mTrackSource.get() : nullptr; + } + bool Activated() const { return static_cast<bool>(mDeviceState); } bool Stopped() const { return mStopped; } @@ -563,7 +588,7 @@ class GetUserMediaWindowListener { */ void Activate(RefPtr<DeviceListener> aListener, RefPtr<LocalMediaDevice> aDevice, - RefPtr<LocalTrackSource> aTrackSource) { + RefPtr<LocalTrackSource> aTrackSource, bool aIsAllocated) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aListener); MOZ_ASSERT(!aListener->Activated()); @@ -581,7 +606,8 @@ class GetUserMediaWindowListener { } mInactiveListeners.RemoveElement(aListener); - aListener->Activate(std::move(aDevice), std::move(aTrackSource), muted); + aListener->Activate(std::move(aDevice), std::move(aTrackSource), muted, + aIsAllocated); mActiveListeners.AppendElement(std::move(aListener)); } @@ -808,7 +834,7 @@ class LocalTrackSource : public MediaStreamTrackSource { LocalTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel, const RefPtr<DeviceListener>& aListener, MediaSourceEnum aSource, MediaTrack* aTrack, - RefPtr<PeerIdentity> aPeerIdentity, + RefPtr<const PeerIdentity> aPeerIdentity, TrackingId aTrackingId = TrackingId()) : MediaStreamTrackSource(aPrincipal, aLabel, std::move(aTrackingId)), mSource(aSource), @@ -862,6 +888,20 @@ class LocalTrackSource : public MediaStreamTrackSource { } } + CloneResult Clone() override { + if (!mListener) { + return {}; + } + RefPtr listener = mListener->Clone(); + MOZ_ASSERT(listener); + if (!listener) { + return {}; + } + + return {.mSource = listener->GetTrackSource(), + .mInputTrack = listener->GetTrackSource()->mTrack}; + } + void Disable() override { if (mListener) { mListener->SetDeviceEnabled(false); @@ -1151,6 +1191,11 @@ const TrackingId& LocalMediaDevice::GetTrackingId() const { return mSource->GetTrackingId(); } +const dom::MediaTrackConstraints& LocalMediaDevice::Constraints() const { + MOZ_ASSERT(MediaManager::IsInMediaThread()); + return mConstraints; +} + // Threadsafe since mKind and mSource are const. NS_IMETHODIMP LocalMediaDevice::GetMediaSource(nsAString& aMediaSource) { @@ -1175,7 +1220,12 @@ nsresult LocalMediaDevice::Allocate(const MediaTrackConstraints& aConstraints, return NS_ERROR_FAILURE; } - return Source()->Allocate(aConstraints, aPrefs, aWindowID, aOutBadConstraint); + nsresult rv = + Source()->Allocate(aConstraints, aPrefs, aWindowID, aOutBadConstraint); + if (NS_SUCCEEDED(rv)) { + mConstraints = aConstraints; + } + return rv; } void LocalMediaDevice::SetTrack(const RefPtr<MediaTrack>& aTrack, @@ -1219,7 +1269,11 @@ nsresult LocalMediaDevice::Reconfigure( } } } - return Source()->Reconfigure(aConstraints, aPrefs, aOutBadConstraint); + nsresult rv = Source()->Reconfigure(aConstraints, aPrefs, aOutBadConstraint); + if (NS_SUCCEEDED(rv)) { + mConstraints = aConstraints; + } + return rv; } nsresult LocalMediaDevice::FocusOnSelectedSource() { @@ -1239,6 +1293,21 @@ nsresult LocalMediaDevice::Deallocate() { return mSource->Deallocate(); } +already_AddRefed<LocalMediaDevice> LocalMediaDevice::Clone() const { + MOZ_ASSERT(NS_IsMainThread()); + auto device = MakeRefPtr<LocalMediaDevice>(mRawDevice, mID, mGroupID, mName); + device->mSource = + mRawDevice->mEngine->CreateSourceFrom(mSource, device->mRawDevice); +#ifdef MOZ_THREAD_SAFETY_OWNERSHIP_CHECKS_SUPPORTED + // The source is normally created on the MediaManager thread. But for cloning, + // it ends up being created on main thread. Make sure its owning event target + // is set properly. + auto* src = device->Source(); + src->_mOwningThread = mSource->_mOwningThread; +#endif + return device.forget(); +} + MediaSourceEnum MediaDevice::GetMediaSource() const { return mMediaSource; } static const MediaTrackConstraints& GetInvariant( @@ -1766,11 +1835,13 @@ void GetUserMediaStreamTask::PrepareDOMStream() { // is freed when the page is invalidated (on navigation or close). if (mAudioDeviceListener) { mWindowListener->Activate(mAudioDeviceListener, mAudioDevice, - std::move(audioTrackSource)); + std::move(audioTrackSource), + /*aIsAllocated=*/true); } if (mVideoDeviceListener) { mWindowListener->Activate(mVideoDeviceListener, mVideoDevice, - std::move(videoTrackSource)); + std::move(videoTrackSource), + /*aIsAllocated=*/true); } // Dispatch to the media thread to ask it to start the sources, because that @@ -4255,7 +4326,7 @@ void DeviceListener::Register(GetUserMediaWindowListener* aListener) { void DeviceListener::Activate(RefPtr<LocalMediaDevice> aDevice, RefPtr<LocalTrackSource> aTrackSource, - bool aStartMuted) { + bool aStartMuted, bool aIsAllocated) { MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); LOG("DeviceListener %p activating %s device %p", this, @@ -4281,6 +4352,7 @@ void DeviceListener::Activate(RefPtr<LocalMediaDevice> aDevice, mDeviceState = MakeUnique<DeviceState>( std::move(aDevice), std::move(aTrackSource), offWhileDisabled); mDeviceState->mDeviceMuted = aStartMuted; + mDeviceState->mAllocated = aIsAllocated; if (aStartMuted) { mDeviceState->mTrackSource->Mute(); } @@ -4291,51 +4363,24 @@ DeviceListener::InitializeAsync() { MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); MOZ_DIAGNOSTIC_ASSERT(!mStopped); - return MediaManager::Dispatch<DeviceListenerPromise>( - __func__, - [principal = GetPrincipalHandle(), device = mDeviceState->mDevice, + return InvokeAsync( + MediaManager::Get()->mMediaThread, __func__, + [this, self = RefPtr(this), principal = GetPrincipalHandle(), + device = mDeviceState->mDevice, track = mDeviceState->mTrackSource->mTrack, - deviceMuted = mDeviceState->mDeviceMuted]( - MozPromiseHolder<DeviceListenerPromise>& aHolder) { - auto kind = device->Kind(); - device->SetTrack(track, principal); - nsresult rv = deviceMuted ? NS_OK : device->Start(); - if (kind == MediaDeviceKind::Audioinput || - kind == MediaDeviceKind::Videoinput) { - if ((rv == NS_ERROR_NOT_AVAILABLE && - kind == MediaDeviceKind::Audioinput) || - (NS_FAILED(rv) && kind == MediaDeviceKind::Videoinput)) { - PR_Sleep(200); - rv = device->Start(); - } - if (rv == NS_ERROR_NOT_AVAILABLE && - kind == MediaDeviceKind::Audioinput) { - nsCString log; - log.AssignLiteral("Concurrent mic process limit."); - aHolder.Reject(MakeRefPtr<MediaMgrError>( - MediaMgrError::Name::NotReadableError, - std::move(log)), - __func__); - return; - } - } - if (NS_FAILED(rv)) { - nsCString log; - log.AppendPrintf("Starting %s failed", - dom::GetEnumString(kind).get()); - aHolder.Reject( - MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError, - std::move(log)), - __func__); - return; + deviceMuted = mDeviceState->mDeviceMuted] { + nsresult rv = Initialize(principal, device, track, + /*aStartDevice=*/!deviceMuted); + if (NS_SUCCEEDED(rv)) { + return GenericPromise::CreateAndResolve( + true, "DeviceListener::InitializeAsync success"); } - LOG("started %s device %p", dom::GetEnumString(kind).get(), - device.get()); - aHolder.Resolve(true, __func__); + return GenericPromise::CreateAndReject( + rv, "DeviceListener::InitializeAsync failure"); }) ->Then( GetMainThreadSerialEventTarget(), __func__, - [self = RefPtr<DeviceListener>(this), this]() { + [self = RefPtr<DeviceListener>(this), this](bool) { if (mStopped) { // We were shut down during the async init return DeviceListenerPromise::CreateAndResolve(true, __func__); @@ -4350,10 +4395,25 @@ DeviceListener::InitializeAsync() { mDeviceState->mTrackEnabledTime = TimeStamp::Now(); return DeviceListenerPromise::CreateAndResolve(true, __func__); }, - [self = RefPtr<DeviceListener>(this), - this](const RefPtr<MediaMgrError>& aResult) { + [self = RefPtr<DeviceListener>(this), this](nsresult aRv) { + auto kind = mDeviceState->mDevice->Kind(); + RefPtr<MediaMgrError> err; + if (aRv == NS_ERROR_NOT_AVAILABLE && + kind == MediaDeviceKind::Audioinput) { + nsCString log; + log.AssignLiteral("Concurrent mic process limit."); + err = MakeRefPtr<MediaMgrError>( + MediaMgrError::Name::NotReadableError, std::move(log)); + } else if (NS_FAILED(aRv)) { + nsCString log; + log.AppendPrintf("Starting %s failed", + dom::GetEnumString(kind).get()); + err = MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError, + std::move(log)); + } + if (mStopped) { - return DeviceListenerPromise::CreateAndReject(aResult, __func__); + return DeviceListenerPromise::CreateAndReject(err, __func__); } MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mTrackEnabled); @@ -4361,10 +4421,150 @@ DeviceListener::InitializeAsync() { MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mStopped); Stop(); - return DeviceListenerPromise::CreateAndReject(aResult, __func__); + + return DeviceListenerPromise::CreateAndReject(err, __func__); }); } +nsresult DeviceListener::Initialize(PrincipalHandle aPrincipal, + LocalMediaDevice* aDevice, + MediaTrack* aTrack, bool aStartDevice) { + MOZ_ASSERT(MediaManager::IsInMediaThread()); + + auto kind = aDevice->Kind(); + aDevice->SetTrack(aTrack, aPrincipal); + nsresult rv = aStartDevice ? aDevice->Start() : NS_OK; + if (kind == MediaDeviceKind::Audioinput || + kind == MediaDeviceKind::Videoinput) { + if ((rv == NS_ERROR_NOT_AVAILABLE && kind == MediaDeviceKind::Audioinput) || + (NS_FAILED(rv) && kind == MediaDeviceKind::Videoinput)) { + PR_Sleep(200); + rv = aDevice->Start(); + } + } + LOG("started %s device %p", dom::GetEnumString(kind).get(), aDevice); + return rv; +} + +already_AddRefed<DeviceListener> DeviceListener::Clone() const { + MOZ_ASSERT(NS_IsMainThread()); + MediaManager* mgr = MediaManager::GetIfExists(); + if (!mgr) { + return nullptr; + } + if (!mWindowListener) { + return nullptr; + } + auto* thisDevice = GetDevice(); + if (!thisDevice) { + return nullptr; + } + + auto* thisTrackSource = GetTrackSource(); + if (!thisTrackSource) { + return nullptr; + } + + // See PrepareDOMStream for how a gUM/gDM track is created. + RefPtr<MediaTrack> track; + MediaTrackGraph* mtg = thisTrackSource->mTrack->Graph(); + if (const auto source = thisDevice->GetMediaSource(); + source == dom::MediaSourceEnum::Microphone) { +#ifdef MOZ_WEBRTC + if (thisDevice->IsFake()) { + track = mtg->CreateSourceTrack(MediaSegment::AUDIO); + } else { + track = AudioProcessingTrack::Create(mtg); + track->Suspend(); // Microphone source resumes in SetTrack + } +#else + track = mtg->CreateSourceTrack(MediaSegment::AUDIO); +#endif + } else if (source == dom::MediaSourceEnum::Camera || + source == dom::MediaSourceEnum::Screen || + source == dom::MediaSourceEnum::Window || + source == dom::MediaSourceEnum::Browser) { + track = mtg->CreateSourceTrack(MediaSegment::VIDEO); + } + + if (!track) { + return nullptr; + } + + RefPtr device = thisDevice->Clone(); + auto listener = MakeRefPtr<DeviceListener>(); + auto trackSource = MakeRefPtr<LocalTrackSource>( + thisTrackSource->GetPrincipal(), thisTrackSource->mLabel, listener, + thisTrackSource->mSource, track, thisTrackSource->mPeerIdentity, + thisTrackSource->mTrackingId); + + LOG("DeviceListener %p registering clone", this); + mWindowListener->Register(listener); + LOG("DeviceListener %p activating clone", this); + mWindowListener->Activate(listener, device, trackSource, + /*aIsAllocated=*/false); + + listener->mDeviceState->mDeviceEnabled = mDeviceState->mDeviceEnabled; + listener->mDeviceState->mDeviceMuted = mDeviceState->mDeviceMuted; + listener->mDeviceState->mTrackEnabled = mDeviceState->mTrackEnabled; + listener->mDeviceState->mTrackEnabledTime = TimeStamp::Now(); + + // We have to do an async operation here, even though Clone() is sync. + // This is fine because JS will not be able to trigger any operation to run + // async on the media thread. + LOG("DeviceListener %p allocating clone device %p async", this, device.get()); + InvokeAsync( + mgr->mMediaThread, __func__, + [thisDevice = RefPtr(thisDevice), device, prefs = mgr->mPrefs, + windowId = mWindowListener->WindowID(), listener, + principal = GetPrincipalHandle(), track, + startDevice = !listener->mDeviceState->mDeviceMuted && + listener->mDeviceState->mDeviceEnabled] { + const char* outBadConstraint{}; + nsresult rv = device->Source()->Allocate( + thisDevice->Constraints(), prefs, windowId, &outBadConstraint); + LOG("Allocated clone device %p. rv=%s", device.get(), + GetStaticErrorName(rv)); + if (NS_FAILED(rv)) { + return GenericPromise::CreateAndReject( + rv, "DeviceListener::Clone failure #1"); + } + rv = listener->Initialize(principal, device, track, startDevice); + if (NS_SUCCEEDED(rv)) { + return GenericPromise::CreateAndResolve( + true, "DeviceListener::Clone success"); + } + return GenericPromise::CreateAndReject( + rv, "DeviceListener::Clone failure #2"); + }) + ->Then(GetMainThreadSerialEventTarget(), __func__, + [listener, device, + trackSource](GenericPromise::ResolveOrRejectValue&& aValue) { + if (aValue.IsReject()) { + // Allocating/initializing failed. Stopping the device listener + // will destroy the MediaStreamTrackSource's MediaTrack, which + // will make the MediaStreamTrack's mTrack MediaTrack auto-end + // due to lack of inputs. This makes the MediaStreamTrack's + // readyState transition to "ended" as expected. + LOG("Allocating clone device %p failed. Stopping.", + device.get()); + listener->Stop(); + return; + } + listener->mDeviceState->mAllocated = true; + if (listener->mDeviceState->mStopped) { + MediaManager::Dispatch(NS_NewRunnableFunction( + "DeviceListener::Clone::Stop", + [device = listener->mDeviceState->mDevice]() { + device->Stop(); + device->Deallocate(); + })); + } + }); + + return listener.forget(); +} + void DeviceListener::Stop() { MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread"); @@ -4386,10 +4586,12 @@ void DeviceListener::Stop() { mDeviceState->mTrackSource->Stop(); - MediaManager::Dispatch(NewTaskFrom([device = mDeviceState->mDevice]() { - device->Stop(); - device->Deallocate(); - })); + if (mDeviceState->mAllocated) { + MediaManager::Dispatch(NewTaskFrom([device = mDeviceState->mDevice]() { + device->Stop(); + device->Deallocate(); + })); + } mWindowListener->ChromeAffectingStateChanged(); } diff --git a/dom/media/MediaManager.h b/dom/media/MediaManager.h @@ -148,6 +148,11 @@ class LocalMediaDevice final : public nsIMediaDevice { nsresult Stop(); nsresult Deallocate(); + /** + * Clones the LocalMediaDevice and sets a cloned source. + */ + already_AddRefed<LocalMediaDevice> Clone() const; + void GetSettings(dom::MediaTrackSettings& aOutSettings); void GetCapabilities(dom::MediaTrackCapabilities& aOutCapabilities); MediaEngineSource* Source(); @@ -162,6 +167,7 @@ class LocalMediaDevice final : public nsIMediaDevice { dom::MediaDeviceKind Kind() const { return mRawDevice->mKind; } bool IsFake() const { return mRawDevice->mIsFake; } const nsString& RawID() { return mRawDevice->mRawID; } + const dom::MediaTrackConstraints& Constraints() const; private: virtual ~LocalMediaDevice() = default; @@ -184,6 +190,8 @@ class LocalMediaDevice final : public nsIMediaDevice { private: RefPtr<MediaEngineSource> mSource; + // Currently applied constraints. Media thread only. + dom::MediaTrackConstraints mConstraints; }; typedef nsRefPtrHashtable<nsUint64HashKey, GetUserMediaWindowListener> diff --git a/dom/media/MediaStreamTrack.cpp b/dom/media/MediaStreamTrack.cpp @@ -46,6 +46,8 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaStreamTrackSource) NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrincipal) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +auto MediaStreamTrackSource::Clone() -> CloneResult { return {}; } + auto MediaStreamTrackSource::ApplyConstraints( const dom::MediaTrackConstraints& aConstraints, CallerType aCallerType) -> RefPtr<ApplyConstraintsPromise> { @@ -539,13 +541,6 @@ void MediaStreamTrack::RemoveConsumer(MediaStreamTrackConsumer* aConsumer) { } } -already_AddRefed<MediaStreamTrack> MediaStreamTrack::Clone() { - RefPtr<MediaStreamTrack> newTrack = CloneInternal(); - newTrack->SetEnabled(Enabled()); - newTrack->SetMuted(Muted()); - return newTrack.forget(); -} - void MediaStreamTrack::SetReadyState(MediaStreamTrackState aState) { MOZ_ASSERT(!(mReadyState == MediaStreamTrackState::Ended && aState == MediaStreamTrackState::Live), diff --git a/dom/media/MediaStreamTrack.h b/dom/media/MediaStreamTrack.h @@ -128,6 +128,18 @@ class MediaStreamTrackSource : public nsISupports { */ virtual void Destroy() {} + struct CloneResult { + RefPtr<MediaStreamTrackSource> mSource; + RefPtr<mozilla::MediaTrack> mInputTrack; + }; + + /** + * Clone this MediaStreamTrackSource. Cloned sources allow independent track + * settings. Not supported by all source types. A source not supporting + * cloning returns nullptr. + */ + virtual CloneResult Clone(); + /** * Gets the source's MediaSourceEnum for usage by PeerConnections. */ @@ -469,7 +481,7 @@ class MediaStreamTrack : public DOMEventTargetHelper, public SupportsWeakPtr { already_AddRefed<Promise> ApplyConstraints( const dom::MediaTrackConstraints& aConstraints, CallerType aCallerType, ErrorResult& aRv); - already_AddRefed<MediaStreamTrack> Clone(); + virtual already_AddRefed<MediaStreamTrack> Clone() = 0; MediaStreamTrackState ReadyState() { return mReadyState; } IMPL_EVENT_HANDLER(mute) @@ -642,7 +654,22 @@ class MediaStreamTrack : public DOMEventTargetHelper, public SupportsWeakPtr { * Creates a new MediaStreamTrack with the same kind, input track, input * track ID and source as this MediaStreamTrack. */ - virtual already_AddRefed<MediaStreamTrack> CloneInternal() = 0; + + template <typename TrackType> + already_AddRefed<MediaStreamTrack> CloneInternal() { + auto cloneRes = mSource->Clone(); + MOZ_ASSERT(!!cloneRes.mSource == !!cloneRes.mInputTrack); + if (!cloneRes.mSource || !cloneRes.mInputTrack) { + cloneRes.mSource = mSource; + cloneRes.mInputTrack = mInputTrack; + } + auto newTrack = + MakeRefPtr<TrackType>(mWindow, cloneRes.mInputTrack, cloneRes.mSource, + ReadyState(), Muted(), mConstraints); + newTrack->SetEnabled(Enabled()); + newTrack->SetMuted(Muted()); + return newTrack.forget(); + } nsTArray<PrincipalChangeObserver<MediaStreamTrack>*> mPrincipalChangeObservers; diff --git a/dom/media/VideoStreamTrack.cpp b/dom/media/VideoStreamTrack.cpp @@ -74,9 +74,8 @@ void VideoStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType) { MediaStreamTrack::GetLabel(aLabel, aCallerType); } -already_AddRefed<MediaStreamTrack> VideoStreamTrack::CloneInternal() { - return do_AddRef(new VideoStreamTrack(mWindow, mInputTrack, mSource, - ReadyState(), Muted(), mConstraints)); +already_AddRefed<MediaStreamTrack> VideoStreamTrack::Clone() { + return MediaStreamTrack::CloneInternal<VideoStreamTrack>(); } } // namespace mozilla::dom diff --git a/dom/media/VideoStreamTrack.h b/dom/media/VideoStreamTrack.h @@ -25,6 +25,8 @@ class VideoStreamTrack : public MediaStreamTrack { bool aMuted = false, const MediaTrackConstraints& aConstraints = MediaTrackConstraints()); + already_AddRefed<MediaStreamTrack> Clone() override; + void Destroy() override; VideoStreamTrack* AsVideoStreamTrack() override { return this; } @@ -45,9 +47,6 @@ class VideoStreamTrack : public MediaStreamTrack { void GetLabel(nsAString& aLabel, CallerType aCallerType) override; - protected: - already_AddRefed<MediaStreamTrack> CloneInternal() override; - private: nsTArray<RefPtr<VideoOutput>> mVideoOutputs; }; diff --git a/dom/media/gtest/TestAudioDecoderInputTrack.cpp b/dom/media/gtest/TestAudioDecoderInputTrack.cpp @@ -67,8 +67,6 @@ class MockTestGraph : public MediaTrackGraphImpl { protected: ~MockDriver() = default; }; - - bool mEnableFakeAppend = false; }; AudioData* CreateAudioDataFromInfo(uint32_t aFrames, const AudioInfo& aInfo) { @@ -394,30 +392,21 @@ TEST_F(TestAudioDecoderInputTrack, OutputAndEndEvent) { // Append an audio and EOS, the output event should notify the amount of // frames that is equal to the amount of audio we appended. RefPtr<AudioData> audio = CreateAudioData(10); - MozPromiseHolder<GenericPromise> holder; - RefPtr<GenericPromise> p = holder.Ensure(__func__); - MediaEventListener outputListener = - mTrack->OnOutput().Connect(NS_GetCurrentThread(), [&](TrackTime aFrame) { - EXPECT_EQ(aFrame, audio->Frames()); - holder.Resolve(true, __func__); - }); + auto outputPromise = TakeN(mTrack->OnOutput(), 1); mTrack->AppendData(audio, nullptr); mTrack->NotifyEndOfStream(); TrackTime start = 0; TrackTime end = 10; mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END); - (void)WaitFor(p); + auto output = WaitFor(outputPromise).unwrap()[0]; + EXPECT_EQ(std::get<int64_t>(output), audio->Frames()); // Track should end in this iteration, so the end event should be notified. - p = holder.Ensure(__func__); - MediaEventListener endListener = mTrack->OnEnd().Connect( - NS_GetCurrentThread(), [&]() { holder.Resolve(true, __func__); }); + auto endPromise = TakeN(mTrack->OnEnd(), 1); start = end; end += 10; mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END); - (void)WaitFor(p); - outputListener.Disconnect(); - endListener.Disconnect(); + (void)WaitFor(endPromise); } TEST_F(TestAudioDecoderInputTrack, PlaybackRateChange) { @@ -445,10 +434,13 @@ TEST_F(TestAudioDecoderInputTrack, PlaybackRateChange) { mTrack->NotifyEndOfStream(); // Playback rate is 2x, so we should only get 1/2x sample frames, another 1/2 - // should be silence. + // should be silence. Output should not be rate-aware. + auto outputPromise = TakeN(mTrack->OnOutput(), 1); TrackTime start = 0; TrackTime end = audio->Frames(); mTrack->ProcessInput(start, end, kNoFlags); + auto output = WaitFor(outputPromise).unwrap()[0]; + EXPECT_EQ(std::get<int64_t>(output), audio->Frames()); EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, audio->Frames() / 2); EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start + audio->Frames() / 2, end); } diff --git a/dom/media/gtest/TestDecodedStream.cpp b/dom/media/gtest/TestDecodedStream.cpp @@ -0,0 +1,382 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#include "BlankDecoderModule.h" +#include "DecodedStream.h" +#include "ImageContainer.h" +#include "MediaData.h" +#include "MediaQueue.h" +#include "MediaTrackGraphImpl.h" +#include "MediaTrackListener.h" +#include "MockCubeb.h" +#include "VideoSegment.h" +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include "mozilla/gtest/WaitFor.h" +#include "nsJSEnvironment.h" + +using mozilla::layers::ImageContainer; +using mozilla::layers::ImageUsageType; +using mozilla::media::TimeUnit; +using testing::ElementsAre; +using testing::Test; + +namespace mozilla { +// Short-hand for DispatchToCurrentThread with a function. +#define DispatchFunction(f) \ + NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f)) + +enum MediaType { Audio = 1, Video = 2, AudioVideo = Audio | Video }; + +template <MediaType Type> +CopyableTArray<RefPtr<ProcessedMediaTrack>> CreateOutputTracks( + MediaTrackGraphImpl* aGraph) { + CopyableTArray<RefPtr<ProcessedMediaTrack>> outputTracks; + if constexpr (Type & Audio) { + outputTracks.AppendElement( + aGraph->CreateForwardedInputTrack(MediaSegment::AUDIO)); + } + if constexpr (Type & Video) { + outputTracks.AppendElement( + aGraph->CreateForwardedInputTrack(MediaSegment::VIDEO)); + } + return outputTracks; +} + +template <MediaType Type> +MediaInfo CreateMediaInfo() { + MediaInfo info; + info.mStartTime = TimeUnit::Zero(); + if constexpr (Type & Audio) { + info.EnableAudio(); + } + if constexpr (Type & Video) { + info.EnableVideo(); + } + return info; +} + +class OnFallbackListener : public MediaTrackListener { + const RefPtr<MediaTrack> mTrack; + Atomic<bool> mOnFallback{true}; + + public: + explicit OnFallbackListener(MediaTrack* aTrack) : mTrack(aTrack) {} + + void Reset() { mOnFallback = true; } + bool OnFallback() { return mOnFallback; } + + void NotifyOutput(MediaTrackGraph*, TrackTime) override { + if (auto* ad = + mTrack->GraphImpl()->CurrentDriver()->AsAudioCallbackDriver()) { + mOnFallback = ad->OnFallback(); + } + } +}; + +template <typename Segment> +class CapturingListener : public MediaTrackListener { + public: + Segment mSegment; + + void NotifyQueuedChanges(MediaTrackGraph* aGraph, TrackTime aTrackOffset, + const MediaSegment& aQueuedMedia) { + mSegment.AppendSlice(aQueuedMedia, 0, aQueuedMedia.GetDuration()); + } +}; + +class TestableDecodedStream : public DecodedStream { + public: + TestableDecodedStream( + AbstractThread* aOwnerThread, + nsMainThreadPtrHandle<SharedDummyTrack> aDummyTrack, + CopyableTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks, + AbstractCanonical<PrincipalHandle>* aCanonicalOutputPrincipal, + double aVolume, double aPlaybackRate, bool aPreservesPitch, + MediaQueue<AudioData>& aAudioQueue, MediaQueue<VideoData>& aVideoQueue) + : DecodedStream(aOwnerThread, std::move(aDummyTrack), + std::move(aOutputTracks), aCanonicalOutputPrincipal, + aVolume, aPlaybackRate, aPreservesPitch, aAudioQueue, + aVideoQueue) {} + + using DecodedStream::GetPositionImpl; + using DecodedStream::LastOutputSystemTime; + using DecodedStream::LastVideoTimeStamp; +}; + +template <MediaType Type> +class TestDecodedStream : public Test { + public: + static constexpr TrackRate kRate = 48000; + static constexpr uint32_t kChannels = 2; + const RefPtr<MockCubeb> mMockCubeb; + RefPtr<SmartMockCubebStream> mMockCubebStream; + MediaQueue<AudioData> mAudioQueue; + MediaQueue<VideoData> mVideoQueue; + RefPtr<MediaTrackGraphImpl> mGraph; + nsMainThreadPtrHandle<SharedDummyTrack> mDummyTrack; + CopyableTArray<RefPtr<ProcessedMediaTrack>> mOutputTracks; + Canonical<PrincipalHandle> mCanonicalOutputPrincipal; + RefPtr<TestableDecodedStream> mDecodedStream; + + TestDecodedStream() + : mMockCubeb(MakeRefPtr<MockCubeb>(MockCubeb::RunningMode::Manual)), + mGraph(MediaTrackGraphImpl::GetInstance( + MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1, kRate, + nullptr, GetMainThreadSerialEventTarget())), + mDummyTrack(new nsMainThreadPtrHolder<SharedDummyTrack>( + __func__, new SharedDummyTrack( + mGraph->CreateSourceTrack(MediaSegment::AUDIO)))), + mOutputTracks(CreateOutputTracks<Type>(mGraph)), + mCanonicalOutputPrincipal( + AbstractThread::GetCurrent(), PRINCIPAL_HANDLE_NONE, + "TestDecodedStream::mCanonicalOutputPrincipal"), + mDecodedStream(MakeRefPtr<TestableDecodedStream>( + AbstractThread::GetCurrent(), mDummyTrack, mOutputTracks, + &mCanonicalOutputPrincipal, /* aVolume = */ 1.0, + /* aPlaybackRate = */ 1.0, + /* aPreservesPitch = */ true, mAudioQueue, mVideoQueue)) { + MOZ_ASSERT(NS_IsMainThread()); + }; + + void SetUp() override { + MOZ_ASSERT(NS_IsMainThread()); + CubebUtils::ForceSetCubebContext(mMockCubeb->AsCubebContext()); + + for (const auto& track : mOutputTracks) { + track->QueueSetAutoend(false); + } + + // Resume the dummy track because a suspended audio track will not use an + // AudioCallbackDriver. + mDummyTrack->mTrack->Resume(); + + RefPtr fallbackListener = new OnFallbackListener(mDummyTrack->mTrack); + mDummyTrack->mTrack->AddListener(fallbackListener); + + mMockCubebStream = WaitFor(mMockCubeb->StreamInitEvent()); + while (mMockCubebStream->State().isNothing()) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + ASSERT_EQ(*mMockCubebStream->State(), CUBEB_STATE_STARTED); + // Wait for the AudioCallbackDriver to come into effect. + while (fallbackListener->OnFallback()) { + ASSERT_EQ(mMockCubebStream->ManualDataCallback(1), + MockCubebStream::KeepProcessing::Yes); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + + void TearDown() override { + MOZ_ASSERT(NS_IsMainThread()); + // Destroy all tracks so they're removed from the graph. + mDecodedStream->Shutdown(); + for (const auto& t : mOutputTracks) { + t->Destroy(); + } + mDummyTrack = nullptr; + // DecodedStream also has a ref to the dummy track. + mDecodedStream = nullptr; + + // Wait for the graph to shutdown. If all tracks are indeed removed, it will + // not switch to another driver. + MockCubebStream::KeepProcessing keepProcessing{}; + while ((keepProcessing = mMockCubebStream->ManualDataCallback(0)) == + MockCubebStream::KeepProcessing::Yes) { + NS_ProcessPendingEvents(nullptr); + } + ASSERT_EQ(keepProcessing, MockCubebStream::KeepProcessing::No); + + // Process the final track removal and run the stable state runnable. + NS_ProcessPendingEvents(nullptr); + // Process the shutdown runnable. + NS_ProcessPendingEvents(nullptr); + + // Graph should be shut down. + ASSERT_TRUE(mGraph->OnGraphThreadOrNotRunning()) + << "Not on graph thread so graph must still be running!"; + ASSERT_EQ(mGraph->LifecycleStateRef(), + MediaTrackGraphImpl::LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN) + << "The graph should be in its final state. Note it does not advance " + "the state any further on thread shutdown."; + CubebUtils::ForceSetCubebContext(nullptr); + + // mGraph should be the last or second last reference to the graph. The last + // reference may be the JS-based shutdown blocker, which will eventually be + // destroyed by CC and GC. + MediaTrackGraphImpl* graph{}; + mGraph.forget(&graph); + int32_t refcnt = static_cast<int32_t>(graph->Release()); + EXPECT_LE(refcnt, 1); + + // Attempt to release the last reference to the graph, to avoid its lifetime + // reaching into future tests. + nsJSContext::CycleCollectNow(CCReason::API); + nsJSContext::GarbageCollectNow(JS::GCReason::API); + NS_ProcessPendingEvents(nullptr); + } + + MediaInfo CreateMediaInfo() { return mozilla::CreateMediaInfo<Type>(); } + + void TestVideoTimestampsWithPlaybackRate(double aPlaybackRate); +}; + +using TestDecodedStreamA = TestDecodedStream<Audio>; +using TestDecodedStreamV = TestDecodedStream<Video>; +using TestDecodedStreamAV = TestDecodedStream<AudioVideo>; + +TEST_F(TestDecodedStreamAV, StartStop) { + mDecodedStream->Start(TimeUnit::Zero(), CreateMediaInfo()); + mDecodedStream->SetPlaying(true); + mDecodedStream->Stop(); +} + +TEST_F(TestDecodedStreamA, LastOutputSystemTime) { + auto start = AwakeTimeStamp::Now(); + BlankAudioDataCreator creator(2, kRate); + auto raw = MakeRefPtr<MediaRawData>(); + raw->mDuration = TimeUnit(kRate, kRate); + mAudioQueue.Push(RefPtr(creator.Create(raw))->As<AudioData>()); + + mDecodedStream->Start(TimeUnit::Zero(), CreateMediaInfo()); + mDecodedStream->SetPlaying(true); + NS_ProcessPendingEvents(nullptr); + mMockCubebStream->ManualDataCallback(0); + + auto before = AwakeTimeStamp::Now(); + // This runs the events on the graph thread, sampling the system clock. + mMockCubebStream->ManualDataCallback(512); + auto after = AwakeTimeStamp::Now(); + // This runs the event handlers on the MDSM thread, updating the timestamps. + NS_ProcessPendingEvents(nullptr); + EXPECT_GE(mDecodedStream->LastOutputSystemTime() - start, before - start); + EXPECT_LE(mDecodedStream->LastOutputSystemTime() - start, after - start); + + mDecodedStream->Stop(); +} + +TEST_F(TestDecodedStreamA, InterpolatedPosition) { + BlankAudioDataCreator creator(2, kRate); + auto raw = MakeRefPtr<MediaRawData>(); + raw->mDuration = TimeUnit(kRate, kRate); + mAudioQueue.Push(RefPtr(creator.Create(raw))->As<AudioData>()); + + mDecodedStream->Start(TimeUnit::Zero(), CreateMediaInfo()); + mDecodedStream->SetPlaying(true); + NS_ProcessPendingEvents(nullptr); + mMockCubebStream->ManualDataCallback(0); + + auto now = TimeStamp::Now(); + auto awakeNow = AwakeTimeStamp::Now(); + TimeStamp outNow; + TimeUnit pos = mDecodedStream->GetPositionImpl(now, awakeNow, &outNow); + EXPECT_EQ(now, outNow); + EXPECT_EQ(pos, TimeUnit::Zero()) << pos.ToMilliseconds(); + + mMockCubebStream->ManualDataCallback(512); + NS_ProcessPendingEvents(nullptr); + + now += TimeDuration::FromSeconds( + (mDecodedStream->LastOutputSystemTime() - awakeNow).ToSeconds()); + awakeNow = mDecodedStream->LastOutputSystemTime(); + pos = mDecodedStream->GetPositionImpl(now, awakeNow); + EXPECT_EQ(pos.ToMicroseconds(), TimeUnit(512, kRate).ToMicroseconds()); + + // Check that the position is interpolated based on wall clock time since last + // output notification. + now += TimeDuration::FromSeconds( + (mDecodedStream->LastOutputSystemTime() - awakeNow).ToSeconds()) + + TimeDuration::FromMilliseconds(10); + awakeNow = mDecodedStream->LastOutputSystemTime() + + AwakeTimeDuration::FromMilliseconds(10); + pos = mDecodedStream->GetPositionImpl(now, awakeNow); + EXPECT_EQ(pos.ToMicroseconds(), + (TimeUnit(512, kRate) + TimeUnit(10, 1000)).ToMicroseconds()); + + mDecodedStream->Stop(); +} + +template <MediaType Type> +void TestDecodedStream<Type>::TestVideoTimestampsWithPlaybackRate( + double aPlaybackRate) { + static_assert(Type == MediaType::Video); + + auto imageContainer = MakeRefPtr<ImageContainer>(ImageUsageType::Webrtc, + ImageContainer::SYNCHRONOUS); + // Capture the output into a dedicated segment, that the graph will not prune + // like it will for the output track's mSegment. + RefPtr capturingListener = new CapturingListener<VideoSegment>(); + mOutputTracks[0]->AddListener(capturingListener); + VideoSegment* segment = &capturingListener->mSegment; + + { + // Add 4 video frames a 100ms each. Later we'll check timestamps of 3. We + // add 4 here to make the 3rd frames duration deterministic. + BlankVideoDataCreator creator(640, 480, imageContainer); + TimeUnit t = TimeUnit::Zero(); + for (size_t i = 0; i < 4; ++i) { + constexpr TimeUnit kDuration = TimeUnit(kRate / 10, kRate); + auto raw = MakeRefPtr<MediaRawData>(); + raw->mTime = t; + raw->mDuration = kDuration; + t += kDuration; + mVideoQueue.Push(RefPtr(creator.Create(raw))->template As<VideoData>()); + } + } + + mDecodedStream->SetPlaybackRate(aPlaybackRate); + mDecodedStream->Start(TimeUnit::Zero(), CreateMediaInfo()); + mDecodedStream->SetPlaying(true); + NS_ProcessPendingEvents(nullptr); + mMockCubebStream->ManualDataCallback(0); + + // Advance time enough to extract all 3 video frames. + long duration = 0; + while (duration < static_cast<long>((static_cast<double>(kRate) / 10) * 3 / + aPlaybackRate)) { + constexpr long kChunk = 512; + mMockCubebStream->ManualDataCallback(kChunk); + NS_ProcessPendingEvents(nullptr); + duration += kChunk; + } + EXPECT_EQ(segment->GetDuration(), duration); + + // Calculate the expected timestamp of the first frame. At this point all + // frames in the VideoQueue have been sent, so LastVideoTimeStamp() matches + // the timestamp of frame 4. + const auto frameGap = + TimeDuration::FromMilliseconds(100).MultDouble(1 / aPlaybackRate); + TimeStamp videoStartOffset = + mDecodedStream->LastVideoTimeStamp() - frameGap * 3; + + // Check durations of the first 3 frames. + AutoTArray<TrackTime, 3> durations; + AutoTArray<TimeDuration, 3> timestamps; + for (VideoSegment::ConstChunkIterator i(*segment); + durations.Length() < 3 && !i.IsEnded(); i.Next()) { + durations.AppendElement(i->GetDuration()); + timestamps.AppendElement(i->mTimeStamp - videoStartOffset); + } + const TrackTime d = + static_cast<TrackTime>(static_cast<double>(kRate) / 10 / aPlaybackRate); + EXPECT_THAT(durations, ElementsAre(d, d, d)); + EXPECT_THAT(timestamps, + ElementsAre(frameGap * 0, frameGap * 1, frameGap * 2)); + + mOutputTracks[0]->RemoveListener(capturingListener); + mDecodedStream->Stop(); +} + +TEST_F(TestDecodedStreamV, VideoTimeStamps) { + TestVideoTimestampsWithPlaybackRate(1.0); +} +TEST_F(TestDecodedStreamV, VideoTimeStampsFaster) { + TestVideoTimestampsWithPlaybackRate(2.0); +} +TEST_F(TestDecodedStreamV, VideoTimeStampsSlower) { + TestVideoTimestampsWithPlaybackRate(0.5); +} +} // namespace mozilla diff --git a/dom/media/gtest/moz.build b/dom/media/gtest/moz.build @@ -40,6 +40,7 @@ UNIFIED_SOURCES += [ "TestBufferReader.cpp", "TestCubebInputStream.cpp", "TestDataMutex.cpp", + "TestDecodedStream.cpp", "TestDeviceInputTrack.cpp", "TestDriftCompensation.cpp", "TestGMPUtils.cpp", diff --git a/dom/media/mediasink/AudioDecoderInputTrack.cpp b/dom/media/mediasink/AudioDecoderInputTrack.cpp @@ -597,7 +597,7 @@ void AudioDecoderInputTrack::NotifyInTheEndOfProcessInput( LOG("Notify, fill=%" PRId64 ", total written=%" PRId64 ", ended=%d", aFillDuration, mWrittenFrames, Ended()); if (aFillDuration > 0) { - mOnOutput.Notify(mWrittenFrames); + mOnOutput.Notify(mWrittenFrames, TimeStamp::Now(), AwakeTimeStamp::Now()); } if (Ended()) { mOnEnd.Notify(); diff --git a/dom/media/mediasink/AudioDecoderInputTrack.h b/dom/media/mediasink/AudioDecoderInputTrack.h @@ -12,7 +12,6 @@ #include "MediaTrackGraph.h" #include "TimeUnits.h" #include "mozilla/SPSCQueue.h" -#include "mozilla/StateMirroring.h" #include "mozilla/TimeStamp.h" #include "nsISerialEventTarget.h" @@ -101,7 +100,9 @@ class AudioDecoderInputTrack final : public ProcessedMediaTrack { void Close(); bool HasBatchedData() const; - MediaEventSource<int64_t>& OnOutput() { return mOnOutput; } + MediaEventSource<int64_t, TimeStamp, AwakeTimeStamp>& OnOutput() { + return mOnOutput; + } MediaEventSource<void>& OnEnd() { return mOnEnd; } // Graph Thread API @@ -176,8 +177,9 @@ class AudioDecoderInputTrack final : public ProcessedMediaTrack { const RefPtr<nsISerialEventTarget> mDecoderThread; - // Notify the amount of audio frames which have been sent to the track. - MediaEventProducer<int64_t> mOnOutput; + // Notify the amount of audio frames which have been sent to the track, + // sampled by the awake system time (and non-awake, for now) they were sent. + MediaEventProducer<int64_t, TimeStamp, AwakeTimeStamp> mOnOutput; // Notify when the track is ended. MediaEventProducer<void> mOnEnd; diff --git a/dom/media/mediasink/DecodedStream.cpp b/dom/media/mediasink/DecodedStream.cpp @@ -7,20 +7,17 @@ #include "DecodedStream.h" #include "AudioDecoderInputTrack.h" -#include "AudioSegment.h" #include "MediaData.h" #include "MediaDecoderStateMachine.h" #include "MediaQueue.h" #include "MediaTrackGraph.h" #include "MediaTrackListener.h" -#include "SharedBuffer.h" #include "Tracing.h" #include "VideoSegment.h" #include "VideoUtils.h" #include "mozilla/AbstractThread.h" #include "mozilla/CheckedInt.h" #include "mozilla/ProfilerLabels.h" -#include "mozilla/ProfilerMarkerTypes.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/SyncRunnable.h" #include "mozilla/gfx/Point.h" @@ -104,9 +101,11 @@ class DecodedStreamGraphListener { void RegisterListeners() { if (mAudioTrack) { mOnAudioOutput = mAudioTrack->OnOutput().Connect( - mDecoderThread, - [self = RefPtr<DecodedStreamGraphListener>(this)](TrackTime aTime) { - self->NotifyOutput(MediaSegment::AUDIO, aTime); + mDecoderThread, [self = RefPtr<DecodedStreamGraphListener>(this)]( + TrackTime aTime, TimeStamp aSystemTime, + AwakeTimeStamp aAwakeSystemTime) { + self->NotifyOutput(MediaSegment::AUDIO, aTime, aSystemTime, + aAwakeSystemTime); }); mOnAudioEnd = mAudioTrack->OnEnd().Connect( mDecoderThread, [self = RefPtr<DecodedStreamGraphListener>(this)]() { @@ -147,7 +146,8 @@ class DecodedStreamGraphListener { mOnAudioEnd.DisconnectIfExists(); } - void NotifyOutput(MediaSegment::Type aType, TrackTime aCurrentTrackTime) { + void NotifyOutput(MediaSegment::Type aType, TrackTime aCurrentTrackTime, + TimeStamp aSystemTime, AwakeTimeStamp aAwakeSystemTime) { AssertOnDecoderThread(); if (aType == MediaSegment::AUDIO) { mAudioOutputFrames = aCurrentTrackTime; @@ -181,7 +181,8 @@ class DecodedStreamGraphListener { const MediaTrack* track = aType == MediaSegment::VIDEO ? static_cast<MediaTrack*>(mVideoTrack) : static_cast<MediaTrack*>(mAudioTrack); - mOnOutput.Notify(track->TrackTimeToMicroseconds(aCurrentTrackTime)); + mOnOutput.Notify(track->TrackTimeToMicroseconds(aCurrentTrackTime), + aSystemTime, aAwakeSystemTime); } void NotifyEnded(MediaSegment::Type aType) { @@ -237,7 +238,9 @@ class DecodedStreamGraphListener { return mAudioOutputFrames; } - MediaEventSource<int64_t>& OnOutput() { return mOnOutput; } + MediaEventSource<int64_t, TimeStamp, AwakeTimeStamp>& OnOutput() { + return mOnOutput; + } private: ~DecodedStreamGraphListener() { @@ -252,7 +255,7 @@ class DecodedStreamGraphListener { const RefPtr<nsISerialEventTarget> mDecoderThread; // Accessible on any thread, but only notify on the decoder thread. - MediaEventProducer<int64_t> mOnOutput; + MediaEventProducer<int64_t, TimeStamp, AwakeTimeStamp> mOnOutput; RefPtr<SourceVideoTrackListener> mVideoTrackListener; @@ -299,9 +302,12 @@ void SourceVideoTrackListener::NotifyOutput(MediaTrackGraph* aGraph, mLastVideoOutputTime = aCurrentTrackTime; mDecoderThread->Dispatch(NS_NewRunnableFunction( "SourceVideoTrackListener::NotifyOutput", - [self = RefPtr<SourceVideoTrackListener>(this), aCurrentTrackTime]() { + [self = RefPtr<SourceVideoTrackListener>(this), aCurrentTrackTime, + systemTime = TimeStamp::Now(), + awakeSystemTime = AwakeTimeStamp::Now()]() { self->mGraphListener->NotifyOutput(MediaSegment::VIDEO, - aCurrentTrackTime); + aCurrentTrackTime, systemTime, + awakeSystemTime); })); } @@ -333,7 +339,7 @@ class DecodedStreamData final { float aPlaybackRate, float aVolume, bool aPreservesPitch, nsISerialEventTarget* aDecoderThread); ~DecodedStreamData(); - MediaEventSource<int64_t>& OnOutput(); + MediaEventSource<int64_t, TimeStamp, AwakeTimeStamp>& OnOutput(); // This is used to mark track as closed and should be called before Forget(). // Decoder thread only. void Close(); @@ -443,7 +449,8 @@ DecodedStreamData::~DecodedStreamData() { } } -MediaEventSource<int64_t>& DecodedStreamData::OnOutput() { +MediaEventSource<int64_t, TimeStamp, AwakeTimeStamp>& +DecodedStreamData::OnOutput() { return mListener->OnOutput(); } @@ -467,19 +474,20 @@ void DecodedStreamData::GetDebugInfo(dom::DecodedStreamDataDebugInfo& aInfo) { } DecodedStream::DecodedStream( - MediaDecoderStateMachine* aStateMachine, + AbstractThread* aOwnerThread, nsMainThreadPtrHandle<SharedDummyTrack> aDummyTrack, - CopyableTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks, double aVolume, - double aPlaybackRate, bool aPreservesPitch, - MediaQueue<AudioData>& aAudioQueue, MediaQueue<VideoData>& aVideoQueue, - RefPtr<AudioDeviceInfo> aAudioDevice) - : mOwnerThread(aStateMachine->OwnerThread()), + CopyableTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks, + AbstractCanonical<PrincipalHandle>* aCanonicalOutputPrincipal, + double aVolume, double aPlaybackRate, bool aPreservesPitch, + MediaQueue<AudioData>& aAudioQueue, MediaQueue<VideoData>& aVideoQueue) + : mOwnerThread(aOwnerThread), mDummyTrack(std::move(aDummyTrack)), + mWatchManager(this, mOwnerThread), mPlaying(false, "DecodedStream::mPlaying"), - mPrincipalHandle(aStateMachine->OwnerThread(), PRINCIPAL_HANDLE_NONE, + mPrincipalHandle(aOwnerThread, PRINCIPAL_HANDLE_NONE, "DecodedStream::mPrincipalHandle (Mirror)"), - mCanonicalOutputPrincipal(aStateMachine->CanonicalOutputPrincipal()), + mCanonicalOutputPrincipal(aCanonicalOutputPrincipal), mOutputTracks(std::move(aOutputTracks)), mVolume(aVolume), mPlaybackRate(aPlaybackRate), @@ -508,6 +516,7 @@ nsresult DecodedStream::Start(const TimeUnit& aStartTime, const MediaInfo& aInfo) { AssertOwnerThread(); MOZ_ASSERT(mStartTime.isNothing(), "playback already started."); + MOZ_ASSERT(mLastReportedPosition.isNothing()); AUTO_PROFILER_LABEL(FUNCTION_SIGNATURE, MEDIA_PLAYBACK); if (profiler_thread_is_being_profiled_for_markers()) { @@ -520,6 +529,7 @@ nsresult DecodedStream::Start(const TimeUnit& aStartTime, mStartTime.emplace(aStartTime); mLastOutputTime = TimeUnit::Zero(); + mLastOutputSystemTime = Nothing(); mInfo = aInfo; mPlaying = true; mPrincipalHandle.Connect(mCanonicalOutputPrincipal); @@ -635,6 +645,7 @@ void DecodedStream::Stop() { ResetVideo(mPrincipalHandle); ResetAudio(); mStartTime.reset(); + mLastReportedPosition = Nothing(); mAudioEndedPromise = nullptr; mVideoEndedPromise = nullptr; @@ -983,9 +994,10 @@ void DecodedStream::SendVideo(const PrincipalHandle& aPrincipalHandle) { // video frame). E.g. if we have a video frame that is 30 sec long // and capture happens at 15 sec, we'll have to append a black frame // that is 15 sec long. - TimeStamp t = - std::max(mData->mLastVideoTimeStamp, - currentTime + (lastEnd - currentPosition).ToTimeDuration()); + TimeStamp t = std::max(mData->mLastVideoTimeStamp, + currentTime + (lastEnd - currentPosition) + .ToTimeDuration() + .MultDouble(1 / mPlaybackRate)); mData->WriteVideoToSegment(mData->mLastVideoImage, lastEnd, v->mTime, mData->mLastVideoImageDisplaySize, t, &output, aPrincipalHandle, mPlaybackRate); @@ -997,9 +1009,10 @@ void DecodedStream::SendVideo(const PrincipalHandle& aPrincipalHandle) { // before the last frame's end time for some videos. This only matters for // the track's lifetime in the MTG, as rendering is based on timestamps, // aka frame start times. - TimeStamp t = - std::max(mData->mLastVideoTimeStamp, - currentTime + (lastEnd - currentPosition).ToTimeDuration()); + TimeStamp t = std::max(mData->mLastVideoTimeStamp, + currentTime + (lastEnd - currentPosition) + .ToTimeDuration() + .MultDouble(1 / mPlaybackRate)); TimeUnit end = std::max( v->GetEndTime(), lastEnd + TimeUnit::FromMicroseconds( @@ -1100,16 +1113,47 @@ TimeUnit DecodedStream::GetEndTime(TrackType aType) const { TimeUnit DecodedStream::GetPosition(TimeStamp* aTimeStamp) { AssertOwnerThread(); TRACE("DecodedStream::GetPosition"); + return GetPositionImpl(TimeStamp::Now(), AwakeTimeStamp::Now(), aTimeStamp); +} + +TimeUnit DecodedStream::GetPositionImpl(TimeStamp aNow, + AwakeTimeStamp aAwakeNow, + TimeStamp* aTimeStamp) { + AssertOwnerThread(); // This is only called after MDSM starts playback. So mStartTime is // guaranteed to be something. MOZ_ASSERT(mStartTime.isSome()); if (aTimeStamp) { - *aTimeStamp = TimeStamp::Now(); - } - return mStartTime.ref() + mLastOutputTime; + *aTimeStamp = aNow; + } + AwakeTimeDuration timeSinceLastOutput; + if (mLastOutputSystemTime) { + MOZ_ASSERT(aAwakeNow >= *mLastOutputSystemTime); + timeSinceLastOutput = aAwakeNow - *mLastOutputSystemTime; + } + TimeUnit position = mStartTime.ref() + mLastOutputTime + + TimeUnit::FromSeconds(timeSinceLastOutput.ToSeconds()); + if (mLastReportedPosition && position < *mLastReportedPosition) { + // There's a theoretical risk of time going backwards because of the + // interpolation based on mLastOutputSystemTime. Prevent that here. + position = *mLastReportedPosition; + } + mLastReportedPosition = Some(position); + return position; +} + +AwakeTimeStamp DecodedStream::LastOutputSystemTime() const { + AssertOwnerThread(); + return *mLastOutputSystemTime; +} + +TimeStamp DecodedStream::LastVideoTimeStamp() const { + AssertOwnerThread(); + return mData->mLastVideoTimeStamp; } -void DecodedStream::NotifyOutput(int64_t aTime) { +void DecodedStream::NotifyOutput(int64_t aTime, TimeStamp aSystemTime, + AwakeTimeStamp aAwakeSystemTime) { AssertOwnerThread(); TimeUnit time = TimeUnit::FromMicroseconds(aTime); if (time == mLastOutputTime) { @@ -1117,7 +1161,10 @@ void DecodedStream::NotifyOutput(int64_t aTime) { } MOZ_ASSERT(mLastOutputTime < time); mLastOutputTime = time; - auto currentTime = GetPosition(); + MOZ_ASSERT_IF(mLastOutputSystemTime, + *mLastOutputSystemTime < aAwakeSystemTime); + mLastOutputSystemTime = Some(aAwakeSystemTime); + auto currentTime = GetPositionImpl(aSystemTime, aAwakeSystemTime); if (profiler_thread_is_being_profiled_for_markers()) { nsPrintfCString markerString("OutputTime=%" PRId64, @@ -1179,6 +1226,10 @@ void DecodedStream::GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) { NS_ConvertUTF8toUTF16(nsPrintfCString("%p", this)); aInfo.mDecodedStream.mStartTime = startTime; aInfo.mDecodedStream.mLastOutputTime = mLastOutputTime.ToMicroseconds(); + aInfo.mDecodedStream.mLastReportedPosition = + mLastReportedPosition + .map([](const auto& aT) { return aT.ToMicroseconds(); }) + .valueOr(0); aInfo.mDecodedStream.mPlaying = mPlaying.Ref(); auto lastAudio = mAudioQueue.PeekBack(); aInfo.mDecodedStream.mLastAudio = diff --git a/dom/media/mediasink/DecodedStream.h b/dom/media/mediasink/DecodedStream.h @@ -10,13 +10,14 @@ #include "AudibilityMonitor.h" #include "MediaEventSource.h" #include "MediaInfo.h" -#include "MediaSegment.h" #include "MediaSink.h" #include "mozilla/AbstractThread.h" +#include "mozilla/AwakeTimeStamp.h" #include "mozilla/Maybe.h" #include "mozilla/MozPromise.h" #include "mozilla/RefPtr.h" #include "mozilla/StateMirroring.h" +#include "mozilla/TimeStamp.h" #include "mozilla/UniquePtr.h" namespace mozilla { @@ -28,20 +29,19 @@ class VideoData; struct PlaybackInfoInit; class ProcessedMediaTrack; struct SharedDummyTrack; -class TimeStamp; template <class T> class MediaQueue; class DecodedStream : public MediaSink { public: - DecodedStream(MediaDecoderStateMachine* aStateMachine, + DecodedStream(AbstractThread* aOwnerThread, nsMainThreadPtrHandle<SharedDummyTrack> aDummyTrack, CopyableTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks, + AbstractCanonical<PrincipalHandle>* aCanonicalOutputPrincipal, double aVolume, double aPlaybackRate, bool aPreservesPitch, MediaQueue<AudioData>& aAudioQueue, - MediaQueue<VideoData>& aVideoQueue, - RefPtr<AudioDeviceInfo> aAudioDevice); + MediaQueue<VideoData>& aVideoQueue); RefPtr<EndedPromise> OnEnded(TrackType aType) override; media::TimeUnit GetEndTime(TrackType aType) const override; @@ -78,6 +78,12 @@ class DecodedStream : public MediaSink { protected: virtual ~DecodedStream(); + // A bit many clocks to sample, but what do you do... + media::TimeUnit GetPositionImpl(TimeStamp aNow, AwakeTimeStamp aAwakeNow, + TimeStamp* aTimeStamp = nullptr); + AwakeTimeStamp LastOutputSystemTime() const; + TimeStamp LastVideoTimeStamp() const; + private: void DestroyData(UniquePtr<DecodedStreamData>&& aData); void SendAudio(const PrincipalHandle& aPrincipalHandle); @@ -85,7 +91,8 @@ class DecodedStream : public MediaSink { void ResetAudio(); void ResetVideo(const PrincipalHandle& aPrincipalHandle); void SendData(); - void NotifyOutput(int64_t aTime); + void NotifyOutput(int64_t aTime, TimeStamp aSystemTime, + AwakeTimeStamp aAwakeSystemTime); void CheckIsDataAudible(const AudioData* aData); void AssertOwnerThread() const { @@ -128,6 +135,8 @@ class DecodedStream : public MediaSink { media::NullableTimeUnit mStartTime; media::TimeUnit mLastOutputTime; + Maybe<AwakeTimeStamp> mLastOutputSystemTime; + Maybe<media::TimeUnit> mLastReportedPosition; MediaInfo mInfo; // True when stream is producing audible sound, false when stream is silent. bool mIsAudioDataAudible = false; diff --git a/dom/media/systemservices/CamerasChild.cpp b/dom/media/systemservices/CamerasChild.cpp @@ -141,35 +141,42 @@ mozilla::ipc::IPCResult CamerasChild::RecvReplyNumberOfCapabilities( template <class T = int> class LockAndDispatch { public: + using Result = CamerasChild::DispatchToParentResult; + LockAndDispatch(CamerasChild* aCamerasChild, const char* aRequestingFunc, - nsIRunnable* aRunnable, T aFailureValue, + nsIRunnable* aRunnable, T aFailureValue, T aIPCFailureValue, const T& aSuccessValue) : mCamerasChild(aCamerasChild), mRequestingFunc(aRequestingFunc), mRunnable(aRunnable), mReplyLock(aCamerasChild->mReplyMonitor), mRequestLock(aCamerasChild->mRequestMutex), - mSuccess(true), + mStatus(Result::SUCCESS), mFailureValue(aFailureValue), + mIPCFailureValue(aIPCFailureValue), mSuccessValue(aSuccessValue) { Dispatch(); } T ReturnValue() const { - if (mSuccess) { + if (mStatus == Result::SUCCESS) { return mSuccessValue; - } else { + } + if (mStatus == Result::FAILURE) { return mFailureValue; } + MOZ_ASSERT(mStatus == Result::DISCONNECTED); + return mIPCFailureValue; } - const bool& Success() const { return mSuccess; } + bool Success() const { return mStatus == Result::SUCCESS; } + bool Disconnected() const { return mStatus == Result::DISCONNECTED; } private: void Dispatch() { - if (!mCamerasChild->DispatchToParent(mRunnable, mReplyLock)) { + mStatus = mCamerasChild->DispatchToParent(mRunnable, mReplyLock); + if (mStatus != Result::SUCCESS) { LOG(("Cameras dispatch for IPC failed in %s", mRequestingFunc)); - mSuccess = false; } } @@ -181,13 +188,15 @@ class LockAndDispatch { // the reply to be filled in, necessitating the additional mRequestLock/Mutex; MonitorAutoLock mReplyLock; MutexAutoLock mRequestLock; - bool mSuccess; + CamerasChild::DispatchToParentResult mStatus; const T mFailureValue; + const T mIPCFailureValue; const T& mSuccessValue; }; -bool CamerasChild::DispatchToParent(nsIRunnable* aRunnable, - MonitorAutoLock& aMonitor) { +auto CamerasChild::DispatchToParent(nsIRunnable* aRunnable, + MonitorAutoLock& aMonitor) + -> DispatchToParentResult { CamerasSingleton::Mutex().AssertCurrentThreadOwns(); mReplyMonitor.AssertCurrentThreadOwns(); CamerasSingleton::Thread()->Dispatch(aRunnable, NS_DISPATCH_NORMAL); @@ -197,11 +206,12 @@ bool CamerasChild::DispatchToParent(nsIRunnable* aRunnable, do { // If the parent has been shut down, then we won't receive a reply. if (!mIPCIsAlive) { - return false; + return DispatchToParentResult::DISCONNECTED; } aMonitor.Wait(); } while (!mReceivedReply); - return mReplySuccess; + return mReplySuccess ? DispatchToParentResult::SUCCESS + : DispatchToParentResult::FAILURE; } int CamerasChild::NumberOfCapabilities(CaptureEngine aCapEngine, @@ -213,7 +223,7 @@ int CamerasChild::NumberOfCapabilities(CaptureEngine aCapEngine, mozilla::NewRunnableMethod<CaptureEngine, nsCString>( "camera::PCamerasChild::SendNumberOfCapabilities", this, &CamerasChild::SendNumberOfCapabilities, aCapEngine, unique_id); - LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger); + LockAndDispatch<> dispatcher(this, __func__, runnable, 0, 0, mReplyInteger); LOG(("Capture capability count: %d", dispatcher.ReturnValue())); return dispatcher.ReturnValue(); } @@ -223,7 +233,7 @@ int CamerasChild::NumberOfCaptureDevices(CaptureEngine aCapEngine) { nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod<CaptureEngine>( "camera::PCamerasChild::SendNumberOfCaptureDevices", this, &CamerasChild::SendNumberOfCaptureDevices, aCapEngine); - LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger); + LockAndDispatch<> dispatcher(this, __func__, runnable, 0, 0, mReplyInteger); LOG(("Capture Devices: %d", dispatcher.ReturnValue())); return dispatcher.ReturnValue(); } @@ -244,8 +254,8 @@ int CamerasChild::EnsureInitialized(CaptureEngine aCapEngine) { nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod<CaptureEngine>( "camera::PCamerasChild::SendEnsureInitialized", this, &CamerasChild::SendEnsureInitialized, aCapEngine); - LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger); - LOG(("Capture Devices: %d", dispatcher.ReturnValue())); + LockAndDispatch<> dispatcher(this, __func__, runnable, 0, 0, mReplyInteger); + LOG(("Initialized: %d", dispatcher.ReturnValue())); return dispatcher.ReturnValue(); } @@ -262,7 +272,8 @@ int CamerasChild::GetCaptureCapability( &CamerasChild::SendGetCaptureCapability, aCapEngine, unique_id, capability_number); mReplyCapability = capability; - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + kSuccess); mReplyCapability = nullptr; return dispatcher.ReturnValue(); } @@ -292,7 +303,8 @@ int CamerasChild::GetCaptureDevice( mozilla::NewRunnableMethod<CaptureEngine, unsigned int>( "camera::PCamerasChild::SendGetCaptureDevice", this, &CamerasChild::SendGetCaptureDevice, aCapEngine, list_number); - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + kSuccess); if (dispatcher.Success()) { base::strlcpy(device_nameUTF8, mReplyDeviceName.get(), device_nameUTF8Length); @@ -328,7 +340,8 @@ int CamerasChild::AllocateCapture(CaptureEngine aCapEngine, mozilla::NewRunnableMethod<CaptureEngine, nsCString, uint64_t>( "camera::PCamerasChild::SendAllocateCapture", this, &CamerasChild::SendAllocateCapture, aCapEngine, unique_id, aWindowID); - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mReplyInteger); + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + mReplyInteger); if (dispatcher.Success()) { LOG(("Capture Device allocated: %d", mReplyInteger)); } @@ -353,7 +366,8 @@ int CamerasChild::ReleaseCapture(CaptureEngine aCapEngine, mozilla::NewRunnableMethod<CaptureEngine, int>( "camera::PCamerasChild::SendReleaseCapture", this, &CamerasChild::SendReleaseCapture, aCapEngine, capture_id); - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + kSuccess); return dispatcher.ReturnValue(); } @@ -401,7 +415,8 @@ int CamerasChild::StartCapture(CaptureEngine aCapEngine, const int capture_id, "camera::PCamerasChild::SendStartCapture", this, &CamerasChild::SendStartCapture, aCapEngine, capture_id, capCap, constraints, resize_mode); - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + kSuccess); return dispatcher.ReturnValue(); } @@ -412,7 +427,8 @@ int CamerasChild::FocusOnSelectedSource(CaptureEngine aCapEngine, mozilla::NewRunnableMethod<CaptureEngine, int>( "camera::PCamerasChild::SendFocusOnSelectedSource", this, &CamerasChild::SendFocusOnSelectedSource, aCapEngine, aCaptureId); - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + kSuccess); return dispatcher.ReturnValue(); } @@ -422,8 +438,9 @@ int CamerasChild::StopCapture(CaptureEngine aCapEngine, const int capture_id) { mozilla::NewRunnableMethod<CaptureEngine, int>( "camera::PCamerasChild::SendStopCapture", this, &CamerasChild::SendStopCapture, aCapEngine, capture_id); - LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero); - if (dispatcher.Success()) { + LockAndDispatch<> dispatcher(this, __func__, runnable, kError, kIpcError, + kSuccess); + if (dispatcher.Success() || dispatcher.Disconnected()) { RemoveCallback(capture_id); } return dispatcher.ReturnValue(); @@ -476,27 +493,32 @@ void Shutdown(void) { CamerasSingleton::Thread() = nullptr; } -mozilla::ipc::IPCResult CamerasChild::RecvCaptureEnded(const int& capId) { +mozilla::ipc::IPCResult CamerasChild::RecvCaptureEnded( + nsTArray<int>&& aCaptureIds) { MutexAutoLock lock(mCallbackMutex); - if (Callback(capId)) { - Callback(capId)->OnCaptureEnded(); - } else { - LOG(("CaptureEnded called with dead callback")); + for (int capId : aCaptureIds) { + if (auto* cb = Callback(capId)) { + cb->OnCaptureEnded(); + } else { + LOG(("CaptureEnded called with dead callback")); + } } return IPC_OK(); } mozilla::ipc::IPCResult CamerasChild::RecvDeliverFrame( - const int& capId, mozilla::ipc::Shmem&& shmem, - const VideoFrameProperties& prop) { + const int& aCaptureId, nsTArray<int>&& aStreamIds, + mozilla::ipc::Shmem&& aShmem, const VideoFrameProperties& aProps) { MutexAutoLock lock(mCallbackMutex); - if (Callback(capId)) { - unsigned char* image = shmem.get<unsigned char>(); - Callback(capId)->DeliverFrame(image, prop); - } else { - LOG(("DeliverFrame called with dead callback")); + for (const int& streamId : aStreamIds) { + if (auto* cb = Callback(streamId)) { + unsigned char* image = aShmem.get<unsigned char>(); + cb->DeliverFrame(image, aProps); + } else { + LOG(("DeliverFrame called with dead callback")); + } } - SendReleaseFrame(std::move(shmem)); + SendReleaseFrame(aCaptureId, std::move(aShmem)); return IPC_OK(); } @@ -521,7 +543,6 @@ CamerasChild::CamerasChild() mReplyMonitor("mozilla::cameras::CamerasChild::mReplyMonitor"), mReceivedReply(false), mReplySuccess(false), - mZero(0), mReplyInteger(0), mReplyScary(false) { LOG(("CamerasChild: %p", this)); diff --git a/dom/media/systemservices/CamerasChild.h b/dom/media/systemservices/CamerasChild.h @@ -45,6 +45,10 @@ class CamerasChild; template <class T> class LockAndDispatch; +static constexpr int kSuccess = 0; +static constexpr int kError = -1; +static constexpr int kIpcError = -2; + // We emulate the sync webrtc.org API with the help of singleton // CamerasSingleton, which manages a pointer to an IPC object, a thread // where IPC operations should run on, and a mutex. @@ -146,10 +150,12 @@ class CamerasChild final : public PCamerasChild { // IPC messages recevied, received on the PBackground thread // these are the actual callbacks with data - mozilla::ipc::IPCResult RecvCaptureEnded(const int&) override; + mozilla::ipc::IPCResult RecvCaptureEnded( + nsTArray<int>&& aCaptureIds) override; mozilla::ipc::IPCResult RecvDeliverFrame( - const int&, mozilla::ipc::Shmem&&, - const VideoFrameProperties& prop) override; + const int& aCaptureId, nsTArray<int>&& aStreamIds, + mozilla::ipc::Shmem&& aShmem, + const VideoFrameProperties& aProps) override; mozilla::ipc::IPCResult RecvDeviceChange() override; @@ -221,7 +227,13 @@ class CamerasChild final : public PCamerasChild { ~CamerasChild(); // Dispatch a Runnable to the PCamerasParent, by executing it on the // decidecated Cameras IPC/PBackground thread. - bool DispatchToParent(nsIRunnable* aRunnable, MonitorAutoLock& aMonitor); + enum class DispatchToParentResult : int8_t { + SUCCESS = 0, + FAILURE = -1, + DISCONNECTED = -2, + }; + DispatchToParentResult DispatchToParent(nsIRunnable* aRunnable, + MonitorAutoLock& aMonitor); void AddCallback(int capture_id, FrameRelay* render); void RemoveCallback(int capture_id); @@ -248,7 +260,6 @@ class CamerasChild final : public PCamerasChild { bool mReceivedReply; // Async responses data contents; bool mReplySuccess; - const int mZero; int mReplyInteger; webrtc::VideoCaptureCapability* mReplyCapability = nullptr; nsCString mReplyDeviceName; diff --git a/dom/media/systemservices/CamerasParent.cpp b/dom/media/systemservices/CamerasParent.cpp @@ -57,10 +57,6 @@ using dom::VideoResizeModeEnum; using media::ShutdownBlockingTicket; namespace camera { -MOZ_RUNINIT std::map<uint32_t, const char*> sDeviceUniqueIDs; -MOZ_RUNINIT std::map<uint32_t, webrtc::VideoCaptureCapability> - sAllRequestedCapabilities; - uint32_t ResolutionFeasibilityDistance(int32_t candidate, int32_t requested) { // The purpose of this function is to find a smallest resolution // which is larger than all requested capabilities. @@ -119,6 +115,12 @@ static StaticRefPtr<nsIThread> sVideoCaptureThread; // objects. Created on IPC background thread, destroyed on main thread on // shutdown. Outlives the CamerasParent instances. static StaticRefPtr<VideoCaptureFactory> sVideoCaptureFactory; +// All live capturers across all CamerasParent instances. The array and its +// members are only modified on the video capture thread. The outermost refcount +// is IPC background thread only. +static StaticRefPtr< + media::Refcountable<nsTArray<std::unique_ptr<AggregateCapturer>>>> + sCapturers; static void ClearCameraDeviceInfo() { ipc::AssertIsOnBackgroundThread(); @@ -196,6 +198,9 @@ MakeAndAddRefVideoCaptureThreadAndSingletons() { sEngines = MakeRefPtr<VideoEngineArray>(); sEngines->AppendElements(CaptureEngine::MaxEngine); + + sCapturers = MakeRefPtr< + media::Refcountable<nsTArray<std::unique_ptr<AggregateCapturer>>>>(); } ++sNumCamerasParents; @@ -214,8 +219,10 @@ static void ReleaseVideoCaptureThreadAndSingletons() { // No other CamerasParent instances alive. Clean up. LOG("Shutting down VideoEngines and the VideoCapture thread"); - MOZ_ALWAYS_SUCCEEDS(sVideoCaptureThread->Dispatch( - NS_NewRunnableFunction(__func__, [engines = RefPtr(sEngines.forget())] { + MOZ_ALWAYS_SUCCEEDS(sVideoCaptureThread->Dispatch(NS_NewRunnableFunction( + __func__, [engines = RefPtr(sEngines.forget()), + capturers = RefPtr(sCapturers.forget())] { + MOZ_ASSERT(capturers->IsEmpty(), "No capturers expected on shutdown"); for (RefPtr<VideoEngine>& engine : *engines) { if (engine) { VideoEngine::Delete(engine); @@ -251,43 +258,34 @@ void CamerasParent::OnDeviceChange() { class DeliverFrameRunnable : public mozilla::Runnable { public: + // Constructor for when no ShmemBuffer (of the right size) was available, so + // keep the frame around until we can allocate one on PBackground (in Run). DeliverFrameRunnable(CamerasParent* aParent, CaptureEngine aEngine, - uint32_t aStreamId, const TrackingId& aTrackingId, + int aCaptureId, nsTArray<int>&& aStreamIds, + const TrackingId& aTrackingId, const webrtc::VideoFrame& aFrame, const VideoFrameProperties& aProperties) : Runnable("camera::DeliverFrameRunnable"), mParent(aParent), mCapEngine(aEngine), - mStreamId(aStreamId), + mCaptureId(aCaptureId), + mStreamIds(std::move(aStreamIds)), mTrackingId(aTrackingId), - mProperties(aProperties), - mResult(0) { - // No ShmemBuffer (of the right size) was available, so make an - // extra buffer here. We have no idea when we are going to run and - // it will be potentially long after the webrtc frame callback has - // returned, so the copy needs to be no later than here. - // We will need to copy this back into a Shmem later on so we prefer - // using ShmemBuffers to avoid the extra copy. - PerformanceRecorder<CopyVideoStage> rec( - "CamerasParent::VideoFrameToAltBuffer"_ns, aTrackingId, aFrame.width(), - aFrame.height()); - mAlternateBuffer.reset(new unsigned char[aProperties.bufferSize()]); - VideoFrameUtils::CopyVideoFrameBuffers(mAlternateBuffer.get(), - aProperties.bufferSize(), aFrame); - rec.Record(); - } + mBuffer(aFrame), + mProperties(aProperties) {} DeliverFrameRunnable(CamerasParent* aParent, CaptureEngine aEngine, - uint32_t aStreamId, const TrackingId& aTrackingId, - ShmemBuffer aBuffer, VideoFrameProperties& aProperties) + int aCaptureId, nsTArray<int>&& aStreamIds, + const TrackingId& aTrackingId, ShmemBuffer aBuffer, + VideoFrameProperties& aProperties) : Runnable("camera::DeliverFrameRunnable"), mParent(aParent), mCapEngine(aEngine), - mStreamId(aStreamId), + mCaptureId(aCaptureId), + mStreamIds(std::move(aStreamIds)), mTrackingId(aTrackingId), mBuffer(std::move(aBuffer)), - mProperties(aProperties), - mResult(0) {}; + mProperties(aProperties) {} NS_IMETHOD Run() override { // runs on BackgroundEventTarget @@ -295,45 +293,43 @@ class DeliverFrameRunnable : public mozilla::Runnable { mParent->mPBackgroundEventTarget); if (mParent->IsShuttingDown()) { // Communication channel is being torn down - mResult = 0; return NS_OK; } - if (!mParent->DeliverFrameOverIPC(mCapEngine, mStreamId, mTrackingId, - std::move(mBuffer), - mAlternateBuffer.get(), mProperties)) { - mResult = -1; - } else { - mResult = 0; - } + mParent->DeliverFrameOverIPC(mCapEngine, mCaptureId, mStreamIds, + mTrackingId, std::move(mBuffer), mProperties); return NS_OK; } - int GetResult() { return mResult; } - private: const RefPtr<CamerasParent> mParent; const CaptureEngine mCapEngine; - const uint32_t mStreamId; + const int mCaptureId; + const nsTArray<int> mStreamIds; const TrackingId mTrackingId; - ShmemBuffer mBuffer; - UniquePtr<unsigned char[]> mAlternateBuffer; + Variant<ShmemBuffer, webrtc::VideoFrame> mBuffer; const VideoFrameProperties mProperties; - int mResult; }; -int CamerasParent::DeliverFrameOverIPC(CaptureEngine aCapEngine, - uint32_t aStreamId, - const TrackingId& aTrackingId, - ShmemBuffer aBuffer, - unsigned char* aAltBuffer, - const VideoFrameProperties& aProps) { +int CamerasParent::DeliverFrameOverIPC( + CaptureEngine aCapEngine, int aCaptureId, const Span<const int>& aStreamIds, + const TrackingId& aTrackingId, + Variant<ShmemBuffer, webrtc::VideoFrame>&& aBuffer, + const VideoFrameProperties& aProps) { // No ShmemBuffers were available, so construct one now of the right size // and copy into it. That is an extra copy, but we expect this to be // the exceptional case, because we just assured the next call *will* have a // buffer of the right size. - if (aAltBuffer != nullptr) { + if (!aBuffer.is<ShmemBuffer>()) { // Get a shared memory buffer from the pool, at least size big - ShmemBuffer shMemBuff = mShmemPool.Get(this, aProps.bufferSize()); + ShmemBuffer shMemBuff; + { + auto guard = mShmemPools.Lock(); + auto it = guard->find(aCaptureId); + if (it != guard->end()) { + auto& [_, pool] = *it; + shMemBuff = pool.Get(this, aProps.bufferSize()); + } + } if (!shMemBuff.Valid()) { LOG("No usable Video shmem in DeliverFrame (out of buffers?)"); @@ -344,18 +340,20 @@ int CamerasParent::DeliverFrameOverIPC(CaptureEngine aCapEngine, PerformanceRecorder<CopyVideoStage> rec( "CamerasParent::AltBufferToShmem"_ns, aTrackingId, aProps.width(), aProps.height()); - // get() and Size() check for proper alignment of the segment - memcpy(shMemBuff.GetBytes(), aAltBuffer, aProps.bufferSize()); + VideoFrameUtils::CopyVideoFrameBuffers(shMemBuff, + aBuffer.as<webrtc::VideoFrame>()); rec.Record(); - if (!SendDeliverFrame(aStreamId, std::move(shMemBuff.Get()), aProps)) { + if (!SendDeliverFrame(aCaptureId, aStreamIds, std::move(shMemBuff.Get()), + aProps)) { return -1; } } else { - MOZ_ASSERT(aBuffer.Valid()); + MOZ_ASSERT(aBuffer.as<ShmemBuffer>().Valid()); // ShmemBuffer was available, we're all good. A single copy happened // in the original webrtc callback. - if (!SendDeliverFrame(aStreamId, std::move(aBuffer.Get()), aProps)) { + if (!SendDeliverFrame(aCaptureId, aStreamIds, + std::move(aBuffer.as<ShmemBuffer>().Get()), aProps)) { return -1; } } @@ -363,57 +361,275 @@ int CamerasParent::DeliverFrameOverIPC(CaptureEngine aCapEngine, return 0; } -ShmemBuffer CamerasParent::GetBuffer(size_t aSize) { - return mShmemPool.GetIfAvailable(aSize); +bool CamerasParent::IsWindowCapturing(uint64_t aWindowId, + const nsACString& aUniqueId) const { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + for (const auto& capturer : *mCapturers) { + if (capturer->mUniqueId != aUniqueId) { + continue; + } + auto streamsGuard = capturer->mStreams.ConstLock(); + for (const auto& stream : *streamsGuard) { + if (stream->mWindowId == aWindowId) { + return true; + } + } + } + return false; +} + +ShmemBuffer CamerasParent::GetBuffer(int aCaptureId, size_t aSize) { + auto guard = mShmemPools.Lock(); + auto it = guard->find(aCaptureId); + if (it == guard->end()) { + return ShmemBuffer(); + } + auto& [_, pool] = *it; + return pool.GetIfAvailable(aSize); +} + +/*static*/ +std::unique_ptr<AggregateCapturer> AggregateCapturer::Create( + nsISerialEventTarget* aVideoCaptureThread, CaptureEngine aCapEng, + VideoEngine* aEngine, const nsCString& aUniqueId, uint64_t aWindowId, + nsTArray<webrtc::VideoCaptureCapability>&& aCapabilities, + CamerasParent* aParent) { + MOZ_ASSERT(aVideoCaptureThread->IsOnCurrentThread()); + int captureId = aEngine->CreateVideoCapture(aUniqueId.get(), aWindowId); + auto capturer = WrapUnique( + new AggregateCapturer(aVideoCaptureThread, aCapEng, aEngine, aUniqueId, + captureId, std::move(aCapabilities))); + capturer->AddStream(aParent, captureId, aWindowId); + aEngine->WithEntry(captureId, [&](VideoEngine::CaptureEntry& aEntry) -> void { + aEntry.VideoCapture()->SetTrackingId(capturer->mTrackingId.mUniqueInProcId); + aEntry.VideoCapture()->RegisterCaptureDataCallback(capturer.get()); + if (auto* event = aEntry.CaptureEndedEvent()) { + capturer->mCaptureEndedListener = + event->Connect(aVideoCaptureThread, capturer.get(), + &AggregateCapturer::OnCaptureEnded); + } + }); + return capturer; +} + +AggregateCapturer::AggregateCapturer( + nsISerialEventTarget* aVideoCaptureThread, CaptureEngine aCapEng, + VideoEngine* aEngine, const nsCString& aUniqueId, int aCaptureId, + nsTArray<webrtc::VideoCaptureCapability>&& aCapabilities) + : mVideoCaptureThread(aVideoCaptureThread), + mCapEngine(aCapEng), + mEngine(aEngine), + mUniqueId(aUniqueId), + mCaptureId(aCaptureId), + mTrackingId(CaptureEngineToTrackingSourceStr(aCapEng), mCaptureId), + mCapabilities(std::move(aCapabilities)), + mStreams("CallbackHelper::mStreams") { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); +} + +AggregateCapturer::~AggregateCapturer() { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); +#ifdef DEBUG + { + auto streamsGuard = mStreams.Lock(); + MOZ_ASSERT(streamsGuard->IsEmpty()); + } +#endif + mCaptureEndedListener.DisconnectIfExists(); + mEngine->WithEntry(mCaptureId, [&](VideoEngine::CaptureEntry& aEntry) { + if (auto* cap = aEntry.VideoCapture().get()) { + cap->DeRegisterCaptureDataCallback(this); + cap->StopCaptureIfAllClientsClose(); + } + }); + MOZ_ALWAYS_FALSE(mEngine->ReleaseVideoCapture(mCaptureId)); +} + +void AggregateCapturer::AddStream(CamerasParent* aParent, int aStreamId, + uint64_t aWindowId) { + auto streamsGuard = mStreams.Lock(); +#ifdef DEBUG + for (const auto& stream : *streamsGuard) { + MOZ_ASSERT(stream->mId != aStreamId); + } +#endif + streamsGuard->AppendElement( + new Stream{.mParent = aParent, .mId = aStreamId, .mWindowId = aWindowId}); +} + +auto AggregateCapturer::RemoveStream(int aStreamId) -> RemoveStreamResult { + auto streamsGuard = mStreams.Lock(); + size_t idx = streamsGuard->IndexOf( + aStreamId, 0, + [](const auto& aElem, const auto& aId) { return aElem->mId - aId; }); + if (idx == streamsGuard->NoIndex) { + return {.mNumRemainingStreams = 0, .mNumRemainingStreamsForParent = 0}; + } + CamerasParent* parent = streamsGuard->ElementAt(idx)->mParent; + streamsGuard->RemoveElementAt(idx); + size_t remainingForParent = 0; + for (const auto& s : *streamsGuard) { + if (s->mParent == parent) { + remainingForParent += 1; + } + } + return {.mNumRemainingStreams = streamsGuard->Length(), + .mNumRemainingStreamsForParent = remainingForParent}; +} + +auto AggregateCapturer::RemoveStreamsFor(CamerasParent* aParent) + -> RemoveStreamResult { + auto streamsGuard = mStreams.Lock(); + streamsGuard->RemoveElementsBy( + [&](const auto& aElem) { return aElem->mParent == aParent; }); + return {.mNumRemainingStreams = streamsGuard->Length(), + .mNumRemainingStreamsForParent = 0}; +} + +Maybe<int> AggregateCapturer::CaptureIdFor(int aStreamId) { + auto streamsGuard = mStreams.Lock(); + for (auto& stream : *streamsGuard) { + if (stream->mId == aStreamId) { + return Some(mCaptureId); + } + } + return Nothing(); } -void CallbackHelper::SetConfiguration( - const webrtc::VideoCaptureCapability& aCapability, +void AggregateCapturer::SetConfigurationFor( + int aStreamId, const webrtc::VideoCaptureCapability& aCapability, const NormalizedConstraints& aConstraints, - const dom::VideoResizeModeEnum& aResizeMode) { - auto c = mConfiguration.Lock(); - c.ref() = Configuration{ - .mCapability = aCapability, - .mConstraints = aConstraints, - .mResizeMode = aResizeMode, - }; + const dom::VideoResizeModeEnum& aResizeMode, bool aStarted) { + auto streamsGuard = mStreams.Lock(); + for (auto& stream : *streamsGuard) { + if (stream->mId == aStreamId) { + stream->mConfiguration = { + .mCapability = aCapability, + .mConstraints = aConstraints, + .mResizeMode = aResizeMode, + }; + stream->mStarted = aStarted; + break; + } + } } -void CallbackHelper::OnCaptureEnded() { - nsIEventTarget* target = mParent->GetBackgroundEventTarget(); +webrtc::VideoCaptureCapability AggregateCapturer::CombinedCapability() { + Maybe<webrtc::VideoCaptureCapability> combinedCap; + CamerasParent* someParent{}; + const auto streamsGuard = mStreams.ConstLock(); + for (const auto& stream : *streamsGuard) { + if (!stream->mStarted) { + continue; + } + if (!someParent) { + someParent = stream->mParent; + } + const auto& cap = stream->mConfiguration.mCapability; + if (!combinedCap) { + combinedCap = Some(cap); + continue; + } + auto combinedRes = combinedCap->width * combinedCap->height; + combinedCap->maxFPS = std::max(combinedCap->maxFPS, cap.maxFPS); + if (mCapEngine == CaptureEngine::CameraEngine) { + auto newCombinedRes = cap.width * cap.height; + if (newCombinedRes > combinedRes) { + combinedCap->videoType = cap.videoType; + } + combinedCap->width = std::max(combinedCap->width, cap.width); + combinedCap->height = std::max(combinedCap->height, cap.height); + } + } + if (mCapEngine == CameraEngine) { + const webrtc::VideoCaptureCapability* minDistanceCapability{}; + uint64_t minDistance = UINT64_MAX; - MOZ_ALWAYS_SUCCEEDS(target->Dispatch(NS_NewRunnableFunction( - __func__, [parent = RefPtr(mParent), id = mStreamId] { - (void)parent->SendCaptureEnded(id); - }))); + for (const auto& candidateCapability : mCapabilities) { + if (candidateCapability.videoType != combinedCap->videoType) { + continue; + } + // The first priority is finding a suitable resolution. + // So here we raise the weight of width and height + uint64_t distance = + uint64_t(ResolutionFeasibilityDistance(candidateCapability.width, + combinedCap->width)) + + uint64_t(ResolutionFeasibilityDistance(candidateCapability.height, + combinedCap->height)) + + uint64_t(FeasibilityDistance(candidateCapability.maxFPS, + combinedCap->maxFPS)); + if (distance < minDistance) { + minDistanceCapability = &candidateCapability; + minDistance = distance; + } + } + if (minDistanceCapability) { + combinedCap = Some(*minDistanceCapability); + } + } + return combinedCap.extract(); +} + +void AggregateCapturer::OnCaptureEnded() { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + std::multimap<CamerasParent*, int> parentsAndIds; + { + auto streamsGuard = mStreams.Lock(); + for (const auto& stream : *streamsGuard) { + parentsAndIds.insert({stream->mParent, stream->mId}); + } + } + + for (auto it = parentsAndIds.begin(); it != parentsAndIds.end();) { + const auto& parent = it->first; + auto nextParentIt = parentsAndIds.upper_bound(parent); + AutoTArray<int, 4> ids; + while (it != nextParentIt) { + const auto& [_, id] = *it; + ids.AppendElement(id); + ++it; + } + nsIEventTarget* target = parent->GetBackgroundEventTarget(); + MOZ_ALWAYS_SUCCEEDS(target->Dispatch(NS_NewRunnableFunction( + __func__, [parent = RefPtr(parent), ids = std::move(ids)] { + (void)parent->SendCaptureEnded(ids); + }))); + } } -void CallbackHelper::OnFrame(const webrtc::VideoFrame& aVideoFrame) { +void AggregateCapturer::OnFrame(const webrtc::VideoFrame& aVideoFrame) { + std::multimap<CamerasParent*, int> parentsAndIds; { // Proactively drop frames that would not get processed anyway. - auto c = mConfiguration.Lock(); - - const double maxFramerate = static_cast<double>( - c->mCapability.maxFPS > 0 ? c->mCapability.maxFPS : 120); - const double desiredFramerate = - c->mResizeMode == VideoResizeModeEnum::Crop_and_scale - ? c->mConstraints.mFrameRate.Get(maxFramerate) - : maxFramerate; - const double targetFramerate = std::clamp(desiredFramerate, 0.01, 120.); - - // Allow 5% higher fps than configured as frame time sampling is timing - // dependent. - const auto minInterval = - media::TimeUnit(1000, static_cast<int64_t>(1050 * targetFramerate)); - const auto frameTime = - media::TimeUnit::FromMicroseconds(aVideoFrame.timestamp_us()); - const auto frameInterval = frameTime - mLastFrameTime; - if (frameInterval < minInterval) { - return; + auto streamsGuard = mStreams.Lock(); + + for (auto& stream : *streamsGuard) { + auto& c = stream->mConfiguration; + const double maxFramerate = static_cast<double>( + c.mCapability.maxFPS > 0 ? c.mCapability.maxFPS : 120); + const double desiredFramerate = + c.mResizeMode == VideoResizeModeEnum::Crop_and_scale + ? c.mConstraints.mFrameRate.Get(maxFramerate) + : maxFramerate; + const double targetFramerate = std::clamp(desiredFramerate, 0.01, 120.); + + // Allow 5% higher fps than configured as frame time sampling is timing + // dependent. + const auto minInterval = + media::TimeUnit(1000, static_cast<int64_t>(1050 * targetFramerate)); + const auto frameTime = + media::TimeUnit::FromMicroseconds(aVideoFrame.timestamp_us()); + const auto frameInterval = frameTime - stream->mLastFrameTime; + if (frameInterval < minInterval) { + continue; + } + stream->mLastFrameTime = frameTime; + LOG_VERBOSE("CamerasParent::%s parent=%p, id=%d.", __func__, + stream->mParent, stream->mId); + parentsAndIds.insert({stream->mParent, stream->mId}); } - mLastFrameTime = frameTime; } - LOG_VERBOSE("CamerasParent(%p)::%s", mParent, __func__); + if (profiler_thread_is_being_profiled_for_markers()) { PROFILER_MARKER_UNTYPED( nsPrintfCString("CaptureVideoFrame %dx%d %s %s", aVideoFrame.width(), @@ -423,43 +639,68 @@ void CallbackHelper::OnFrame(const webrtc::VideoFrame& aVideoFrame) { mTrackingId.ToString().get()), MEDIA_RT); } - RefPtr<DeliverFrameRunnable> runnable = nullptr; + // Get frame properties camera::VideoFrameProperties properties; VideoFrameUtils::InitFrameBufferProperties(aVideoFrame, properties); - // Get a shared memory buffer to copy the frame data into - ShmemBuffer shMemBuffer = mParent->GetBuffer(properties.bufferSize()); - if (!shMemBuffer.Valid()) { - // Either we ran out of buffers or they're not the right size yet - LOG("Correctly sized Video shmem not available in DeliverFrame"); - // We will do the copy into a(n extra) temporary buffer inside - // the DeliverFrameRunnable constructor. - } else { - // Shared memory buffers of the right size are available, do the copy here. - PerformanceRecorder<CopyVideoStage> rec( - "CamerasParent::VideoFrameToShmem"_ns, mTrackingId, aVideoFrame.width(), - aVideoFrame.height()); - VideoFrameUtils::CopyVideoFrameBuffers( - shMemBuffer.GetBytes(), properties.bufferSize(), aVideoFrame); - rec.Record(); - runnable = - new DeliverFrameRunnable(mParent, mCapEngine, mStreamId, mTrackingId, - std::move(shMemBuffer), properties); - } - if (!runnable) { - runnable = new DeliverFrameRunnable(mParent, mCapEngine, mStreamId, - mTrackingId, aVideoFrame, properties); + + for (auto it = parentsAndIds.begin(); it != parentsAndIds.end();) { + const auto& parent = it->first; + auto nextParentIt = parentsAndIds.upper_bound(parent); + AutoTArray<int, 4> ids; + while (it != nextParentIt) { + const auto& [_, id] = *it; + ids.AppendElement(id); + ++it; + } + + LOG_VERBOSE("CamerasParent(%p)::%s", parent, __func__); + RefPtr<DeliverFrameRunnable> runnable = nullptr; + // Get a shared memory buffer to copy the frame data into + ShmemBuffer shMemBuffer = + parent->GetBuffer(mCaptureId, properties.bufferSize()); + if (!shMemBuffer.Valid()) { + // Either we ran out of buffers or they're not the right size yet + LOG("Correctly sized Video shmem not available in DeliverFrame"); + // We will do the copy into a(n extra) temporary buffer inside + // the DeliverFrameRunnable constructor. + } else { + // Shared memory buffers of the right size are available, do the copy + // here. + PerformanceRecorder<CopyVideoStage> rec( + "CamerasParent::VideoFrameToShmem"_ns, mTrackingId, + aVideoFrame.width(), aVideoFrame.height()); + VideoFrameUtils::CopyVideoFrameBuffers( + shMemBuffer.GetBytes(), properties.bufferSize(), aVideoFrame); + rec.Record(); + runnable = new DeliverFrameRunnable(parent, mCapEngine, mCaptureId, + std::move(ids), mTrackingId, + std::move(shMemBuffer), properties); + } + if (!runnable) { + runnable = new DeliverFrameRunnable(parent, mCapEngine, mCaptureId, + std::move(ids), mTrackingId, + aVideoFrame, properties); + } + nsIEventTarget* target = parent->GetBackgroundEventTarget(); + target->Dispatch(runnable, NS_DISPATCH_NORMAL); } - MOZ_ASSERT(mParent); - nsIEventTarget* target = mParent->GetBackgroundEventTarget(); - MOZ_ASSERT(target != nullptr); - target->Dispatch(runnable, NS_DISPATCH_NORMAL); } -ipc::IPCResult CamerasParent::RecvReleaseFrame(ipc::Shmem&& aShmem) { +ipc::IPCResult CamerasParent::RecvReleaseFrame(const int& aCaptureId, + ipc::Shmem&& aShmem) { MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); - mShmemPool.Put(ShmemBuffer(aShmem)); + auto guard = mShmemPools.Lock(); + auto it = guard->find(aCaptureId); + if (it == guard->end()) { + MOZ_ASSERT_UNREACHABLE( + "Releasing shmem but pool is already gone. Shmem must have been " + "deallocated."); + return IPC_FAIL(this, "Shmem was already deallocated"); + } + auto& [_, pool] = *it; + pool.Put(ShmemBuffer(aShmem)); return IPC_OK(); } @@ -467,13 +708,16 @@ void CamerasParent::CloseEngines() { MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); LOG_FUNCTION(); - // Stop the callers - while (!mCallbacks.IsEmpty()) { - auto capEngine = mCallbacks[0]->mCapEngine; - auto streamNum = mCallbacks[0]->mStreamId; - LOG("Forcing shutdown of engine %d, capturer %d", capEngine, streamNum); - StopCapture(capEngine, streamNum); - (void)ReleaseCapture(capEngine, streamNum); + // Stop the capturers. + for (const auto& capturer : Reversed(*mCapturers)) { + auto removed = capturer->RemoveStreamsFor(this); + if (removed.mNumRemainingStreams == 0) { + auto capEngine = capturer->mCapEngine; + auto captureId = capturer->mCaptureId; + size_t idx = mCapturers->LastIndexOf(capturer); + mCapturers->RemoveElementAtUnsafe(idx); + LOG("Forcing shutdown of engine %d, capturer %d", capEngine, captureId); + } } mDeviceChangeEventListener.DisconnectIfExists(); @@ -675,30 +919,21 @@ ipc::IPCResult CamerasParent::RecvGetCaptureCapability( InvokeAsync(mVideoCaptureThread, __func__, [this, self = RefPtr(this), id = nsCString(aUniqueId), aCapEngine, aIndex] { + nsTArray<webrtc::VideoCaptureCapability> const* capabilities = + EnsureCapabilitiesPopulated(aCapEngine, id); webrtc::VideoCaptureCapability webrtcCaps; - int error = -1; - if (auto devInfo = GetDeviceInfo(aCapEngine)) { - error = devInfo->GetCapability(id.get(), aIndex, webrtcCaps); - } - - if (!error && aCapEngine == CameraEngine) { - auto iter = mAllCandidateCapabilities.find(id); - if (iter == mAllCandidateCapabilities.end()) { - std::map<uint32_t, webrtc::VideoCaptureCapability> - candidateCapabilities; - candidateCapabilities.emplace(aIndex, webrtcCaps); - mAllCandidateCapabilities.emplace(id, - candidateCapabilities); - } else { - (iter->second).emplace(aIndex, webrtcCaps); - } + if (!capabilities) { + return Promise::CreateAndReject( + -1, "CamerasParent::RecvGetCaptureCapability"); } - if (error) { + if (aIndex < 0 || + static_cast<size_t>(aIndex) >= capabilities->Length()) { return Promise::CreateAndReject( - error, "CamerasParent::RecvGetCaptureCapability"); + -2, "CamerasParent::RecvGetCaptureCapability"); } return Promise::CreateAndResolve( - webrtcCaps, "CamerasParent::RecvGetCaptureCapability"); + capabilities->ElementAt(aIndex), + "CamerasParent::RecvGetCaptureCapability"); }) ->Then( mPBackgroundEventTarget, __func__, @@ -854,46 +1089,66 @@ ipc::IPCResult CamerasParent::RecvAllocateCapture( using Promise1 = MozPromise<bool, bool, true>; using Data = std::tuple<int, int>; using Promise2 = MozPromise<Data, bool, true>; - InvokeAsync(GetMainThreadSerialEventTarget(), __func__, - [aWindowID] { - // Verify whether the claimed origin has received permission - // to use the camera, either persistently or this session (one - // shot). - bool allowed = HasCameraPermission(aWindowID); - if (!allowed) { - // Developer preference for turning off permission check. - if (Preferences::GetBool( - "media.navigator.permission.disabled", false)) { - allowed = true; - LOG("No permission but checks are disabled"); - } else { - LOG("No camera permission for this origin"); - } - } - return Promise1::CreateAndResolve( - allowed, "CamerasParent::RecvAllocateCapture"); - }) - ->Then(mVideoCaptureThread, __func__, - [this, self = RefPtr(this), aCapEngine, - unique_id = nsCString(aUniqueIdUTF8)]( - Promise1::ResolveOrRejectValue&& aValue) { - bool allowed = aValue.ResolveValue(); - int captureId = -1; - int error = -1; - if (allowed && EnsureInitialized(aCapEngine)) { - VideoEngine* engine = mEngines->ElementAt(aCapEngine); - captureId = engine->CreateVideoCapture(unique_id.get()); - engine->WithEntry(captureId, - [&error](VideoEngine::CaptureEntry& cap) { - if (cap.VideoCapture()) { - error = 0; - } - }); - } - return Promise2::CreateAndResolve( - std::make_tuple(captureId, error), - "CamerasParent::RecvAllocateCapture"); - }) + InvokeAsync( + GetMainThreadSerialEventTarget(), __func__, + [aWindowID] { + // Verify whether the claimed origin has received permission + // to use the camera, either persistently or this session (one + // shot). + bool allowed = HasCameraPermission(aWindowID); + if (!allowed && Preferences::GetBool( + "media.navigator.permission.disabled", false)) { + // Developer preference for turning off permission check. + allowed = true; + LOG("No permission but checks are disabled"); + } + if (!allowed) { + LOG("No camera permission for this origin"); + } + return Promise1::CreateAndResolve(allowed, + "CamerasParent::RecvAllocateCapture"); + }) + ->Then( + mVideoCaptureThread, __func__, + [this, self = RefPtr(this), aCapEngine, aWindowID, + unique_id = nsCString(aUniqueIdUTF8)]( + Promise1::ResolveOrRejectValue&& aValue) { + VideoEngine* engine = EnsureInitialized(aCapEngine); + if (!engine) { + return Promise2::CreateAndResolve( + std::make_tuple(-1, -1), + "CamerasParent::RecvAllocateCapture no engine"); + } + bool allowed = aValue.ResolveValue(); + if (!allowed && IsWindowCapturing(aWindowID, unique_id)) { + allowed = true; + LOG("No permission but window is already capturing this device"); + } + if (!allowed) { + return Promise2::CreateAndResolve( + std::make_tuple(-1, -1), + "CamerasParent::RecvAllocateCapture"); + } + + nsTArray<webrtc::VideoCaptureCapability> capabilities; + if (const auto* caps = + EnsureCapabilitiesPopulated(aCapEngine, unique_id)) { + capabilities.AppendElements(*caps); + } + + auto created = GetOrCreateCapturer(aCapEngine, aWindowID, unique_id, + std::move(capabilities)); + int error = -1; + engine->WithEntry(created.mCapturer->mCaptureId, + [&](VideoEngine::CaptureEntry& cap) { + if (cap.VideoCapture()) { + error = 0; + } + }); + return Promise2::CreateAndResolve( + std::make_tuple(created.mStreamId, error), + "CamerasParent::RecvAllocateCapture"); + }) ->Then( mPBackgroundEventTarget, __func__, [this, self = RefPtr(this)](Promise2::ResolveOrRejectValue&& aValue) { @@ -915,34 +1170,24 @@ ipc::IPCResult CamerasParent::RecvAllocateCapture( return IPC_OK(); } -int CamerasParent::ReleaseCapture(const CaptureEngine& aCapEngine, - int aCaptureId) { - MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); - int error = -1; - if (auto* engine = EnsureInitialized(aCapEngine)) { - error = engine->ReleaseVideoCapture(aCaptureId); - } - return error; -} - ipc::IPCResult CamerasParent::RecvReleaseCapture( - const CaptureEngine& aCapEngine, const int& aCaptureId) { + const CaptureEngine& aCapEngine, const int& aStreamId) { MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); MOZ_ASSERT(!mDestroyed); LOG_FUNCTION(); - LOG("RecvReleaseCamera device nr %d", aCaptureId); + LOG("RecvReleaseCapture stream nr %d", aStreamId); using Promise = MozPromise<int, bool, true>; InvokeAsync(mVideoCaptureThread, __func__, - [this, self = RefPtr(this), aCapEngine, aCaptureId] { + [this, self = RefPtr(this), aCapEngine, aStreamId] { return Promise::CreateAndResolve( - ReleaseCapture(aCapEngine, aCaptureId), + ReleaseStream(aCapEngine, aStreamId), "CamerasParent::RecvReleaseCapture"); }) ->Then(mPBackgroundEventTarget, __func__, [this, self = RefPtr(this), - aCaptureId](Promise::ResolveOrRejectValue&& aValue) { + aStreamId](Promise::ResolveOrRejectValue&& aValue) { int error = aValue.ResolveValue(); if (mDestroyed) { @@ -952,19 +1197,19 @@ ipc::IPCResult CamerasParent::RecvReleaseCapture( if (error != 0) { (void)SendReplyFailure(); - LOG("RecvReleaseCapture: Failed to free device nr %d", - aCaptureId); + LOG("RecvReleaseCapture: Failed to free stream nr %d", + aStreamId); return; } (void)SendReplySuccess(); - LOG("Freed device nr %d", aCaptureId); + LOG("Freed stream nr %d", aStreamId); }); return IPC_OK(); } ipc::IPCResult CamerasParent::RecvStartCapture( - const CaptureEngine& aCapEngine, const int& aCaptureId, + const CaptureEngine& aCapEngine, const int& aStreamId, const VideoCaptureCapability& aIpcCaps, const NormalizedConstraints& aConstraints, const dom::VideoResizeModeEnum& aResizeMode) { @@ -976,18 +1221,24 @@ ipc::IPCResult CamerasParent::RecvStartCapture( using Promise = MozPromise<int, bool, true>; InvokeAsync( mVideoCaptureThread, __func__, - [this, self = RefPtr(this), aCapEngine, aCaptureId, aIpcCaps, - aConstraints, aResizeMode] { + [this, self = RefPtr(this), aCapEngine, aStreamId, aIpcCaps, aConstraints, + aResizeMode] { LOG_FUNCTION(); - int error = -1; if (!EnsureInitialized(aCapEngine)) { - return Promise::CreateAndResolve(error, + return Promise::CreateAndResolve(-1, "CamerasParent::RecvStartCapture"); } + AggregateCapturer* cbh = GetCapturer(aCapEngine, aStreamId); + if (!cbh) { + return Promise::CreateAndResolve(-1, + "CamerasParent::RecvStartCapture"); + } + + int error = -1; mEngines->ElementAt(aCapEngine) - ->WithEntry(aCaptureId, [&](VideoEngine::CaptureEntry& cap) { + ->WithEntry(cbh->mCaptureId, [&](VideoEngine::CaptureEntry& cap) { webrtc::VideoCaptureCapability capability; capability.width = aIpcCaps.width(); capability.height = aIpcCaps.height(); @@ -996,123 +1247,15 @@ ipc::IPCResult CamerasParent::RecvStartCapture( static_cast<webrtc::VideoType>(aIpcCaps.videoType()); capability.interlaced = aIpcCaps.interlaced(); - if (sDeviceUniqueIDs.find(aCaptureId) == sDeviceUniqueIDs.end()) { - sDeviceUniqueIDs.emplace( - aCaptureId, cap.VideoCapture()->CurrentDeviceName()); - sAllRequestedCapabilities.emplace(aCaptureId, capability); - } else { - // Starting capture for an id that already exists. Update its - // requested capability. - MOZ_DIAGNOSTIC_ASSERT( - strcmp(sDeviceUniqueIDs[aCaptureId], - cap.VideoCapture()->CurrentDeviceName()) == 0); - MOZ_DIAGNOSTIC_ASSERT( - sAllRequestedCapabilities.find(aCaptureId) != - sAllRequestedCapabilities.end()); - sAllRequestedCapabilities[aCaptureId] = capability; - } - - if (aCapEngine == CameraEngine) { - for (const auto& it : sDeviceUniqueIDs) { - if (strcmp(it.second, - cap.VideoCapture()->CurrentDeviceName()) == 0) { - capability.width = - std::max(capability.width, - sAllRequestedCapabilities[it.first].width); - capability.height = - std::max(capability.height, - sAllRequestedCapabilities[it.first].height); - capability.maxFPS = - std::max(capability.maxFPS, - sAllRequestedCapabilities[it.first].maxFPS); - } - } - - auto candidateCapabilities = mAllCandidateCapabilities.find( - nsCString(cap.VideoCapture()->CurrentDeviceName())); - if ((candidateCapabilities != - mAllCandidateCapabilities.end()) && - (!candidateCapabilities->second.empty())) { - int32_t minIdx = -1; - uint64_t minDistance = UINT64_MAX; - - for (auto& candidateCapability : - candidateCapabilities->second) { - if (candidateCapability.second.videoType != - capability.videoType) { - continue; - } - // The first priority is finding a suitable resolution. - // So here we raise the weight of width and height - uint64_t distance = uint64_t(ResolutionFeasibilityDistance( - candidateCapability.second.width, - capability.width)) + - uint64_t(ResolutionFeasibilityDistance( - candidateCapability.second.height, - capability.height)) + - uint64_t(FeasibilityDistance( - candidateCapability.second.maxFPS, - capability.maxFPS)); - if (distance < minDistance) { - minIdx = static_cast<int32_t>(candidateCapability.first); - minDistance = distance; - } - } - MOZ_ASSERT(minIdx != -1); - capability = candidateCapabilities->second[minIdx]; - } - } else if (aCapEngine == ScreenEngine || - aCapEngine == BrowserEngine || - aCapEngine == WinEngine) { - for (const auto& it : sDeviceUniqueIDs) { - if (strcmp(it.second, - cap.VideoCapture()->CurrentDeviceName()) == 0) { - capability.maxFPS = - std::max(capability.maxFPS, - sAllRequestedCapabilities[it.first].maxFPS); - } - } - } - - CallbackHelper* cbh = nullptr; - for (auto& cb : mCallbacks) { - if (cb->mCapEngine == aCapEngine && - cb->mStreamId == (uint32_t)aCaptureId) { - cbh = cb.get(); - break; - } - } - bool cbhCreated = !cbh; - if (!cbh) { - cbh = mCallbacks - .AppendElement(MakeUnique<CallbackHelper>( - static_cast<CaptureEngine>(aCapEngine), - aCaptureId, this)) - ->get(); - cap.VideoCapture()->SetTrackingId( - cbh->mTrackingId.mUniqueInProcId); - } - - cbh->SetConfiguration(capability, aConstraints, aResizeMode); - error = cap.VideoCapture()->StartCapture(capability); - - if (!error) { - if (cbhCreated) { - cap.VideoCapture()->RegisterCaptureDataCallback( - static_cast< - webrtc::VideoSinkInterface<webrtc::VideoFrame>*>( - cbh)); - if (auto* event = cap.CaptureEndedEvent(); - event && !cbh->mConnectedToCaptureEnded) { - cbh->mCaptureEndedListener = - event->Connect(mVideoCaptureThread, cbh, - &CallbackHelper::OnCaptureEnded); - cbh->mConnectedToCaptureEnded = true; - } + if (cbh) { + cbh->SetConfigurationFor(aStreamId, capability, aConstraints, + aResizeMode, /*aStarted=*/true); + error = + cap.VideoCapture()->StartCapture(cbh->CombinedCapability()); + if (error) { + cbh->SetConfigurationFor(aStreamId, capability, aConstraints, + aResizeMode, /*aStarted=*/false); } - } else { - sDeviceUniqueIDs.erase(aCaptureId); - sAllRequestedCapabilities.erase(aCaptureId); } }); @@ -1141,7 +1284,7 @@ ipc::IPCResult CamerasParent::RecvStartCapture( } ipc::IPCResult CamerasParent::RecvFocusOnSelectedSource( - const CaptureEngine& aCapEngine, const int& aCaptureId) { + const CaptureEngine& aCapEngine, const int& aStreamId) { MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); MOZ_ASSERT(!mDestroyed); @@ -1149,11 +1292,17 @@ ipc::IPCResult CamerasParent::RecvFocusOnSelectedSource( using Promise = MozPromise<bool, bool, true>; InvokeAsync(mVideoCaptureThread, __func__, - [this, self = RefPtr(this), aCapEngine, aCaptureId] { + [this, self = RefPtr(this), aCapEngine, aStreamId] { bool result = false; + auto* capturer = GetCapturer(aCapEngine, aStreamId); + if (!capturer) { + return Promise::CreateAndResolve( + result, "CamerasParent::RecvFocusOnSelectedSource"); + } if (auto* engine = EnsureInitialized(aCapEngine)) { engine->WithEntry( - aCaptureId, [&](VideoEngine::CaptureEntry& cap) { + capturer->mCaptureId, + [&](VideoEngine::CaptureEntry& cap) { if (cap.VideoCapture()) { result = cap.VideoCapture()->FocusOnSelectedSource(); } @@ -1182,45 +1331,109 @@ ipc::IPCResult CamerasParent::RecvFocusOnSelectedSource( return IPC_OK(); } -void CamerasParent::StopCapture(const CaptureEngine& aCapEngine, - int aCaptureId) { +auto CamerasParent::GetOrCreateCapturer( + CaptureEngine aEngine, uint64_t aWindowId, const nsCString& aUniqueId, + nsTArray<webrtc::VideoCaptureCapability>&& aCapabilities) + -> GetOrCreateCapturerResult { MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); - if (auto* engine = EnsureInitialized(aCapEngine)) { - // we're removing elements, iterate backwards - for (size_t i = mCallbacks.Length(); i > 0; i--) { - if (mCallbacks[i - 1]->mCapEngine == aCapEngine && - mCallbacks[i - 1]->mStreamId == (uint32_t)aCaptureId) { - CallbackHelper* cbh = mCallbacks[i - 1].get(); - engine->WithEntry(aCaptureId, [cbh, &aCaptureId]( - VideoEngine::CaptureEntry& cap) { - if (cap.VideoCapture()) { - cap.VideoCapture()->DeRegisterCaptureDataCallback( - static_cast<webrtc::VideoSinkInterface<webrtc::VideoFrame>*>( - cbh)); - cap.VideoCapture()->StopCaptureIfAllClientsClose(); - - sDeviceUniqueIDs.erase(aCaptureId); - sAllRequestedCapabilities.erase(aCaptureId); - } - }); - cbh->mCaptureEndedListener.DisconnectIfExists(); - mCallbacks.RemoveElementAt(i - 1); - break; - } + VideoEngine* engine = EnsureInitialized(aEngine); + const auto ensureShmemPool = [&](int aCaptureId) { + auto guard = mShmemPools.Lock(); + constexpr size_t kMaxShmemBuffers = 1; + guard->try_emplace(aCaptureId, kMaxShmemBuffers); + }; + for (auto& capturer : *mCapturers) { + if (capturer->mCapEngine != aEngine) { + continue; + } + if (capturer->mUniqueId.Equals(aUniqueId)) { + int streamId = engine->GenerateId(); + ensureShmemPool(capturer->mCaptureId); + capturer->AddStream(this, streamId, aWindowId); + return {.mCapturer = capturer.get(), .mStreamId = streamId}; + } + } + NotNull capturer = mCapturers->AppendElement( + AggregateCapturer::Create(mVideoCaptureThread, aEngine, engine, aUniqueId, + aWindowId, std::move(aCapabilities), this)); + ensureShmemPool(capturer->get()->mCaptureId); + return {.mCapturer = capturer->get(), + .mStreamId = capturer->get()->mCaptureId}; +} + +AggregateCapturer* CamerasParent::GetCapturer(CaptureEngine aEngine, + int aStreamId) { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + for (auto& capturer : *mCapturers) { + if (capturer->mCapEngine != aEngine) { + continue; + } + Maybe captureId = capturer->CaptureIdFor(aStreamId); + if (captureId) { + return capturer.get(); } } + return nullptr; +} + +int CamerasParent::ReleaseStream(CaptureEngine aEngine, int aStreamId) { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + auto* capturer = GetCapturer(aEngine, aStreamId); + if (!capturer) { + return -1; + } + auto removed = capturer->RemoveStream(aStreamId); + if (removed.mNumRemainingStreams == 0) { + mCapturers->RemoveElement(capturer); + } + return 0; +} + +nsTArray<webrtc::VideoCaptureCapability> const* +CamerasParent::EnsureCapabilitiesPopulated(CaptureEngine aEngine, + const nsCString& aUniqueId) { + MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread()); + if (auto iter = mAllCandidateCapabilities.find(aUniqueId); + iter != mAllCandidateCapabilities.end()) { + return &iter->second; + } + auto devInfo = GetDeviceInfo(aEngine); + if (!devInfo) { + return nullptr; + } + const int num = devInfo->NumberOfCapabilities(aUniqueId.get()); + if (num <= 0) { + return nullptr; + } + nsTArray<webrtc::VideoCaptureCapability> capabilities(num); + for (int i = 0; i < num; ++i) { + webrtc::VideoCaptureCapability capability; + if (devInfo->GetCapability(aUniqueId.get(), i, capability)) { + return nullptr; + } + capabilities.AppendElement(capability); + } + const auto& [iter, _] = + mAllCandidateCapabilities.emplace(aUniqueId, std::move(capabilities)); + return &iter->second; } ipc::IPCResult CamerasParent::RecvStopCapture(const CaptureEngine& aCapEngine, - const int& aCaptureId) { + const int& aStreamId) { MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); MOZ_ASSERT(!mDestroyed); LOG_FUNCTION(); nsresult rv = mVideoCaptureThread->Dispatch(NS_NewRunnableFunction( - __func__, [this, self = RefPtr(this), aCapEngine, aCaptureId] { - StopCapture(aCapEngine, aCaptureId); + __func__, [this, self = RefPtr(this), aCapEngine, aStreamId] { + auto* capturer = GetCapturer(aCapEngine, aStreamId); + if (capturer) { + capturer->SetConfigurationFor( + aStreamId, webrtc::VideoCaptureCapability{}, + NormalizedConstraints{}, dom::VideoResizeModeEnum::None, + /*aStarted=*/false); + } })); if (mDestroyed) { @@ -1246,7 +1459,12 @@ void CamerasParent::ActorDestroy(ActorDestroyReason aWhy) { LOG_FUNCTION(); // Release shared memory now, it's our last chance - mShmemPool.Cleanup(this); + { + auto guard = mShmemPools.Lock(); + for (auto& [captureId, pool] : *guard) { + pool.Cleanup(this); + } + } // We don't want to receive callbacks or anything if we can't // forward them anymore anyway. mDestroyed = true; @@ -1277,8 +1495,9 @@ CamerasParent::CamerasParent() ? MakeAndAddRefVideoCaptureThreadAndSingletons() : nullptr), mEngines(sEngines), + mCapturers(sCapturers), mVideoCaptureFactory(EnsureVideoCaptureFactory()), - mShmemPool(CaptureEngine::MaxEngine), + mShmemPools("CamerasParent::mShmemPools"), mPBackgroundEventTarget(GetCurrentSerialEventTarget()), mDestroyed(false) { MOZ_ASSERT(mPBackgroundEventTarget != nullptr, diff --git a/dom/media/systemservices/CamerasParent.h b/dom/media/systemservices/CamerasParent.h @@ -28,26 +28,56 @@ namespace mozilla::camera { class CamerasParent; class VideoEngine; -class CallbackHelper : public webrtc::VideoSinkInterface<webrtc::VideoFrame> { +// Class that manages sharing of VideoCaptureImpl instances on top of +// VideoEngine. Sharing is needed as access to most sources is exclusive +// system-wide. +// +// There is at most one AggregateCapturer instance per unique source, as defined +// by its unique capture ID. +// +// There can be multiple requests for a stream from a source, as defined by +// unique stream IDs. +// +// Stream IDs and capture IDs use the same ID space. With capture happening in +// the parent process, application-wide uniqueness is guaranteed. +// +// When multiple stream requests have been made for a source, even across +// multiple CamerasParent instances, this class distributes a single frame to +// each CamerasParent instance that has requested a stream. Distribution to the +// various stream requests happens in CamerasChild::RecvDeliverFrame. +// +// This class similarly handles capture-ended events, and distributes them to +// the correct CamerasParent instances, with distribution to streams happening +// in CamerasChild::RecvCaptureEnded. +class AggregateCapturer final + : public webrtc::VideoSinkInterface<webrtc::VideoFrame> { public: - CallbackHelper(CaptureEngine aCapEng, uint32_t aStreamId, - CamerasParent* aParent) - : mCapEngine(aCapEng), - mStreamId(aStreamId), - mTrackingId(CaptureEngineToTrackingSourceStr(aCapEng), aStreamId), - mParent(aParent), - mConfiguration("CallbackHelper::mConfiguration") {}; - - void SetConfiguration(const webrtc::VideoCaptureCapability& aCapability, - const NormalizedConstraints& aConstraints, - const dom::VideoResizeModeEnum& aResizeMode); + static std::unique_ptr<AggregateCapturer> Create( + nsISerialEventTarget* aVideoCaptureThread, CaptureEngine aCapEng, + VideoEngine* aEngine, const nsCString& aUniqueId, uint64_t aWindowId, + nsTArray<webrtc::VideoCaptureCapability>&& aCapabilities, + CamerasParent* aParent); + + ~AggregateCapturer(); + + void AddStream(CamerasParent* aParent, int aStreamId, uint64_t aWindowId); + struct RemoveStreamResult { + size_t mNumRemainingStreams; + size_t mNumRemainingStreamsForParent; + }; + RemoveStreamResult RemoveStream(int aStreamId); + RemoveStreamResult RemoveStreamsFor(CamerasParent* aParent); + Maybe<int> CaptureIdFor(int aStreamId); + void SetConfigurationFor(int aStreamId, + const webrtc::VideoCaptureCapability& aCapability, + const NormalizedConstraints& aConstraints, + const dom::VideoResizeModeEnum& aResizeMode, + bool aStarted); + webrtc::VideoCaptureCapability CombinedCapability(); void OnCaptureEnded(); void OnFrame(const webrtc::VideoFrame& aVideoFrame) override; - friend CamerasParent; - - private: struct Configuration { webrtc::VideoCaptureCapability mCapability; NormalizedConstraints mConstraints; @@ -55,15 +85,55 @@ class CallbackHelper : public webrtc::VideoSinkInterface<webrtc::VideoFrame> { // defaults factored in. dom::VideoResizeModeEnum mResizeMode{}; }; + // Representation of a stream request for the source of this AggregateCapturer + // instance. + struct Stream { + // The CamerasParent instance that requested this stream. mParent is + // responsible for the lifetime of this stream. + CamerasParent* const mParent; + // The id that identifies this stream. This is unique within the application + // session, in the same set of IDs as AggregateCapturer::mCaptureId. + const int mId{-1}; + // The id of the window where the request for this stream originated. + const uint64_t mWindowId{}; + // The configuration applied to this stream. + Configuration mConfiguration; + // Whether the stream has been started and not stopped. As opposed to + // allocated and not deallocated, which controls the presence of this stream + // altogether. + bool mStarted{false}; + // The timestamp of the last frame sent to mParent for this stream. + media::TimeUnit mLastFrameTime{media::TimeUnit::FromNegativeInfinity()}; + }; + // The video capture thread is where all access to this class must happen. + const nsCOMPtr<nsISerialEventTarget> mVideoCaptureThread; + // The identifier for which VideoEngine instance we are using, i.e. which type + // of source we're associated with. const CaptureEngine mCapEngine; - const uint32_t mStreamId; + // The (singleton from sEngines) VideoEngine instance that mCaptureId is valid + // in. + const RefPtr<VideoEngine> mEngine; + // The unique ID string of the associated device. + const nsCString mUniqueId; + // The id that identifies the capturer instance of the associated source + // device in VideoEngine. + const int mCaptureId; + // Tracking ID of the capturer for profiler markers. const TrackingId mTrackingId; - CamerasParent* const mParent; + // The (immutable) list of capabilities offered by the associated source + // device. + const nsTArray<webrtc::VideoCaptureCapability> mCapabilities; + // The list of streams that have been requested from all CamerasParent + // instances for the associated source device. + DataMutex<nsTArray<std::unique_ptr<Stream>>> mStreams; + + private: + AggregateCapturer(nsISerialEventTarget* aVideoCaptureThread, + CaptureEngine aCapEng, VideoEngine* aEngine, + const nsCString& aUniqueId, int aCaptureId, + nsTArray<webrtc::VideoCaptureCapability>&& aCapabilities); + MediaEventListener mCaptureEndedListener; - bool mConnectedToCaptureEnded = false; - DataMutex<Configuration> mConfiguration; - // Capture thread only. - media::TimeUnit mLastFrameTime = media::TimeUnit::FromNegativeInfinity(); }; class DeliverFrameRunnable; @@ -103,7 +173,7 @@ class CamerasParent final : public PCamerasParent { const CaptureEngine& aCapEngine, const nsACString& aUniqueIdUTF8, const uint64_t& aWindowID) override; mozilla::ipc::IPCResult RecvReleaseCapture(const CaptureEngine& aCapEngine, - const int& aCaptureId) override; + const int& aStreamId) override; mozilla::ipc::IPCResult RecvNumberOfCaptureDevices( const CaptureEngine& aCapEngine) override; mozilla::ipc::IPCResult RecvNumberOfCapabilities( @@ -114,20 +184,21 @@ class CamerasParent final : public PCamerasParent { mozilla::ipc::IPCResult RecvGetCaptureDevice( const CaptureEngine& aCapEngine, const int& aDeviceIndex) override; mozilla::ipc::IPCResult RecvStartCapture( - const CaptureEngine& aCapEngine, const int& aCaptureId, + const CaptureEngine& aCapEngine, const int& aStreamId, const VideoCaptureCapability& aIpcCaps, const NormalizedConstraints& aConstraints, const dom::VideoResizeModeEnum& aResizeMode) override; mozilla::ipc::IPCResult RecvFocusOnSelectedSource( - const CaptureEngine& aCapEngine, const int& aCaptureId) override; + const CaptureEngine& aCapEngine, const int& aStreamId) override; mozilla::ipc::IPCResult RecvStopCapture(const CaptureEngine& aCapEngine, - const int& aCaptureId) override; + const int& aStreamId) override; mozilla::ipc::IPCResult RecvReleaseFrame( - mozilla::ipc::Shmem&& aShmem) override; + const int& aCaptureId, mozilla::ipc::Shmem&& aShmem) override; void ActorDestroy(ActorDestroyReason aWhy) override; mozilla::ipc::IPCResult RecvEnsureInitialized( const CaptureEngine& aCapEngine) override; + bool IsWindowCapturing(uint64_t aWindowId, const nsACString& aUniqueId) const; nsIEventTarget* GetBackgroundEventTarget() { return mPBackgroundEventTarget; }; @@ -136,12 +207,13 @@ class CamerasParent final : public PCamerasParent { MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread()); return mDestroyed; }; - ShmemBuffer GetBuffer(size_t aSize); + ShmemBuffer GetBuffer(int aCaptureId, size_t aSize); // helper to forward to the PBackground thread - int DeliverFrameOverIPC(CaptureEngine aCapEngine, uint32_t aStreamId, - const TrackingId& aTrackingId, ShmemBuffer aBuffer, - unsigned char* aAltBuffer, + int DeliverFrameOverIPC(CaptureEngine aCapEngine, int aCaptureId, + const Span<const int>& aStreamId, + const TrackingId& aTrackingId, + Variant<ShmemBuffer, webrtc::VideoFrame>&& aBuffer, const VideoFrameProperties& aProps); CamerasParent(); @@ -149,9 +221,18 @@ class CamerasParent final : public PCamerasParent { private: virtual ~CamerasParent(); - // We use these helpers for shutdown and for the respective IPC commands. - void StopCapture(const CaptureEngine& aCapEngine, int aCaptureId); - int ReleaseCapture(const CaptureEngine& aCapEngine, int aCaptureId); + struct GetOrCreateCapturerResult { + AggregateCapturer* mCapturer{}; + int mStreamId{}; + }; + GetOrCreateCapturerResult GetOrCreateCapturer( + CaptureEngine aEngine, uint64_t aWindowId, const nsCString& aUniqueId, + nsTArray<webrtc::VideoCaptureCapability>&& aCapabilities); + AggregateCapturer* GetCapturer(CaptureEngine aEngine, int aStreamId); + int ReleaseStream(CaptureEngine aEngine, int aStreamId); + + nsTArray<webrtc::VideoCaptureCapability> const* EnsureCapabilitiesPopulated( + CaptureEngine aEngine, const nsCString& aUniqueId); void OnDeviceChange(); @@ -168,7 +249,6 @@ class CamerasParent final : public PCamerasParent { void OnShutdown(); - nsTArray<UniquePtr<CallbackHelper>> mCallbacks; // If existent, blocks xpcom shutdown while alive. // Note that this makes a reference cycle that gets broken in ActorDestroy(). const UniquePtr<media::ShutdownBlockingTicket> mShutdownBlocker; @@ -181,12 +261,24 @@ class CamerasParent final : public PCamerasParent { // Reference to same VideoEngineArray as sEngines. Video capture thread only. const RefPtr<VideoEngineArray> mEngines; + // Reference to same array of AggregateCapturers as sCapturers. There is one + // AggregateCapturer per allocated video source. It tracks the mapping from + // source to streamIds and CamerasParent instances. Video capture thread only. + const RefPtr< + media::Refcountable<nsTArray<std::unique_ptr<AggregateCapturer>>>> + mCapturers; + // Reference to same VideoCaptureFactory as sVideoCaptureFactory. Video // capture thread only. const RefPtr<VideoCaptureFactory> mVideoCaptureFactory; - // image buffers - ShmemPool mShmemPool; + // Image buffers. One pool per CamerasParent instance and capture id (i.e. + // unique source). Multiple CamerasParent instances capturing the same source + // need distinct ShmemPools as ShmemBuffers are tied to the IPC channel. + // Access is on the PBackground thread for mutations and + // allocating shmem buffers, and on the callback thread (varies by capture + // backend) for querying an existing pool for an available buffer. + DataMutex<std::map<int, ShmemPool>> mShmemPools; // PBackgroundParent thread const nsCOMPtr<nsISerialEventTarget> mPBackgroundEventTarget; @@ -194,7 +286,7 @@ class CamerasParent final : public PCamerasParent { // Set to true in ActorDestroy. PBackground only. bool mDestroyed; - std::map<nsCString, std::map<uint32_t, webrtc::VideoCaptureCapability>> + std::map<nsCString, nsTArray<webrtc::VideoCaptureCapability>> mAllCandidateCapabilities; // Listener for the camera VideoEngine::DeviceChangeEvent(). Video capture diff --git a/dom/media/systemservices/PCameras.ipdl b/dom/media/systemservices/PCameras.ipdl @@ -61,13 +61,13 @@ async protocol PCameras manager PBackground; child: - async CaptureEnded(int streamId); + async CaptureEnded(int[] streamIds); // transfers ownership of |buffer| from parent to child - async DeliverFrame(int streamId, Shmem buffer, VideoFrameProperties props); + async DeliverFrame(int captureId, int[] streamIds, Shmem buffer, VideoFrameProperties props); async DeviceChange(); async ReplyNumberOfCaptureDevices(int deviceCount); async ReplyNumberOfCapabilities(int capabilityCount); - async ReplyAllocateCapture(int captureId); + async ReplyAllocateCapture(int streamId); async ReplyGetCaptureCapability(VideoCaptureCapability cap); async ReplyGetCaptureDevice(nsCString device_name, nsCString device_id, bool scary); async ReplyFailure(); @@ -84,15 +84,15 @@ parent: async AllocateCapture(CaptureEngine engine, nsCString unique_idUTF8, uint64_t windowID); - async ReleaseCapture(CaptureEngine engine, int captureId); - async StartCapture(CaptureEngine engine, int captureId, + async ReleaseCapture(CaptureEngine engine, int streamId); + async StartCapture(CaptureEngine engine, int streamId, VideoCaptureCapability capability, NormalizedConstraints constraints, VideoResizeModeEnum resizeMode); - async FocusOnSelectedSource(CaptureEngine engine, int captureId); - async StopCapture(CaptureEngine engine, int captureId); + async FocusOnSelectedSource(CaptureEngine engine, int streamId); + async StopCapture(CaptureEngine engine, int streamId); // transfers frame back - async ReleaseFrame(Shmem s); + async ReleaseFrame(int captureId, Shmem s); // setup camera engine async EnsureInitialized(CaptureEngine engine); diff --git a/dom/media/systemservices/VideoEngine.cpp b/dom/media/systemservices/VideoEngine.cpp @@ -46,19 +46,21 @@ int VideoEngine::SetAndroidObjects() { } #endif -int32_t VideoEngine::CreateVideoCapture(const char* aDeviceUniqueIdUTF8) { +int32_t VideoEngine::CreateVideoCapture(const char* aDeviceUniqueIdUTF8, + uint64_t aWindowID) { LOG(("%s", __PRETTY_FUNCTION__)); MOZ_ASSERT(aDeviceUniqueIdUTF8); int32_t id = GenerateId(); LOG(("CaptureDeviceType=%s id=%d", EnumValueToString(mCaptureDevType), id)); - for (auto& it : mCaps) { + for (auto& it : mSharedCapturers) { if (it.second.VideoCapture() && it.second.VideoCapture()->CurrentDeviceName() && strcmp(it.second.VideoCapture()->CurrentDeviceName(), aDeviceUniqueIdUTF8) == 0) { - mIdMap.emplace(id, it.first); + mIdToCapturerMap.emplace(id, CaptureHandle{.mCaptureEntryNum = it.first, + .mWindowID = aWindowID}); return id; } } @@ -71,8 +73,9 @@ int32_t VideoEngine::CreateVideoCapture(const char* aDeviceUniqueIdUTF8) { entry = CaptureEntry(id, std::move(capturer.mCapturer), capturer.mDesktopImpl); - mCaps.emplace(id, std::move(entry)); - mIdMap.emplace(id, id); + mSharedCapturers.emplace(id, std::move(entry)); + mIdToCapturerMap.emplace( + id, CaptureHandle{.mCaptureEntryNum = id, .mWindowID = aWindowID}); return id; } @@ -81,14 +84,15 @@ int VideoEngine::ReleaseVideoCapture(const int32_t aId) { #ifdef DEBUG { - auto it = mIdMap.find(aId); - MOZ_ASSERT(it != mIdMap.end()); + auto it = mIdToCapturerMap.find(aId); + MOZ_ASSERT(it != mIdToCapturerMap.end()); (void)it; } #endif - for (auto& it : mIdMap) { - if (it.first != aId && it.second == mIdMap[aId]) { + for (auto& it : mIdToCapturerMap) { + if (it.first != aId && + it.second.mCaptureEntryNum == mIdToCapturerMap[aId].mCaptureEntryNum) { // There are other tracks still using this hardware. found = true; } @@ -101,13 +105,13 @@ int VideoEngine::ReleaseVideoCapture(const int32_t aId) { }); MOZ_ASSERT(found); if (found) { - auto it = mCaps.find(mIdMap[aId]); - MOZ_ASSERT(it != mCaps.end()); - mCaps.erase(it); + auto it = mSharedCapturers.find(mIdToCapturerMap[aId].mCaptureEntryNum); + MOZ_ASSERT(it != mSharedCapturers.end()); + mSharedCapturers.erase(it); } } - mIdMap.erase(aId); + mIdToCapturerMap.erase(aId); return found ? 0 : (-1); } @@ -216,21 +220,44 @@ bool VideoEngine::WithEntry( const std::function<void(CaptureEntry& entry)>&& fn) { #ifdef DEBUG { - auto it = mIdMap.find(entryCapnum); - MOZ_ASSERT(it != mIdMap.end()); + auto it = mIdToCapturerMap.find(entryCapnum); + MOZ_ASSERT(it != mIdToCapturerMap.end()); (void)it; } #endif - auto it = mCaps.find(mIdMap[entryCapnum]); - MOZ_ASSERT(it != mCaps.end()); - if (it == mCaps.end()) { + auto it = + mSharedCapturers.find(mIdToCapturerMap[entryCapnum].mCaptureEntryNum); + MOZ_ASSERT(it != mSharedCapturers.end()); + if (it == mSharedCapturers.end()) { return false; } fn(it->second); return true; } +bool VideoEngine::IsWindowCapturing(uint64_t aWindowID, + const nsCString& aUniqueIdUTF8) { + Maybe<int32_t> sharedId; + for (auto& [id, entry] : mSharedCapturers) { + if (entry.VideoCapture() && entry.VideoCapture()->CurrentDeviceName() && + strcmp(entry.VideoCapture()->CurrentDeviceName(), + aUniqueIdUTF8.get()) == 0) { + sharedId = Some(id); + break; + } + } + if (!sharedId) { + return false; + } + for (auto& [id, handle] : mIdToCapturerMap) { + if (handle.mCaptureEntryNum == *sharedId && handle.mWindowID == aWindowID) { + return true; + } + } + return false; +} + int32_t VideoEngine::GenerateId() { // XXX Something better than this (a map perhaps, or a simple boolean TArray, // given the number in-use is O(1) normally!) @@ -253,8 +280,8 @@ VideoEngine::VideoEngine(const CaptureDeviceType& aCaptureDeviceType, } VideoEngine::~VideoEngine() { - MOZ_ASSERT(mCaps.empty()); - MOZ_ASSERT(mIdMap.empty()); + MOZ_ASSERT(mSharedCapturers.empty()); + MOZ_ASSERT(mIdToCapturerMap.empty()); } } // namespace mozilla::camera diff --git a/dom/media/systemservices/VideoEngine.h b/dom/media/systemservices/VideoEngine.h @@ -44,9 +44,11 @@ class VideoEngine : public webrtc::VideoInputFeedBack { #if defined(ANDROID) static int SetAndroidObjects(); #endif + int32_t GenerateId(); /** Returns a non-negative capture identifier or -1 on failure. */ - int32_t CreateVideoCapture(const char* aDeviceUniqueIdUTF8); + int32_t CreateVideoCapture(const char* aDeviceUniqueIdUTF8, + uint64_t aWindowID); int ReleaseVideoCapture(const int32_t aId); @@ -87,10 +89,17 @@ class VideoEngine : public webrtc::VideoInputFeedBack { friend class VideoEngine; }; + struct CaptureHandle { + int32_t mCaptureEntryNum{}; + uint64_t mWindowID{}; + }; + // Returns true iff an entry for capnum exists bool WithEntry(const int32_t entryCapnum, const std::function<void(CaptureEntry& entry)>&& fn); + bool IsWindowCapturing(uint64_t aWindowID, const nsCString& aUniqueIdUTF8); + void OnDeviceChange() override; MediaEventSource<void>& DeviceChangeEvent() { return mDeviceChangeEvent; } @@ -102,12 +111,11 @@ class VideoEngine : public webrtc::VideoInputFeedBack { const CaptureDeviceType mCaptureDevType; const RefPtr<VideoCaptureFactory> mVideoCaptureFactory; std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> mDeviceInfo; - std::map<int32_t, CaptureEntry> mCaps; - std::map<int32_t, int32_t> mIdMap; + std::map<int32_t, CaptureEntry> mSharedCapturers; + std::map<int32_t, CaptureHandle> mIdToCapturerMap; MediaEventProducer<void> mDeviceChangeEvent; // The validity period for non-camera capture device infos` webrtc::Timestamp mExpiryTime = webrtc::Timestamp::Micros(0); - int32_t GenerateId(); }; } // namespace mozilla::camera #endif diff --git a/dom/media/webrtc/MediaEngine.h b/dom/media/webrtc/MediaEngine.h @@ -49,6 +49,13 @@ class MediaEngine { virtual RefPtr<MediaEngineSource> CreateSource( const MediaDevice* aDevice) = 0; + /** + * Like CreateSource but in addition copies over capabilities and settings + * from another source. + */ + virtual RefPtr<MediaEngineSource> CreateSourceFrom( + const MediaEngineSource* aSource, const MediaDevice* aDevice) = 0; + virtual MediaEventSource<void>& DeviceListChangeEvent() = 0; /** * Return true if devices returned from EnumerateDevices are emulated media diff --git a/dom/media/webrtc/MediaEngineFake.cpp b/dom/media/webrtc/MediaEngineFake.cpp @@ -86,6 +86,9 @@ class MediaEngineFakeVideoSource : public MediaEngineSource { public: MediaEngineFakeVideoSource(); + static already_AddRefed<MediaEngineFakeVideoSource> CreateFrom( + const MediaEngineFakeVideoSource* aSource); + static nsString GetGroupId(); nsresult Allocate(const dom::MediaTrackConstraints& aConstraints, @@ -143,6 +146,15 @@ MediaEngineFakeVideoSource::MediaEngineFakeVideoSource() dom::GetEnumString(dom::VideoResizeModeEnum::None))); } +/*static*/ already_AddRefed<MediaEngineFakeVideoSource> +MediaEngineFakeVideoSource::CreateFrom( + const MediaEngineFakeVideoSource* aSource) { + auto src = MakeRefPtr<MediaEngineFakeVideoSource>(); + *static_cast<MediaTrackSettings*>(src->mSettings) = *aSource->mSettings; + src->mOpts = aSource->mOpts; + return src.forget(); +} + nsString MediaEngineFakeVideoSource::GetGroupId() { return u"Fake Video Group"_ns; } @@ -616,4 +628,20 @@ RefPtr<MediaEngineSource> MediaEngineFake::CreateSource( } } +RefPtr<MediaEngineSource> MediaEngineFake::CreateSourceFrom( + const MediaEngineSource* aSource, const MediaDevice* aMediaDevice) { + MOZ_ASSERT(aMediaDevice->mEngine == this); + switch (aMediaDevice->mMediaSource) { + case MediaSourceEnum::Camera: + return MediaEngineFakeVideoSource::CreateFrom( + static_cast<const MediaEngineFakeVideoSource*>(aSource)); + case MediaSourceEnum::Microphone: + // No main thread members that need to be deep cloned. + return new MediaEngineFakeAudioSource(); + default: + MOZ_ASSERT_UNREACHABLE("Unsupported source type"); + return nullptr; + } +} + } // namespace mozilla diff --git a/dom/media/webrtc/MediaEngineFake.h b/dom/media/webrtc/MediaEngineFake.h @@ -22,6 +22,8 @@ class MediaEngineFake : public MediaEngine { nsTArray<RefPtr<MediaDevice>>*) override; void Shutdown() override {} RefPtr<MediaEngineSource> CreateSource(const MediaDevice* aDevice) override; + RefPtr<MediaEngineSource> CreateSourceFrom( + const MediaEngineSource* aSource, const MediaDevice* aDevice) override; MediaEventSource<void>& DeviceListChangeEvent() override { return mDeviceListChangeEvent; diff --git a/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp b/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp @@ -203,6 +203,22 @@ MediaEngineRemoteVideoSource::MediaEngineRemoteVideoSource( } } +/*static*/ +already_AddRefed<MediaEngineRemoteVideoSource> +MediaEngineRemoteVideoSource::CreateFrom( + const MediaEngineRemoteVideoSource* aSource, + const MediaDevice* aMediaDevice) { + auto src = MakeRefPtr<MediaEngineRemoteVideoSource>(aMediaDevice); + *static_cast<MediaTrackSettings*>(src->mSettings) = *aSource->mSettings; + *static_cast<MediaTrackCapabilities*>(src->mTrackCapabilities) = + *aSource->mTrackCapabilities; + { + MutexAutoLock lock(aSource->mMutex); + src->mIncomingImageSize = aSource->mIncomingImageSize; + } + return src.forget(); +} + MediaEngineRemoteVideoSource::~MediaEngineRemoteVideoSource() { mFirstFramePromiseHolder.RejectIfExists(NS_ERROR_ABORT, __func__); } @@ -283,8 +299,8 @@ nsresult MediaEngineRemoteVideoSource::Allocate( .mCapabilityWidth = cw ? Some(cw) : Nothing(), .mCapabilityHeight = ch ? Some(ch) : Nothing(), .mCapEngine = mCapEngine, - .mInputWidth = cw, - .mInputHeight = ch, + .mInputWidth = cw ? cw : mIncomingImageSize.width, + .mInputHeight = ch ? ch : mIncomingImageSize.height, .mRotation = 0, }; framerate = input.mCanCropAndScale.valueOr(false) @@ -353,12 +369,22 @@ nsresult MediaEngineRemoteVideoSource::Deallocate() { LOG("Video device %d deallocated", mCaptureId); - if (camera::GetChildAndCall(&camera::CamerasChild::ReleaseCapture, mCapEngine, - mCaptureId)) { - // Failure can occur when the parent process is shutting down. - return NS_ERROR_FAILURE; + int error = camera::GetChildAndCall(&camera::CamerasChild::ReleaseCapture, + mCapEngine, mCaptureId); + + if (error == camera::kSuccess) { + return NS_OK; } - return NS_OK; + + if (error == camera::kIpcError) { + // Failure can occur when the parent process is shutting down, and the IPC + // channel is down. We still consider the capturer deallocated in this + // case, since it cannot deliver frames without the IPC channel open. + return NS_OK; + } + + MOZ_ASSERT(error == camera::kError); + return NS_ERROR_FAILURE; } void MediaEngineRemoteVideoSource::SetTrack(const RefPtr<MediaTrack>& aTrack, @@ -452,9 +478,11 @@ nsresult MediaEngineRemoteVideoSource::Stop() { MOZ_ASSERT(mState == kStarted); - if (camera::GetChildAndCall(&camera::CamerasChild::StopCapture, mCapEngine, - mCaptureId)) { - // Failure can occur when the parent process is shutting down. + int error = camera::GetChildAndCall(&camera::CamerasChild::StopCapture, + mCapEngine, mCaptureId); + + if (error == camera::kError) { + // CamerasParent replied with error. The capturer is still running. return NS_ERROR_FAILURE; } @@ -463,7 +491,15 @@ nsresult MediaEngineRemoteVideoSource::Stop() { mState = kStopped; } - return NS_OK; + if (error == camera::kSuccess) { + return NS_OK; + } + + MOZ_ASSERT(error == camera::kIpcError); + // Failure can occur when the parent process is shutting down, and the IPC + // channel is down. We still consider the capturer stopped in this case, + // since it cannot deliver frames without the IPC channel open. + return NS_ERROR_FAILURE; } nsresult MediaEngineRemoteVideoSource::Reconfigure( diff --git a/dom/media/webrtc/MediaEngineRemoteVideoSource.h b/dom/media/webrtc/MediaEngineRemoteVideoSource.h @@ -88,6 +88,10 @@ class MediaEngineRemoteVideoSource : public MediaEngineSource, public: explicit MediaEngineRemoteVideoSource(const MediaDevice* aMediaDevice); + static already_AddRefed<MediaEngineRemoteVideoSource> CreateFrom( + const MediaEngineRemoteVideoSource* aSource, + const MediaDevice* aMediaDevice); + // ExternalRenderer /** * Signals that the capture stream has ended @@ -161,7 +165,7 @@ class MediaEngineRemoteVideoSource : public MediaEngineSource, // mMutex protects certain members on 3 threads: // MediaManager, Cameras IPC and MediaTrackGraph. - Mutex mMutex MOZ_UNANNOTATED; + mutable Mutex mMutex MOZ_UNANNOTATED; // Current state of this source. // Set under mMutex on the owning thread. Accessed under one of the two. diff --git a/dom/media/webrtc/MediaEngineWebRTC.cpp b/dom/media/webrtc/MediaEngineWebRTC.cpp @@ -299,6 +299,25 @@ RefPtr<MediaEngineSource> MediaEngineWebRTC::CreateSource( } } +RefPtr<MediaEngineSource> MediaEngineWebRTC::CreateSourceFrom( + const MediaEngineSource* aSource, const MediaDevice* aMediaDevice) { + MOZ_ASSERT(aMediaDevice->mEngine == this); + if (MediaEngineSource::IsVideo(aMediaDevice->mMediaSource)) { + return MediaEngineRemoteVideoSource::CreateFrom( + static_cast<const MediaEngineRemoteVideoSource*>(aSource), + aMediaDevice); + } + switch (aMediaDevice->mMediaSource) { + case MediaSourceEnum::Microphone: + return MediaEngineWebRTCMicrophoneSource::CreateFrom( + static_cast<const MediaEngineWebRTCMicrophoneSource*>(aSource), + aMediaDevice); + default: + MOZ_CRASH("Unsupported source type"); + return nullptr; + } +} + void MediaEngineWebRTC::Shutdown() { AssertIsOnOwningThread(); mCameraListChangeListener.DisconnectIfExists(); diff --git a/dom/media/webrtc/MediaEngineWebRTC.h b/dom/media/webrtc/MediaEngineWebRTC.h @@ -27,6 +27,8 @@ class MediaEngineWebRTC : public MediaEngine { void EnumerateDevices(dom::MediaSourceEnum, MediaSinkEnum, nsTArray<RefPtr<MediaDevice>>*) override; RefPtr<MediaEngineSource> CreateSource(const MediaDevice* aDevice) override; + RefPtr<MediaEngineSource> CreateSourceFrom(const MediaEngineSource* aSource, + const MediaDevice*) override; MediaEventSource<void>& DeviceListChangeEvent() override { return mDeviceListChangeEvent; diff --git a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp @@ -113,6 +113,17 @@ MediaEngineWebRTCMicrophoneSource::MediaEngineWebRTCMicrophoneSource( })); } +/*static*/ already_AddRefed<MediaEngineWebRTCMicrophoneSource> +MediaEngineWebRTCMicrophoneSource::CreateFrom( + const MediaEngineWebRTCMicrophoneSource* aSource, + const MediaDevice* aMediaDevice) { + auto src = MakeRefPtr<MediaEngineWebRTCMicrophoneSource>(aMediaDevice); + *static_cast<dom::MediaTrackSettings*>(src->mSettings) = *aSource->mSettings; + *static_cast<dom::MediaTrackCapabilities*>(src->mCapabilities) = + *aSource->mCapabilities; + return src.forget(); +} + nsresult MediaEngineWebRTCMicrophoneSource::EvaluateSettings( const NormalizedConstraints& aConstraintsUpdate, const MediaEnginePrefs& aInPrefs, MediaEnginePrefs* aOutPrefs, diff --git a/dom/media/webrtc/MediaEngineWebRTCAudio.h b/dom/media/webrtc/MediaEngineWebRTCAudio.h @@ -34,6 +34,10 @@ class MediaEngineWebRTCMicrophoneSource : public MediaEngineSource { public: explicit MediaEngineWebRTCMicrophoneSource(const MediaDevice* aMediaDevice); + static already_AddRefed<MediaEngineWebRTCMicrophoneSource> CreateFrom( + const MediaEngineWebRTCMicrophoneSource* aSource, + const MediaDevice* aMediaDevice); + nsresult Allocate(const dom::MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs, uint64_t aWindowID, const char** aOutBadConstraint) override; @@ -314,9 +318,13 @@ class AudioProcessingTrack : public DeviceInputConsumerTrack { void DestroyImpl() override; void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override; uint32_t NumberOfChannels() const override { - MOZ_DIAGNOSTIC_ASSERT( - mInputProcessing, - "Must set mInputProcessing before exposing to content"); + if (!mInputProcessing) { + // There's an async gap between adding the track to the graph + // (AudioProcessingTrack::Create) and setting mInputProcessing + // (SetInputProcessing on the media manager thread). + // Return 0 to indicate the default within this gap. + return 0; + } return mInputProcessing->GetRequestedInputChannelCount(); } // Pass the graph's mixed audio output to mInputProcessing for processing as diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html @@ -22,6 +22,10 @@ let streamUntilEnded; const tracks = []; runTest(async () => { try { + await pushPrefs( + ["media.getusermedia.camera.fake.force", true], + ["media.video_loopback_dev", ""], + ); let stream = await getUserMedia({audio: true, video: true}); // We need to test with multiple tracks. We add an extra of each kind. for (const track of stream.getTracks()) { diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html @@ -35,6 +35,10 @@ }); runTest(() => Promise.resolve() + .then(() => pushPrefs( + ["media.getusermedia.camera.fake.force", true], + ["media.video_loopback_dev", ""], + )) .then(() => testSingleTrackClonePlayback({audio: true})) .then(() => testSingleTrackClonePlayback({video: true})) .then(() => getUserMedia({video: true})).then(stream => { diff --git a/dom/midi/midir_impl/src/lib.rs b/dom/midi/midir_impl/src/lib.rs @@ -14,18 +14,6 @@ use uuid::Uuid; * You can obtain one at http://mozilla.org/MPL/2.0/. */ extern crate midir; -#[cfg(target_os = "windows")] -#[repr(C)] -#[derive(Clone, Copy)] -pub struct GeckoTimeStamp { - gtc: u64, - qpc: u64, - - is_null: u8, - has_qpc: u8, -} - -#[cfg(not(target_os = "windows"))] #[repr(C)] #[derive(Clone, Copy)] pub struct GeckoTimeStamp { diff --git a/dom/performance/Performance.cpp b/dom/performance/Performance.cpp @@ -708,8 +708,8 @@ void Performance::MaybeEmitExternalProfilerMarker( uint64_t rawStart = startTimeStamp.RawClockMonotonicNanosecondsSinceBoot(); uint64_t rawEnd = endTimeStamp.RawClockMonotonicNanosecondsSinceBoot(); #elif XP_WIN - uint64_t rawStart = startTimeStamp.RawQueryPerformanceCounterValue().value(); - uint64_t rawEnd = endTimeStamp.RawQueryPerformanceCounterValue().value(); + uint64_t rawStart = startTimeStamp.RawQueryPerformanceCounterValue(); + uint64_t rawEnd = endTimeStamp.RawQueryPerformanceCounterValue(); #elif XP_MACOSX uint64_t rawStart = startTimeStamp.RawMachAbsoluteTimeNanoseconds(); uint64_t rawEnd = endTimeStamp.RawMachAbsoluteTimeNanoseconds(); diff --git a/dom/quota/ActorsParent.cpp b/dom/quota/ActorsParent.cpp @@ -7969,7 +7969,8 @@ QuotaManager::GetOriginInfosExceedingGlobalLimit() const { } QuotaManager::OriginInfosNestedTraversable -QuotaManager::GetOriginInfosWithZeroUsage() const { +QuotaManager::GetOriginInfosWithZeroUsage( + const Maybe<int64_t>& aCutoffAccessTime) const { MutexAutoLock lock(mQuotaMutex); QuotaManager::OriginInfosNestedTraversable res; @@ -7984,7 +7985,8 @@ QuotaManager::GetOriginInfosWithZeroUsage() const { MOZ_ASSERT(!entry.GetKey().IsEmpty()); MOZ_ASSERT(pair); - pair->MaybeInsertNonPersistedZeroUsageOriginInfos(inserter); + pair->MaybeInsertNonPersistedZeroUsageOriginInfos(inserter, + aCutoffAccessTime); } res.AppendElement(std::move(originInfos)); diff --git a/dom/quota/GroupInfoPair.h b/dom/quota/GroupInfoPair.h @@ -73,8 +73,12 @@ class GroupInfoPair { // Inserts non-persisted origins that also have zero quota-charged usage. // Used by cleanup routines to identify candidate origins for removal. + // + // See QuotaManager::GetOriginInfosWithZeroUsage for the semantics and time + // units of |aCutoffAccessTime|. template <typename Iterator> - void MaybeInsertNonPersistedZeroUsageOriginInfos(Iterator aDest) const; + void MaybeInsertNonPersistedZeroUsageOriginInfos( + Iterator aDest, const Maybe<int64_t>& aCutoffAccessTime) const; private: RefPtr<GroupInfo>& GetGroupInfoForPersistenceType( diff --git a/dom/quota/GroupInfoPairImpl.h b/dom/quota/GroupInfoPairImpl.h @@ -42,9 +42,11 @@ void GroupInfoPair::MaybeInsertNonPersistedOriginInfos(Iterator aDest) const { template <typename Iterator> void GroupInfoPair::MaybeInsertNonPersistedZeroUsageOriginInfos( - Iterator aDest) const { - MaybeInsertOriginInfos(aDest, [](const auto& originInfo) { - return !originInfo->LockedPersisted() && originInfo->LockedUsage() == 0; + Iterator aDest, const Maybe<int64_t>& aCutoffAccessTime) const { + MaybeInsertOriginInfos(aDest, [aCutoffAccessTime](const auto& originInfo) { + return !originInfo->LockedPersisted() && originInfo->LockedUsage() == 0 && + (!aCutoffAccessTime || + originInfo->LockedAccessTime() < *aCutoffAccessTime); }); } diff --git a/dom/quota/QuotaManager.h b/dom/quota/QuotaManager.h @@ -860,7 +860,19 @@ class QuotaManager final : public BackgroundThreadObject { OriginInfosNestedTraversable GetOriginInfosExceedingGlobalLimit() const; - OriginInfosNestedTraversable GetOriginInfosWithZeroUsage() const; + // Returns origins with zero usage. If aCutoffAccessTime is provided, origins + // whose last access time is newer than the cutoff are excluded. + // + // The cutoff time is expressed as an int64_t value in microseconds since the + // Unix epoch (1970-01-01 00:00:00 UTC), matching the format returned by + // PR_Now(). This is the same time unit used throughout Quota Manager for + // access and modification timestamps. + // + // Typically callers compute it as: + // const int64_t cutoff = PR_Now() - (N * PR_USEC_PER_SEC); + // where N is the desired age threshold in seconds (for example, one week). + OriginInfosNestedTraversable GetOriginInfosWithZeroUsage( + const Maybe<int64_t>& aCutoffAccessTime = Nothing()) const; /** * Clears the given set of origins. diff --git a/dom/script/ScriptLoader.cpp b/dom/script/ScriptLoader.cpp @@ -3347,7 +3347,8 @@ void ScriptLoader::TryCacheRequest(ScriptLoadRequest* aRequest) { } /* static */ -nsCString& ScriptLoader::BytecodeMimeTypeFor(ScriptLoadRequest* aRequest) { +nsCString& ScriptLoader::BytecodeMimeTypeFor( + const ScriptLoadRequest* aRequest) { if (aRequest->IsModuleRequest()) { return nsContentUtils::JSModuleBytecodeMimeType(); } @@ -3356,7 +3357,7 @@ nsCString& ScriptLoader::BytecodeMimeTypeFor(ScriptLoadRequest* aRequest) { /* static */ nsCString& ScriptLoader::BytecodeMimeTypeFor( - JS::loader::LoadedScript* aLoadedScript) { + const JS::loader::LoadedScript* aLoadedScript) { if (aLoadedScript->IsModuleScript()) { return nsContentUtils::JSModuleBytecodeMimeType(); } @@ -3614,6 +3615,7 @@ void ScriptLoader::MaybeUpdateDiskCache() { } void ScriptLoader::UpdateDiskCache() { + MOZ_ASSERT(!mCache); LOG(("ScriptLoader (%p): Start bytecode encoding.", this)); // If any script got added in the previous loop cycle, wait until all @@ -3635,20 +3637,31 @@ void ScriptLoader::UpdateDiskCache() { for (auto& loadedScript : mDiskCacheQueue) { // The bytecode encoding is performed only when there was no // bytecode stored in the necko cache. - // - // For in-memory cached case, the save might already be performed - // by other requests. - // See also ScriptLoader::MaybePrepareModuleForDiskCacheAfterExecute. - // - // TODO: Move this to SharedScriptCache. if (!loadedScript->HasDiskCacheReference()) { continue; } - EncodeBytecodeAndSave(fc, loadedScript); + MOZ_ASSERT(loadedScript->HasStencil()); + + Vector<uint8_t> compressed; + if (!EncodeAndCompress(fc, loadedScript, loadedScript->GetStencil(), + loadedScript->SRIAndBytecode(), compressed)) { + loadedScript->DropDiskCacheReference(); + loadedScript->DropBytecode(); + TRACE_FOR_TEST(loadedScript, "diskcache:failed"); + continue; + } + + if (!SaveToDiskCache(loadedScript, compressed)) { + loadedScript->DropDiskCacheReference(); + loadedScript->DropBytecode(); + TRACE_FOR_TEST(loadedScript, "diskcache:failed"); + continue; + } loadedScript->DropDiskCacheReference(); loadedScript->DropBytecode(); + TRACE_FOR_TEST(loadedScript, "diskcache:saved"); } mDiskCacheQueue.Clear(); @@ -3656,25 +3669,20 @@ void ScriptLoader::UpdateDiskCache() { } /* static */ -void ScriptLoader::EncodeBytecodeAndSave( - JS::FrontendContext* aFc, JS::loader::LoadedScript* aLoadedScript) { - MOZ_ASSERT(aLoadedScript->HasDiskCacheReference()); - MOZ_ASSERT(aLoadedScript->HasStencil()); - - auto bytecodeFailed = mozilla::MakeScopeExit( - [&]() { TRACE_FOR_TEST(aLoadedScript, "diskcache:failed"); }); - - size_t SRILength = aLoadedScript->SRIAndBytecode().length(); +bool ScriptLoader::EncodeAndCompress( + JS::FrontendContext* aFc, const JS::loader::LoadedScript* aLoadedScript, + JS::Stencil* aStencil, const JS::TranscodeBuffer& aSRI, + Vector<uint8_t>& aCompressed) { + size_t SRILength = aSRI.length(); MOZ_ASSERT(JS::IsTranscodingBytecodeOffsetAligned(SRILength)); JS::TranscodeBuffer SRIAndBytecode; - if (!SRIAndBytecode.appendAll(aLoadedScript->SRIAndBytecode())) { + if (!SRIAndBytecode.appendAll(aSRI)) { LOG(("LoadedScript (%p): Cannot allocate buffer", aLoadedScript)); - return; + return false; } - JS::TranscodeResult result = - JS::EncodeStencil(aFc, aLoadedScript->GetStencil(), SRIAndBytecode); + JS::TranscodeResult result = JS::EncodeStencil(aFc, aStencil, SRIAndBytecode); if (result != JS::TranscodeResult::Ok) { // Encoding can be aborted for non-supported syntax (e.g. asm.js), or @@ -3683,37 +3691,44 @@ void ScriptLoader::EncodeBytecodeAndSave( JS::ClearFrontendErrors(aFc); LOG(("LoadedScript (%p): Cannot serialize bytecode", aLoadedScript)); - return; + return false; } - Vector<uint8_t> compressedBytecode; // TODO probably need to move this to a helper thread - if (!ScriptBytecodeCompress(SRIAndBytecode, SRILength, compressedBytecode)) { - return; + if (!ScriptBytecodeCompress(SRIAndBytecode, SRILength, aCompressed)) { + return false; } - if (compressedBytecode.length() >= UINT32_MAX) { + if (aCompressed.length() >= UINT32_MAX) { LOG( ("LoadedScript (%p): Bytecode cache is too large to be decoded " "correctly.", aLoadedScript)); - return; + return false; } + return true; +} + +/* static */ +bool ScriptLoader::SaveToDiskCache( + const JS::loader::LoadedScript* aLoadedScript, + const Vector<uint8_t>& aCompressed) { + MOZ_ASSERT(NS_IsMainThread()); + // Open the output stream to the cache entry alternate data storage. This // might fail if the stream is already open by another request, in which // case, we just ignore the current one. nsCOMPtr<nsIAsyncOutputStream> output; nsresult rv = aLoadedScript->mCacheInfo->OpenAlternativeOutputStream( BytecodeMimeTypeFor(aLoadedScript), - static_cast<int64_t>(compressedBytecode.length()), - getter_AddRefs(output)); + static_cast<int64_t>(aCompressed.length()), getter_AddRefs(output)); if (NS_FAILED(rv)) { LOG( ("LoadedScript (%p): Cannot open bytecode cache (rv = %X, output " "= %p)", aLoadedScript, unsigned(rv), output.get())); - return; + return false; } MOZ_ASSERT(output); @@ -3723,20 +3738,18 @@ void ScriptLoader::EncodeBytecodeAndSave( }); uint32_t n; - rv = output->Write(reinterpret_cast<char*>(compressedBytecode.begin()), - compressedBytecode.length(), &n); + rv = output->Write(reinterpret_cast<const char*>(aCompressed.begin()), + aCompressed.length(), &n); LOG( ("LoadedScript (%p): Write bytecode cache (rv = %X, length = %u, " "written = %u)", - aLoadedScript, unsigned(rv), unsigned(compressedBytecode.length()), n)); + aLoadedScript, unsigned(rv), unsigned(aCompressed.length()), n)); if (NS_FAILED(rv)) { - return; + return false; } - MOZ_RELEASE_ASSERT(compressedBytecode.length() == n); - - bytecodeFailed.release(); - TRACE_FOR_TEST(aLoadedScript, "diskcache:saved"); + MOZ_RELEASE_ASSERT(aCompressed.length() == n); + return true; } void ScriptLoader::GiveUpDiskCaching() { diff --git a/dom/script/ScriptLoader.h b/dom/script/ScriptLoader.h @@ -709,9 +709,9 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface { JS::Handle<JS::Value> aDebuggerPrivateValue, JS::Handle<JSScript*> aDebuggerIntroductionScript, ErrorResult& aRv); - static nsCString& BytecodeMimeTypeFor(ScriptLoadRequest* aRequest); + static nsCString& BytecodeMimeTypeFor(const ScriptLoadRequest* aRequest); static nsCString& BytecodeMimeTypeFor( - JS::loader::LoadedScript* aLoadedScript); + const JS::loader::LoadedScript* aLoadedScript); // Queue the script load request for caching if we decided to cache it, or // cleanup the script load request fields otherwise. @@ -757,10 +757,21 @@ class ScriptLoader final : public JS::loader::ScriptLoaderInterface { public: /** - * Encode the stencils and save the bytecode to the necko cache. + * Encode the stencils and compress it. + * aLoadedScript is used only for logging purpose, in order to allow + * performing this off main thread. */ - static void EncodeBytecodeAndSave(JS::FrontendContext* aFc, - JS::loader::LoadedScript* aLoadedScript); + static bool EncodeAndCompress(JS::FrontendContext* aFc, + const JS::loader::LoadedScript* aLoadedScript, + JS::Stencil* aStencil, + const JS::TranscodeBuffer& aSRI, + Vector<uint8_t>& aCompressed); + + /** + * Save the bytecode to the necko cache. + */ + static bool SaveToDiskCache(const JS::loader::LoadedScript* aLoadedScript, + const Vector<uint8_t>& aCompressed); private: /** diff --git a/dom/script/SharedScriptCache.cpp b/dom/script/SharedScriptCache.cpp @@ -8,8 +8,10 @@ #include "ScriptLoadHandler.h" // ScriptLoadHandler #include "ScriptLoader.h" // ScriptLoader +#include "ScriptTrace.h" // TRACE_FOR_TEST #include "js/experimental/CompileScript.h" // JS::FrontendContext, JS::NewFrontendContext, JS::DestroyFrontendContext #include "mozilla/Maybe.h" // Maybe, Some, Nothing +#include "mozilla/TaskController.h" // TaskController, Task #include "mozilla/dom/ContentParent.h" // dom::ContentParent #include "nsIMemoryReporter.h" // nsIMemoryReporter, MOZ_DEFINE_MALLOC_SIZE_OF, RegisterWeakMemoryReporter, UnregisterWeakMemoryReporter, MOZ_COLLECT_REPORT, KIND_HEAP, UNITS_BYTES #include "nsIPrefBranch.h" // nsIPrefBranch, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID @@ -221,13 +223,54 @@ bool SharedScriptCache::MaybeScheduleUpdateDiskCache() { return true; } +class ScriptEncodeAndCompressionTask : public mozilla::Task { + public: + ScriptEncodeAndCompressionTask() + : Task(Kind::OffMainThreadOnly, EventQueuePriority::Idle) {} + virtual ~ScriptEncodeAndCompressionTask() = default; + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + bool GetName(nsACString& aName) override { + aName.AssignLiteral("ScriptEncodeAndCompressionTask"); + return true; + } +#endif + + TaskResult Run() override { + SharedScriptCache::Get()->EncodeAndCompress(); + return TaskResult::Complete; + } +}; + +class ScriptSaveTask : public mozilla::Task { + public: + ScriptSaveTask() : Task(Kind::MainThreadOnly, EventQueuePriority::Idle) {} + virtual ~ScriptSaveTask() = default; + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + bool GetName(nsACString& aName) override { + aName.AssignLiteral("ScriptSaveTask"); + return true; + } +#endif + + TaskResult Run() override { + SharedScriptCache::Get()->SaveToDiskCache(); + return TaskResult::Complete; + } +}; + void SharedScriptCache::UpdateDiskCache() { auto strategy = ScriptLoader::GetDiskCacheStrategy(); if (strategy.mIsDisabled) { return; } - JS::FrontendContext* fc = nullptr; + mozilla::MutexAutoLock lock(mEncodeMutex); + + if (!mEncodeItems.empty()) { + return; + } for (auto iter = mComplete.Iter(); !iter.Done(); iter.Next()) { JS::loader::LoadedScript* loadedScript = iter.Data().mResource; @@ -235,24 +278,69 @@ void SharedScriptCache::UpdateDiskCache() { continue; } - if (!fc) { - // Lazily create the context only when there's at least one script - // that needs to be saved. - fc = JS::NewFrontendContext(); - if (!fc) { - return; - } + if (!mEncodeItems.emplaceBack(loadedScript->GetStencil(), + std::move(loadedScript->SRIAndBytecode()), + loadedScript)) { + continue; } + } + + if (mEncodeItems.empty()) { + return; + } - ScriptLoader::EncodeBytecodeAndSave(fc, loadedScript); + RefPtr<ScriptEncodeAndCompressionTask> encodeTask = + new ScriptEncodeAndCompressionTask(); + RefPtr<ScriptSaveTask> saveTask = new ScriptSaveTask(); + saveTask->AddDependency(encodeTask); - loadedScript->DropDiskCacheReference(); - loadedScript->DropBytecode(); + TaskController::Get()->AddTask(encodeTask.forget()); + TaskController::Get()->AddTask(saveTask.forget()); +} + +void SharedScriptCache::EncodeAndCompress() { + JS::FrontendContext* fc = JS::NewFrontendContext(); + if (!fc) { + return; } - if (fc) { - JS::DestroyFrontendContext(fc); + mozilla::MutexAutoLock lock(mEncodeMutex); + + for (auto& item : mEncodeItems) { + if (!ScriptLoader::EncodeAndCompress(fc, item.mLoadedScript, item.mStencil, + item.mSRI, item.mCompressed)) { + item.mCompressed.clear(); + } } + + JS::DestroyFrontendContext(fc); +} + +void SharedScriptCache::SaveToDiskCache() { + MOZ_ASSERT(NS_IsMainThread()); + + mozilla::MutexAutoLock lock(mEncodeMutex); + + for (const auto& item : mEncodeItems) { + if (item.mCompressed.empty()) { + item.mLoadedScript->DropDiskCacheReference(); + item.mLoadedScript->DropBytecode(); + TRACE_FOR_TEST(item.mLoadedScript, "diskcache:failed"); + continue; + } + + if (!ScriptLoader::SaveToDiskCache(item.mLoadedScript, item.mCompressed)) { + item.mLoadedScript->DropDiskCacheReference(); + item.mLoadedScript->DropBytecode(); + TRACE_FOR_TEST(item.mLoadedScript, "diskcache:failed"); + } + + item.mLoadedScript->DropDiskCacheReference(); + item.mLoadedScript->DropBytecode(); + TRACE_FOR_TEST(item.mLoadedScript, "diskcache:saved"); + } + + mEncodeItems.clear(); } } // namespace mozilla::dom diff --git a/dom/script/SharedScriptCache.h b/dom/script/SharedScriptCache.h @@ -14,8 +14,10 @@ #include "js/loader/ScriptLoadRequest.h" // JS::loader::ScriptLoadRequest #include "mozilla/CORSMode.h" // mozilla::CORSMode #include "mozilla/MemoryReporting.h" // MallocSizeOf +#include "mozilla/Mutex.h" // Mutex, GUARDED_BY, MutexAutoLock #include "mozilla/RefPtr.h" // RefPtr #include "mozilla/SharedSubResourceCache.h" // SharedSubResourceCache, SharedSubResourceCacheLoadingValueBase, SubResourceNetworkMetadataHolder +#include "mozilla/ThreadSafety.h" // MOZ_GUARDED_BY #include "mozilla/WeakPtr.h" // SupportsWeakPtr #include "mozilla/dom/CacheExpirationTime.h" // CacheExpirationTime #include "mozilla/dom/SRIMetadata.h" // mozilla::dom::SRIMetadata @@ -196,6 +198,9 @@ class SharedScriptCache final bool MaybeScheduleUpdateDiskCache(); void UpdateDiskCache(); + void EncodeAndCompress(); + void SaveToDiskCache(); + // This has to be static because it's also called for loaders that don't have // a sheet cache (loaders that are not owned by a document). static void LoadCompleted(SharedScriptCache*, ScriptLoadData&); @@ -210,6 +215,28 @@ class SharedScriptCache final protected: ~SharedScriptCache(); + + private: + class EncodeItem { + public: + EncodeItem(JS::Stencil* aStencil, JS::TranscodeBuffer&& aSRI, + JS::loader::LoadedScript* aLoadedScript) + : mStencil(aStencil), + mSRI(std::move(aSRI)), + mLoadedScript(aLoadedScript) {} + + // These fields can be touched from multiple threads. + RefPtr<JS::Stencil> mStencil; + JS::TranscodeBuffer mSRI; + Vector<uint8_t> mCompressed; + + // This can be dereferenced only from the main thread. + // Reading the pointer itself is allowed also off main thread. + RefPtr<JS::loader::LoadedScript> mLoadedScript; + }; + + Mutex mEncodeMutex{"SharedScriptCache::mEncodeMutex"}; + Vector<EncodeItem> mEncodeItems MOZ_GUARDED_BY(mEncodeMutex); }; } // namespace dom diff --git a/dom/svg/SVGGeometryElement.cpp b/dom/svg/SVGGeometryElement.cpp @@ -16,6 +16,7 @@ #include "gfxPlatform.h" #include "mozilla/RefPtr.h" #include "mozilla/SVGContentUtils.h" +#include "mozilla/SVGUtils.h" #include "mozilla/dom/DOMPointBinding.h" #include "mozilla/dom/SVGLengthBinding.h" #include "mozilla/gfx/2D.h" @@ -238,24 +239,11 @@ already_AddRefed<DOMSVGPoint> SVGGeometryElement::GetPointAtLength( } gfx::Matrix SVGGeometryElement::LocalTransform() const { - gfx::Matrix result; nsIFrame* f = GetPrimaryFrame(); if (!f || !f->IsTransformed()) { - return result; + return {}; } - nsStyleTransformMatrix::TransformReferenceBox refBox(f); - const float a2css = AppUnitsPerCSSPixel(); - nsDisplayTransform::FrameTransformProperties props(f, refBox, a2css); - if (!props.HasTransform()) { - return result; - } - auto matrix = nsStyleTransformMatrix::ReadTransforms( - props.mTranslate, props.mRotate, props.mScale, - props.mMotion.ptrOr(nullptr), props.mTransform, refBox, a2css); - if (!matrix.IsIdentity()) { - std::ignore = matrix.CanDraw2D(&result); - } - return result; + return gfx::Matrix(SVGUtils::GetTransformMatrixInUserSpace(f)); } float SVGGeometryElement::GetPathLengthScale(PathLengthScaleForType aFor) { diff --git a/dom/webidl/MediaDebugInfo.webidl b/dom/webidl/MediaDebugInfo.webidl @@ -117,6 +117,7 @@ dictionary DecodedStreamDebugInfo { DOMString instance = ""; long long startTime = 0; long long lastOutputTime = 0; + long long lastReportedPosition = 0; long playing = 0; long long lastAudio = 0; boolean audioQueueFinished = false; diff --git a/ipc/glue/IPCMessageUtilsSpecializations.h b/ipc/glue/IPCMessageUtilsSpecializations.h @@ -27,9 +27,6 @@ #include "mozilla/IntegerRange.h" #include "mozilla/Maybe.h" #include "mozilla/TimeStamp.h" -#ifdef XP_WIN -# include "mozilla/TimeStamp_windows.h" -#endif #include "mozilla/UniquePtr.h" #include "mozilla/Vector.h" @@ -435,27 +432,6 @@ struct ParamTraits<mozilla::TimeStamp> { }; }; -#ifdef XP_WIN - -template <> -struct ParamTraits<mozilla::TimeStampValue> { - typedef mozilla::TimeStampValue paramType; - static void Write(MessageWriter* aWriter, const paramType& aParam) { - WriteParam(aWriter, aParam.mGTC); - WriteParam(aWriter, aParam.mQPC); - WriteParam(aWriter, aParam.mIsNull); - WriteParam(aWriter, aParam.mHasQPC); - } - static bool Read(MessageReader* aReader, paramType* aResult) { - return (ReadParam(aReader, &aResult->mGTC) && - ReadParam(aReader, &aResult->mQPC) && - ReadParam(aReader, &aResult->mIsNull) && - ReadParam(aReader, &aResult->mHasQPC)); - } -}; - -#endif - template <> struct ParamTraits<mozilla::dom::ipc::StructuredCloneData> { typedef mozilla::dom::ipc::StructuredCloneData paramType; diff --git a/js/loader/ImportMap.cpp b/js/loader/ImportMap.cpp @@ -9,6 +9,7 @@ #include "js/Array.h" // IsArrayObject #include "js/friend/ErrorMessages.h" // js::GetErrorMessage, JSMSG_* #include "js/JSON.h" // JS_ParseJSON +#include "js/PropertyDescriptor.h" // JS::PropertyDescriptor #include "LoadedScript.h" #include "ModuleLoaderBase.h" // ScriptLoaderInterface #include "nsContentUtils.h" @@ -371,6 +372,21 @@ static UniquePtr<IntegrityMap> NormalizeIntegrity( return normalized; } +static bool GetOwnProperty(JSContext* aCx, Handle<JSObject*> aObj, + const char* aName, MutableHandle<Value> aValueOut) { + JS::Rooted<mozilla::Maybe<JS::PropertyDescriptor>> desc(aCx); + if (!JS_GetOwnPropertyDescriptor(aCx, aObj, aName, &desc)) { + return false; + } + + if (desc.isNothing()) { + return true; + } + MOZ_ASSERT(!desc->isAccessorDescriptor()); + aValueOut.set(desc->value()); + return true; +} + // https://html.spec.whatwg.org/multipage/webappapis.html#parse-an-import-map-string // static UniquePtr<ImportMap> ImportMap::ParseString( @@ -417,7 +433,7 @@ UniquePtr<ImportMap> ImportMap::ParseString( RootedObject parsedObj(aCx, &parsedVal.toObject()); RootedValue importsVal(aCx); - if (!JS_GetProperty(aCx, parsedObj, "imports", &importsVal)) { + if (!GetOwnProperty(aCx, parsedObj, "imports", &importsVal)) { return nullptr; } @@ -453,7 +469,7 @@ UniquePtr<ImportMap> ImportMap::ParseString( } RootedValue scopesVal(aCx); - if (!JS_GetProperty(aCx, parsedObj, "scopes", &scopesVal)) { + if (!GetOwnProperty(aCx, parsedObj, "scopes", &scopesVal)) { return nullptr; } @@ -489,7 +505,7 @@ UniquePtr<ImportMap> ImportMap::ParseString( } RootedValue integrityVal(aCx); - if (!JS_GetProperty(aCx, parsedObj, "integrity", &integrityVal)) { + if (!GetOwnProperty(aCx, parsedObj, "integrity", &integrityVal)) { return nullptr; } @@ -583,7 +599,6 @@ static mozilla::Result<nsCOMPtr<nsIURI>, ResolveError> ResolveImportsMatch( const SpecifierMap* aSpecifierMap) { // Step 1. For each specifierKey → resolutionResult of specifierMap, for (auto&& [specifierKey, resolutionResult] : *aSpecifierMap) { - nsAutoString specifier{aNormalizedSpecifier}; nsCString asURL = aAsURL ? aAsURL->GetSpecOrDefault() : EmptyCString(); // Step 1.1. If specifierKey is normalizedSpecifier, then: diff --git a/js/src/jit-test/lib/adhoc-multiplatform-test.js b/js/src/jit-test/lib/adhoc-multiplatform-test.js @@ -94,7 +94,7 @@ const archOptions = ldr x29, \\[sp\\]` }, arm: { - encoding: `${HEX}{8} ${HEX}{8}`, + encoding: `${HEX}{8}\\s+${HEX}{8}`, // The move from r9 to fp is writing the callee's wasm instance into // the frame for debug checks -- see WasmFrame.h. prefix: `str fp, \\[sp, #-4\\]! @@ -223,37 +223,17 @@ function codegenTestMultiplatform_adhoc(module_text, export_name, if (!options.no_suffix) { expected = expected + '\n' + suffix; } - if (genArm) { - // For obscure reasons, the arm(32) disassembler prints the - // instruction word twice. Rather than forcing all expected lines to - // do the same, we detect any line starting with 8 hex digits followed - // by a space, and duplicate them so as to match the - // disassembler's output. - let newExpected = ""; - let pattern = /^[0-9a-fA-F]{8} /; - for (line of expected.split(/\n+/)) { - // Remove whitespace at the start of the line. This could happen - // for continuation lines in backtick-style expected strings. - while (line.match(/^\s/)) { - line = line.slice(1); - } - if (line.match(pattern)) { - line = line.slice(0,9) + line; - } - newExpected = newExpected + line + "\n"; - } - expected = newExpected; - } - expected = fixlines(expected, encoding); + expected = fixlines(expected); // Compile the test case and collect disassembly output. let ins = wasmEvalText(module_text, {}, options.features); if (options.instanceBox) options.instanceBox.value = ins; let output = wasmDis(ins.exports[export_name], {tier:"ion", asString:true}); + let output_simple = stripencoding(output, encoding); // Check for success, print diagnostics - let output_matches_expected = output.match(new RegExp(expected)) != null; + let output_matches_expected = output_simple.match(new RegExp(expected)) != null; if (!output_matches_expected) { print("---- adhoc-tier1-test.js: TEST FAILED ----"); } diff --git a/js/src/jit-test/lib/codegen-arm64-test.js b/js/src/jit-test/lib/codegen-arm64-test.js @@ -33,9 +33,10 @@ function codegenTestARM64_adhoc(module_text, export_name, expected, options = {} expected = arm64_prefix + '\n' + expected; if (!options.no_suffix) expected = expected + '\n' + arm64_suffix; - expected = fixlines(expected, `${HEX}{8}`); + expected = fixlines(expected); - const output_matches_expected = output.match(new RegExp(expected)) != null; + const output_simple = stripencoding(output, `${HEX}{8}`); + const output_matches_expected = output_simple.match(new RegExp(expected)) != null; if (!output_matches_expected) { print("---- codegen-arm64-test.js: TEST FAILED ----"); } diff --git a/js/src/jit-test/lib/codegen-test-common.js b/js/src/jit-test/lib/codegen-test-common.js @@ -11,21 +11,28 @@ function wrap(options, funcs) { return `(module ${funcs})`; } -function fixlines(s, insEncoding) { +function fixlines(s) { return s.split(/\n+/) .map(strip) .filter(x => x.length > 0) - .map(x => `(?:0x)?${HEX}+ ${insEncoding} ${x}`) .map(spaces) .join('\n'); } +function stripencoding(s, insEncoding) { + var encoding = RegExp(`^(?:0x)?${HEX}+\\s+${insEncoding}\\s+(.*)$`); + return s.split('\n') + .map(x => x.match(encoding)?.[1] ?? x) + .join('\n'); +} + function strip(s) { - while (s.length > 0 && isspace(s.charAt(0))) - s = s.substring(1); - while (s.length > 0 && isspace(s.charAt(s.length-1))) - s = s.substring(0, s.length-1); - return s; + var start = 0, end = s.length; + while (start < s.length && isspace(s.charAt(start))) + start++; + while (end > start && isspace(s.charAt(end - 1))) + end--; + return s.substring(start, end); } function striplines(s) { diff --git a/js/src/jit-test/lib/codegen-x64-test.js b/js/src/jit-test/lib/codegen-x64-test.js @@ -163,9 +163,10 @@ function codegenTestX64_adhoc(module_text, export_name, expected, options = {}) if (!options.no_suffix) expected = expected + '\n' + x64_suffix; const expected_pretty = striplines(expected); - expected = fixlines(expected, `(?:${HEX}{2} )*`); + expected = fixlines(expected); - const success = output.match(new RegExp(expected)) != null; + const output_simple = stripencoding(output, `(?:${HEX}{2} )*`); + const success = output_simple.match(new RegExp(expected)) != null; if (options.log || !success) { print("Module text:") print(module_text); diff --git a/js/src/jit-test/lib/codegen-x86-test.js b/js/src/jit-test/lib/codegen-x86-test.js @@ -57,9 +57,10 @@ function codegenTestX86_adhoc(module_text, export_name, expected, options = {}) expected = x86_prefix + '\n' + expected; if (!options.no_suffix) expected = expected + '\n' + x86_suffix; - expected = fixlines(expected, `(?:${HEX}{2} )*`); + expected = fixlines(expected); - const output_matches_expected = output.match(new RegExp(expected)) != null; + const output_simple = stripencoding(output, `(?:${HEX}{2} )*`); + const output_matches_expected = output_simple.match(new RegExp(expected)) != null; if (!output_matches_expected) { print("---- codegen-x86-test.js: TEST FAILED ----"); } diff --git a/js/src/jit-test/tests/wasm/binop-x64-ion-codegen.js b/js/src/jit-test/tests/wasm/binop-x64-ion-codegen.js @@ -383,7 +383,7 @@ for ( [pAnyCmp, pAnySel, cmpArgL, cmovArgL ] of )`, 'f', // On Linux we have an extra move - (getBuildConfiguration("windows") ? '' : '48 89 .. mov %r.+, %r.+\n') + + (getBuildConfiguration("windows") ? '' : 'mov %r.+, %r.+\n') + // 'q*' because the disassembler shows 'q' only for the memory cases `mov %r.+, %r.+ cmpq* ${cmpArgL}, %r.+ diff --git a/js/src/jit/LIR.h b/js/src/jit/LIR.h @@ -2169,13 +2169,10 @@ AnyRegister LAllocation::toAnyRegister() const { } // namespace js #include "jit/shared/LIR-shared.h" -#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64) -# if defined(JS_CODEGEN_X86) -# include "jit/x86/LIR-x86.h" -# elif defined(JS_CODEGEN_X64) -# include "jit/x64/LIR-x64.h" -# endif -# include "jit/x86-shared/LIR-x86-shared.h" +#if defined(JS_CODEGEN_X86) +# include "jit/x86/LIR-x86.h" +#elif defined(JS_CODEGEN_X64) +# include "jit/x64/LIR-x64.h" #elif defined(JS_CODEGEN_ARM) # include "jit/arm/LIR-arm.h" #elif defined(JS_CODEGEN_ARM64) diff --git a/js/src/jit/LIROps.yaml b/js/src/jit/LIROps.yaml @@ -4160,6 +4160,60 @@ defer_init: true #endif +#if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64) +- name: DivConstantI + result_type: WordSized + operands: + numerator: WordSized + arguments: + denominator: int32_t + num_temps: 1 + mir_op: Div + +- name: ModConstantI + result_type: WordSized + operands: + numerator: WordSized + arguments: + denominator: int32_t + num_temps: 1 + mir_op: Mod + +- name: UDivConstant + result_type: WordSized + operands: + numerator: WordSized + arguments: + denominator: uint32_t + num_temps: 1 + mir_op: Div + +- name: UModConstant + result_type: WordSized + operands: + numerator: WordSized + arguments: + denominator: uint32_t + num_temps: 1 + mir_op: Mod + +- name: UDiv + result_type: WordSized + operands: + lhs: WordSized + rhs: WordSized + num_temps: 1 + mir_op: Div + +- name: UMod + result_type: WordSized + operands: + lhs: WordSized + rhs: WordSized + num_temps: 1 + mir_op: Mod +#endif + #ifdef JS_CODEGEN_X86 - name: BoxFloatingPoint result_type: BoxedValue @@ -4176,15 +4230,6 @@ - name: UDivOrModI64 gen_boilerplate: false -- name: DivOrModConstantI - gen_boilerplate: false - -- name: UDivOrMod - gen_boilerplate: false - -- name: UDivOrModConstant - gen_boilerplate: false - - name: WasmTruncateToInt64 result_type: Int64 operands: @@ -4245,20 +4290,37 @@ #endif #ifdef JS_CODEGEN_X64 -- name: DivOrModI64 - gen_boilerplate: false - -- name: UDivOrModI64 - gen_boilerplate: false +- name: DivI64 + result_type: Int64 + operands: + lhs: WordSized + rhs: WordSized + num_temps: 1 + mir_op: Div -- name: DivOrModConstantI - gen_boilerplate: false +- name: ModI64 + result_type: Int64 + operands: + lhs: WordSized + rhs: WordSized + num_temps: 1 + mir_op: Mod -- name: UDivOrMod - gen_boilerplate: false +- name: UDivI64 + result_type: Int64 + operands: + lhs: WordSized + rhs: WordSized + num_temps: 1 + mir_op: Div -- name: UDivOrModConstant - gen_boilerplate: false +- name: UModI64 + result_type: Int64 + operands: + lhs: WordSized + rhs: WordSized + num_temps: 1 + mir_op: Mod - name: WasmTruncateToInt64 result_type: Int64 diff --git a/js/src/jit/PerfSpewer.cpp b/js/src/jit/PerfSpewer.cpp @@ -122,7 +122,7 @@ static uint64_t GetMonotonicTimestamp() { # ifdef XP_LINUX return TimeStamp::Now().RawClockMonotonicNanosecondsSinceBoot(); # elif XP_WIN - return TimeStamp::Now().RawQueryPerformanceCounterValue().value(); + return TimeStamp::Now().RawQueryPerformanceCounterValue(); # elif XP_DARWIN return TimeStamp::Now().RawMachAbsoluteTimeNanoseconds(); # else diff --git a/js/src/jit/x64/CodeGenerator-x64.cpp b/js/src/jit/x64/CodeGenerator-x64.cpp @@ -20,8 +20,6 @@ using namespace js; using namespace js::jit; -using mozilla::DebugOnly; - CodeGeneratorX64::CodeGeneratorX64(MIRGenerator* gen, LIRGraph* graph, MacroAssembler* masm, const wasm::CodeMetadata* wasmCodeMeta) @@ -197,42 +195,75 @@ void CodeGenerator::visitMulI64(LMulI64* lir) { } } -void CodeGenerator::visitDivOrModI64(LDivOrModI64* lir) { +template <class LIR> +static void TrapIfDivideByZero(MacroAssembler& masm, LIR* lir, Register rhs) { + auto* mir = lir->mir(); + MOZ_ASSERT(mir->trapOnError()); + + if (mir->canBeDivideByZero()) { + Label nonZero; + masm.branchTestPtr(Assembler::NonZero, rhs, rhs, &nonZero); + masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); + masm.bind(&nonZero); + } +} + +void CodeGenerator::visitDivI64(LDivI64* lir) { Register lhs = ToRegister(lir->lhs()); Register rhs = ToRegister(lir->rhs()); - Register output = ToRegister(lir->output()); - MOZ_ASSERT_IF(lhs != rhs, rhs != rax); + MOZ_ASSERT(lhs == rax); + MOZ_ASSERT(rhs != rax); MOZ_ASSERT(rhs != rdx); - MOZ_ASSERT_IF(output == rax, ToRegister(lir->remainder()) == rdx); - MOZ_ASSERT_IF(output == rdx, ToRegister(lir->remainder()) == rax); + MOZ_ASSERT(ToRegister(lir->output()) == rax); + MOZ_ASSERT(ToRegister(lir->temp0()) == rdx); - Label done; + MDiv* mir = lir->mir(); - // Put the lhs in rax. - if (lhs != rax) { - masm.mov(lhs, rax); + // Handle divide by zero. + TrapIfDivideByZero(masm, lir, rhs); + + // Handle an integer overflow exception from INT64_MIN / -1. + if (mir->canBeNegativeOverflow()) { + Label notOverflow; + masm.branchPtr(Assembler::NotEqual, lhs, ImmWord(INT64_MIN), ¬Overflow); + masm.branchPtr(Assembler::NotEqual, rhs, ImmWord(-1), ¬Overflow); + masm.wasmTrap(wasm::Trap::IntegerOverflow, mir->trapSiteDesc()); + masm.bind(¬Overflow); } + // Sign extend the lhs into rdx to make rdx:rax. + masm.cqo(); + masm.idivq(rhs); +} + +void CodeGenerator::visitModI64(LModI64* lir) { + Register lhs = ToRegister(lir->lhs()); + Register rhs = ToRegister(lir->rhs()); + Register output = ToRegister(lir->output()); + + MOZ_ASSERT(lhs == rax); + MOZ_ASSERT(rhs != rax); + MOZ_ASSERT(rhs != rdx); + MOZ_ASSERT(ToRegister(lir->output()) == rdx); + MOZ_ASSERT(ToRegister(lir->temp0()) == rax); + + MMod* mir = lir->mir(); + + Label done; + // Handle divide by zero. - if (lir->canBeDivideByZero()) { - Label nonZero; - masm.branchTestPtr(Assembler::NonZero, rhs, rhs, &nonZero); - masm.wasmTrap(wasm::Trap::IntegerDivideByZero, lir->trapSiteDesc()); - masm.bind(&nonZero); - } + TrapIfDivideByZero(masm, lir, rhs); // Handle an integer overflow exception from INT64_MIN / -1. - if (lir->canBeNegativeOverflow()) { + if (mir->canBeNegativeDividend()) { Label notOverflow; masm.branchPtr(Assembler::NotEqual, lhs, ImmWord(INT64_MIN), ¬Overflow); masm.branchPtr(Assembler::NotEqual, rhs, ImmWord(-1), ¬Overflow); - if (lir->mir()->isMod()) { + { masm.xorl(output, output); - } else { - masm.wasmTrap(wasm::Trap::IntegerOverflow, lir->trapSiteDesc()); + masm.jump(&done); } - masm.jump(&done); masm.bind(¬Overflow); } @@ -243,36 +274,38 @@ void CodeGenerator::visitDivOrModI64(LDivOrModI64* lir) { masm.bind(&done); } -void CodeGenerator::visitUDivOrModI64(LUDivOrModI64* lir) { - Register lhs = ToRegister(lir->lhs()); +void CodeGenerator::visitUDivI64(LUDivI64* lir) { Register rhs = ToRegister(lir->rhs()); - DebugOnly<Register> output = ToRegister(lir->output()); - MOZ_ASSERT_IF(lhs != rhs, rhs != rax); + MOZ_ASSERT(ToRegister(lir->lhs()) == rax); + MOZ_ASSERT(rhs != rax); MOZ_ASSERT(rhs != rdx); - MOZ_ASSERT_IF(output.value == rax, ToRegister(lir->remainder()) == rdx); - MOZ_ASSERT_IF(output.value == rdx, ToRegister(lir->remainder()) == rax); + MOZ_ASSERT(ToRegister(lir->output()) == rax); + MOZ_ASSERT(ToRegister(lir->temp0()) == rdx); - // Put the lhs in rax. - if (lhs != rax) { - masm.mov(lhs, rax); - } + // Prevent divide by zero. + TrapIfDivideByZero(masm, lir, rhs); - Label done; + // Zero extend the lhs into rdx to make (rdx:rax). + masm.xorl(rdx, rdx); + masm.udivq(rhs); +} + +void CodeGenerator::visitUModI64(LUModI64* lir) { + Register rhs = ToRegister(lir->rhs()); + + MOZ_ASSERT(ToRegister(lir->lhs()) == rax); + MOZ_ASSERT(rhs != rax); + MOZ_ASSERT(rhs != rdx); + MOZ_ASSERT(ToRegister(lir->output()) == rdx); + MOZ_ASSERT(ToRegister(lir->temp0()) == rax); // Prevent divide by zero. - if (lir->canBeDivideByZero()) { - Label nonZero; - masm.branchTestPtr(Assembler::NonZero, rhs, rhs, &nonZero); - masm.wasmTrap(wasm::Trap::IntegerDivideByZero, lir->trapSiteDesc()); - masm.bind(&nonZero); - } + TrapIfDivideByZero(masm, lir, rhs); // Zero extend the lhs into rdx to make (rdx:rax). masm.xorl(rdx, rdx); masm.udivq(rhs); - - masm.bind(&done); } void CodeGeneratorX64::emitBigIntPtrDiv(LBigIntPtrDiv* ins, Register dividend, diff --git a/js/src/jit/x64/LIR-x64.h b/js/src/jit/x64/LIR-x64.h @@ -28,87 +28,6 @@ class LUnbox : public LInstructionHelper<1, BOX_PIECES, 0> { const char* extraName() const { return StringFromMIRType(mir()->type()); } }; -class LDivOrModI64 : public LBinaryMath<1> { - public: - LIR_HEADER(DivOrModI64) - - LDivOrModI64(const LAllocation& lhs, const LAllocation& rhs, - const LDefinition& temp) - : LBinaryMath(classOpcode) { - setOperand(0, lhs); - setOperand(1, rhs); - setTemp(0, temp); - } - - const LDefinition* remainder() { return getTemp(0); } - - MBinaryArithInstruction* mir() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - return static_cast<MBinaryArithInstruction*>(mir_); - } - bool canBeDivideByZero() const { - if (mir_->isMod()) { - return mir_->toMod()->canBeDivideByZero(); - } - return mir_->toDiv()->canBeDivideByZero(); - } - bool canBeNegativeOverflow() const { - if (mir_->isMod()) { - return mir_->toMod()->canBeNegativeDividend(); - } - return mir_->toDiv()->canBeNegativeOverflow(); - } - const wasm::TrapSiteDesc& trapSiteDesc() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - if (mir_->isMod()) { - return mir_->toMod()->trapSiteDesc(); - } - return mir_->toDiv()->trapSiteDesc(); - } -}; - -// This class performs a simple x86 'div', yielding either a quotient or -// remainder depending on whether this instruction is defined to output -// rax (quotient) or rdx (remainder). -class LUDivOrModI64 : public LBinaryMath<1> { - public: - LIR_HEADER(UDivOrModI64); - - LUDivOrModI64(const LAllocation& lhs, const LAllocation& rhs, - const LDefinition& temp) - : LBinaryMath(classOpcode) { - setOperand(0, lhs); - setOperand(1, rhs); - setTemp(0, temp); - } - - const LDefinition* remainder() { return getTemp(0); } - - const char* extraName() const { - return mir()->isTruncated() ? "Truncated" : nullptr; - } - - MBinaryArithInstruction* mir() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - return static_cast<MBinaryArithInstruction*>(mir_); - } - - bool canBeDivideByZero() const { - if (mir_->isMod()) { - return mir_->toMod()->canBeDivideByZero(); - } - return mir_->toDiv()->canBeDivideByZero(); - } - - const wasm::TrapSiteDesc& trapSiteDesc() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - if (mir_->isMod()) { - return mir_->toMod()->trapSiteDesc(); - } - return mir_->toDiv()->trapSiteDesc(); - } -}; - } // namespace jit } // namespace js diff --git a/js/src/jit/x64/Lowering-x64.cpp b/js/src/jit/x64/Lowering-x64.cpp @@ -526,8 +526,8 @@ void LIRGenerator::visitSubstr(MSubstr* ins) { } void LIRGeneratorX64::lowerDivI64(MDiv* div) { - LDivOrModI64* lir = new (alloc()) LDivOrModI64( - useRegister(div->lhs()), useRegister(div->rhs()), tempFixed(rdx)); + auto* lir = new (alloc()) LDivI64(useFixedAtStart(div->lhs(), rax), + useRegister(div->rhs()), tempFixed(rdx)); defineInt64Fixed(lir, div, LInt64Allocation(LAllocation(AnyRegister(rax)))); } @@ -536,8 +536,8 @@ void LIRGeneratorX64::lowerWasmBuiltinDivI64(MWasmBuiltinDivI64* div) { } void LIRGeneratorX64::lowerModI64(MMod* mod) { - LDivOrModI64* lir = new (alloc()) LDivOrModI64( - useRegister(mod->lhs()), useRegister(mod->rhs()), tempFixed(rax)); + auto* lir = new (alloc()) LModI64(useFixedAtStart(mod->lhs(), rax), + useRegister(mod->rhs()), tempFixed(rax)); defineInt64Fixed(lir, mod, LInt64Allocation(LAllocation(AnyRegister(rdx)))); } @@ -546,14 +546,14 @@ void LIRGeneratorX64::lowerWasmBuiltinModI64(MWasmBuiltinModI64* mod) { } void LIRGeneratorX64::lowerUDivI64(MDiv* div) { - LUDivOrModI64* lir = new (alloc()) LUDivOrModI64( - useRegister(div->lhs()), useRegister(div->rhs()), tempFixed(rdx)); + auto* lir = new (alloc()) LUDivI64(useFixedAtStart(div->lhs(), rax), + useRegister(div->rhs()), tempFixed(rdx)); defineInt64Fixed(lir, div, LInt64Allocation(LAllocation(AnyRegister(rax)))); } void LIRGeneratorX64::lowerUModI64(MMod* mod) { - LUDivOrModI64* lir = new (alloc()) LUDivOrModI64( - useRegister(mod->lhs()), useRegister(mod->rhs()), tempFixed(rax)); + auto* lir = new (alloc()) LUModI64(useFixedAtStart(mod->lhs(), rax), + useRegister(mod->rhs()), tempFixed(rax)); defineInt64Fixed(lir, mod, LInt64Allocation(LAllocation(AnyRegister(rdx)))); } diff --git a/js/src/jit/x64/MacroAssembler-x64-inl.h b/js/src/jit/x64/MacroAssembler-x64-inl.h @@ -84,18 +84,28 @@ void MacroAssembler::notPtr(Register reg) { notq(reg); } void MacroAssembler::andPtr(Register src, Register dest) { andq(src, dest); } -void MacroAssembler::andPtr(Imm32 imm, Register dest) { andq(imm, dest); } +void MacroAssembler::andPtr(Imm32 imm, Register dest) { + if (imm.value >= 0) { + andl(imm, dest); + } else { + andq(imm, dest); + } +} void MacroAssembler::andPtr(Imm32 imm, Register src, Register dest) { if (src != dest) { movq(src, dest); } - andq(imm, dest); + andPtr(imm, dest); } void MacroAssembler::and64(Imm64 imm, Register64 dest) { if (INT32_MIN <= int64_t(imm.value) && int64_t(imm.value) <= INT32_MAX) { - andq(Imm32(imm.value), dest.reg); + if (int32_t(imm.value) >= 0) { + andl(Imm32(imm.value), dest.reg); + } else { + andq(Imm32(imm.value), dest.reg); + } } else { ScratchRegisterScope scratch(*this); movq(ImmWord(uintptr_t(imm.value)), scratch); diff --git a/js/src/jit/x64/MacroAssembler-x64.cpp b/js/src/jit/x64/MacroAssembler-x64.cpp @@ -1578,7 +1578,7 @@ void MacroAssembler::convertUInt64ToDouble(Register64 input, mov(input.reg, scratch); mov(input.reg, temp); shrq(Imm32(1), scratch); - andq(Imm32(1), temp); + andl(Imm32(1), temp); orq(temp, scratch); vcvtsq2sd(scratch, output, output); @@ -1608,7 +1608,7 @@ void MacroAssembler::convertUInt64ToFloat32(Register64 input, mov(input.reg, scratch); mov(input.reg, temp); shrq(Imm32(1), scratch); - andq(Imm32(1), temp); + andl(Imm32(1), temp); orq(temp, scratch); vcvtsq2ss(scratch, output, output); diff --git a/js/src/jit/x86-shared/CodeGenerator-x86-shared.cpp b/js/src/jit/x86-shared/CodeGenerator-x86-shared.cpp @@ -872,40 +872,54 @@ void CodeGenerator::visitMulI(LMulI* ins) { } } -void CodeGenerator::visitUDivOrMod(LUDivOrMod* ins) { - Register lhs = ToRegister(ins->lhs()); +template <class LIR> +static void TrapIfDivideByZero(MacroAssembler& masm, LIR* lir, Register rhs) { + auto* mir = lir->mir(); + MOZ_ASSERT(mir->trapOnError()); + MOZ_ASSERT(mir->canBeDivideByZero()); + + Label nonZero; + masm.branchTest32(Assembler::NonZero, rhs, rhs, &nonZero); + masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); + masm.bind(&nonZero); +} + +OutOfLineCode* CodeGeneratorX86Shared::emitOutOfLineZeroForDivideByZero( + Register rhs, Register output) { + // Truncated division by zero is zero: (±Infinity|0 == 0) and (NaN|0 == 0). + auto* ool = new (alloc()) LambdaOutOfLineCode([=](OutOfLineCode& ool) { + masm.mov(ImmWord(0), output); + masm.jmp(ool.rejoin()); + }); + masm.branchTest32(Assembler::Zero, rhs, rhs, ool->entry()); + + return ool; +} + +void CodeGenerator::visitUDiv(LUDiv* ins) { Register rhs = ToRegister(ins->rhs()); Register output = ToRegister(ins->output()); + Register remainder = ToRegister(ins->temp0()); - MOZ_ASSERT_IF(lhs != rhs, rhs != eax); + MOZ_ASSERT(ToRegister(ins->lhs()) == eax); + MOZ_ASSERT(rhs != eax); MOZ_ASSERT(rhs != edx); - MOZ_ASSERT_IF(output == eax, ToRegister(ins->remainder()) == edx); + MOZ_ASSERT(output == eax); + MOZ_ASSERT(remainder == edx); - OutOfLineCode* ool = nullptr; + MDiv* mir = ins->mir(); - // Put the lhs in eax. - if (lhs != eax) { - masm.mov(lhs, eax); - } + OutOfLineCode* ool = nullptr; // Prevent divide by zero. - if (ins->canBeDivideByZero()) { - masm.test32(rhs, rhs); - if (ins->mir()->isTruncated()) { - if (ins->trapOnError()) { - Label nonZero; - masm.j(Assembler::NonZero, &nonZero); - masm.wasmTrap(wasm::Trap::IntegerDivideByZero, ins->trapSiteDesc()); - masm.bind(&nonZero); - } else { - ool = new (alloc()) LambdaOutOfLineCode([=](OutOfLineCode& ool) { - masm.mov(ImmWord(0), output); - masm.jmp(ool.rejoin()); - }); - masm.j(Assembler::Zero, ool->entry()); - } + if (mir->canBeDivideByZero()) { + if (mir->trapOnError()) { + TrapIfDivideByZero(masm, ins, rhs); + } else if (mir->isTruncated()) { + ool = emitOutOfLineZeroForDivideByZero(rhs, output); } else { - bailoutIf(Assembler::Zero, ins->snapshot()); + MOZ_ASSERT(mir->fallible()); + bailoutTest32(Assembler::Zero, rhs, rhs, ins->snapshot()); } } @@ -914,56 +928,102 @@ void CodeGenerator::visitUDivOrMod(LUDivOrMod* ins) { masm.udiv(rhs); // If the remainder is > 0, bailout since this must be a double. - if (ins->mir()->isDiv() && !ins->mir()->toDiv()->canTruncateRemainder()) { - Register remainder = ToRegister(ins->remainder()); - masm.test32(remainder, remainder); - bailoutIf(Assembler::NonZero, ins->snapshot()); + if (!mir->canTruncateRemainder()) { + bailoutTest32(Assembler::NonZero, remainder, remainder, ins->snapshot()); } - // Unsigned div or mod can return a value that's not a signed int32. + // Unsigned div can return a value that's not a signed int32. // If our users aren't expecting that, bail. - if (!ins->mir()->isTruncated()) { - masm.test32(output, output); - bailoutIf(Assembler::Signed, ins->snapshot()); + if (!mir->isTruncated()) { + bailoutTest32(Assembler::Signed, output, output, ins->snapshot()); } if (ool) { - addOutOfLineCode(ool, ins->mir()); + addOutOfLineCode(ool, mir); masm.bind(ool->rejoin()); } } -void CodeGenerator::visitUDivOrModConstant(LUDivOrModConstant* ins) { - Register lhs = ToRegister(ins->numerator()); +void CodeGenerator::visitUMod(LUMod* ins) { + Register rhs = ToRegister(ins->rhs()); Register output = ToRegister(ins->output()); - uint32_t d = ins->denominator(); - // This emits the division answer into edx or the modulus answer into eax. - MOZ_ASSERT(output == eax || output == edx); - MOZ_ASSERT(lhs != eax && lhs != edx); - bool isDiv = (output == edx); + MOZ_ASSERT(ToRegister(ins->lhs()) == eax); + MOZ_ASSERT(rhs != eax); + MOZ_ASSERT(rhs != edx); + MOZ_ASSERT(output == edx); + MOZ_ASSERT(ToRegister(ins->temp0()) == eax); - if (d == 0) { - if (ins->mir()->isTruncated()) { - if (ins->trapOnError()) { - masm.wasmTrap(wasm::Trap::IntegerDivideByZero, ins->trapSiteDesc()); - } else { - masm.xorl(output, output); - } + MMod* mir = ins->mir(); + + OutOfLineCode* ool = nullptr; + + // Prevent divide by zero. + if (mir->canBeDivideByZero()) { + if (mir->trapOnError()) { + TrapIfDivideByZero(masm, ins, rhs); + } else if (mir->isTruncated()) { + ool = emitOutOfLineZeroForDivideByZero(rhs, output); } else { - bailout(ins->snapshot()); + MOZ_ASSERT(mir->fallible()); + bailoutTest32(Assembler::Zero, rhs, rhs, ins->snapshot()); } - return; } + // Zero extend the lhs into edx to make (edx:eax), since udiv is 64-bit. + masm.mov(ImmWord(0), edx); + masm.udiv(rhs); + + // Unsigned mod can return a value that's not a signed int32. + // If our users aren't expecting that, bail. + if (!mir->isTruncated()) { + bailoutTest32(Assembler::Signed, output, output, ins->snapshot()); + } + + if (ool) { + addOutOfLineCode(ool, mir); + masm.bind(ool->rejoin()); + } +} + +template <class LUDivOrUMod> +static void UnsignedDivideWithConstant(MacroAssembler& masm, LUDivOrUMod* ins, + Register result, Register temp) { + Register lhs = ToRegister(ins->numerator()); + uint32_t d = ins->denominator(); + + MOZ_ASSERT(lhs != result && lhs != temp); +#ifdef JS_CODEGEN_X86 + MOZ_ASSERT(result == edx && temp == eax); +#else + MOZ_ASSERT(result != temp); +#endif + // The denominator isn't a power of 2 (see LDivPowTwoI and LModPowTwoI). - MOZ_ASSERT((d & (d - 1)) != 0); + MOZ_ASSERT(!mozilla::IsPowerOfTwo(d)); auto rmc = ReciprocalMulConstants::computeUnsignedDivisionConstants(d); // We first compute (M * n) >> 32, where M = rmc.multiplier. +#ifdef JS_CODEGEN_X86 masm.movl(Imm32(rmc.multiplier), eax); masm.umull(lhs); +#else + // Zero-extend |lhs| in preparation for a 64-bit multiplication. + masm.movl(lhs, result); + + // Note that imul sign-extends its 32-bit immediate, but we need an unsigned + // multiplication. + if (int32_t(rmc.multiplier) >= 0) { + masm.imulq(Imm32(rmc.multiplier), result, result); + } else { + masm.movl(Imm32(rmc.multiplier), temp); + masm.imulq(temp, result); + } + if (rmc.multiplier > UINT32_MAX || rmc.shiftAmount == 0) { + masm.shrq(Imm32(32), result); + } +#endif if (rmc.multiplier > UINT32_MAX) { // M >= 2^32 and shift == 0 is impossible, as d >= 2 implies that // ((M * n) >> (32 + shift)) >= n > floor(n/d) whenever n >= d, @@ -971,45 +1031,112 @@ void CodeGenerator::visitUDivOrModConstant(LUDivOrModConstant* ins) { MOZ_ASSERT(rmc.shiftAmount > 0); MOZ_ASSERT(rmc.multiplier < (int64_t(1) << 33)); - // We actually computed edx = ((uint32_t(M) * n) >> 32) instead. Since - // (M * n) >> (32 + shift) is the same as (edx + n) >> shift, we can + // We actually computed result = ((uint32_t(M) * n) >> 32) instead. Since + // (M * n) >> (32 + shift) is the same as (result + n) >> shift, we can // correct for the overflow. This case is a bit trickier than the signed - // case, though, as the (edx + n) addition itself can overflow; however, - // note that (edx + n) >> shift == (((n - edx) >> 1) + edx) >> (shift - 1), + // case, though, as the (result + n) addition itself can overflow; however, + // note that + // (result + n) >> shift == (((n - result) >> 1) + result) >> (shift - 1), // which is overflow-free. See Hacker's Delight, section 10-8 for details. - // Compute (n - edx) >> 1 into eax. - masm.movl(lhs, eax); - masm.subl(edx, eax); - masm.shrl(Imm32(1), eax); + // Compute (n - result) >> 1 into temp. + masm.movl(lhs, temp); + masm.subl(result, temp); + masm.shrl(Imm32(1), temp); // Finish the computation. - masm.addl(eax, edx); - masm.shrl(Imm32(rmc.shiftAmount - 1), edx); + masm.addl(temp, result); + if (rmc.shiftAmount > 1) { + masm.shrl(Imm32(rmc.shiftAmount - 1), result); + } } else { - masm.shrl(Imm32(rmc.shiftAmount), edx); - } - - // We now have the truncated division value in edx. If we're - // computing a modulus or checking whether the division resulted - // in an integer, we need to multiply the obtained value by d and - // finish the computation/check. - if (!isDiv) { - masm.imull(Imm32(d), edx, edx); - masm.movl(lhs, eax); - masm.subl(edx, eax); - - // The final result of the modulus op, just computed above by the - // sub instruction, can be a number in the range [2^31, 2^32). If - // this is the case and the modulus is not truncated, we must bail - // out. - if (!ins->mir()->isTruncated()) { - bailoutIf(Assembler::Signed, ins->snapshot()); + if (rmc.shiftAmount > 0) { +#ifdef JS_CODEGEN_X86 + masm.shrl(Imm32(rmc.shiftAmount), result); +#else + masm.shrq(Imm32(32 + rmc.shiftAmount), result); +#endif } - } else if (!ins->mir()->isTruncated()) { - masm.imull(Imm32(d), edx, eax); - masm.cmpl(lhs, eax); - bailoutIf(Assembler::NotEqual, ins->snapshot()); + } +} + +void CodeGenerator::visitUDivConstant(LUDivConstant* ins) { + Register lhs = ToRegister(ins->numerator()); + Register output = ToRegister(ins->output()); + Register temp = ToRegister(ins->temp0()); + uint32_t d = ins->denominator(); + + MDiv* mir = ins->mir(); + +#ifdef JS_CODEGEN_X86 + // This emits the division answer into edx. + MOZ_ASSERT(output == edx); + MOZ_ASSERT(temp == eax); +#endif + + if (d == 0) { + if (mir->trapOnError()) { + masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); + } else if (mir->isTruncated()) { + masm.xorl(output, output); + } else { + bailout(ins->snapshot()); + } + return; + } + + // Compute the truncated division result in |output|. + UnsignedDivideWithConstant(masm, ins, output, temp); + + if (!mir->isTruncated()) { + masm.imull(Imm32(d), output, temp); + bailoutCmp32(Assembler::NotEqual, lhs, temp, ins->snapshot()); + } +} + +void CodeGenerator::visitUModConstant(LUModConstant* ins) { + Register lhs = ToRegister(ins->numerator()); + Register output = ToRegister(ins->output()); + Register temp = ToRegister(ins->temp0()); + uint32_t d = ins->denominator(); + + MMod* mir = ins->mir(); + +#ifdef JS_CODEGEN_X86 + // This emits the modulus answer into eax. + MOZ_ASSERT(output == eax); + MOZ_ASSERT(temp == edx); +#endif + + if (d == 0) { + if (mir->trapOnError()) { + masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); + } else if (mir->isTruncated()) { + masm.xorl(output, output); + } else { + bailout(ins->snapshot()); + } + return; + } + + // Compute the truncated division result in |temp|. + UnsignedDivideWithConstant(masm, ins, temp, output); + + // We now have the truncated division value in |temp|. If we're computing a + // modulus or checking whether the division resulted in an integer, we need + // to multiply the obtained value by d and finish the computation/check. + // + // output = lhs - d * temp + masm.imull(Imm32(d), temp, temp); + masm.movl(lhs, output); + masm.subl(temp, output); + + // The final result of the modulus op, just computed above by the + // sub instruction, can be a number in the range [2^31, 2^32). If + // this is the case and the modulus is not truncated, we must bail + // out. + if (!mir->isTruncated()) { + bailoutIf(Assembler::Signed, ins->snapshot()); } } @@ -1027,15 +1154,14 @@ void CodeGenerator::visitDivPowTwoI(LDivPowTwoI* ins) { if (!mir->isTruncated() && negativeDivisor) { // 0 divided by a negative number must return a double. - masm.test32(lhs, lhs); - bailoutIf(Assembler::Zero, ins->snapshot()); + bailoutTest32(Assembler::Zero, lhs, lhs, ins->snapshot()); } if (shift) { if (!mir->isTruncated()) { // If the remainder is != 0, bailout since this must be a double. - masm.test32(lhs, Imm32(UINT32_MAX >> (32 - shift))); - bailoutIf(Assembler::NonZero, ins->snapshot()); + bailoutTest32(Assembler::NonZero, lhs, Imm32(UINT32_MAX >> (32 - shift)), + ins->snapshot()); } if (mir->isUnsigned()) { @@ -1084,93 +1210,165 @@ void CodeGenerator::visitDivPowTwoI(LDivPowTwoI* ins) { masm.bind(&ok); } } else if (mir->isUnsigned() && !mir->isTruncated()) { - // Unsigned division by 1 can overflow if output is not - // truncated. - masm.test32(lhs, lhs); - bailoutIf(Assembler::Signed, ins->snapshot()); + // Unsigned division by 1 can overflow if output is not truncated. + bailoutTest32(Assembler::Signed, lhs, lhs, ins->snapshot()); } } -void CodeGenerator::visitDivOrModConstantI(LDivOrModConstantI* ins) { +template <class LDivOrMod> +static void DivideWithConstant(MacroAssembler& masm, LDivOrMod* ins, + Register result, Register temp) { Register lhs = ToRegister(ins->numerator()); - Register output = ToRegister(ins->output()); int32_t d = ins->denominator(); - // This emits the division answer into edx or the modulus answer into eax. - MOZ_ASSERT(output == eax || output == edx); - MOZ_ASSERT(lhs != eax && lhs != edx); - bool isDiv = (output == edx); + MOZ_ASSERT(lhs != result && lhs != temp); +#ifdef JS_CODEGEN_X86 + MOZ_ASSERT(result == edx && temp == eax); +#else + MOZ_ASSERT(result != temp); +#endif // The absolute value of the denominator isn't a power of 2 (see LDivPowTwoI // and LModPowTwoI). MOZ_ASSERT(!mozilla::IsPowerOfTwo(mozilla::Abs(d))); + auto* mir = ins->mir(); + // We will first divide by Abs(d), and negate the answer if d is negative. // If desired, this can be avoided by generalizing computeDivisionConstants. auto rmc = ReciprocalMulConstants::computeSignedDivisionConstants(d); // We first compute (M * n) >> 32, where M = rmc.multiplier. +#ifdef JS_CODEGEN_X86 masm.movl(Imm32(rmc.multiplier), eax); masm.imull(lhs); +#else + // Sign-extend |lhs| in preparation for a 64-bit multiplication. + masm.movslq(lhs, result); + masm.imulq(Imm32(rmc.multiplier), result, result); + if (rmc.multiplier > INT32_MAX || rmc.shiftAmount == 0) { + masm.shrq(Imm32(32), result); + } +#endif if (rmc.multiplier > INT32_MAX) { MOZ_ASSERT(rmc.multiplier < (int64_t(1) << 32)); - // We actually computed edx = ((int32_t(M) * n) >> 32) instead. Since - // (M * n) >> 32 is the same as (edx + n), we can correct for the overflow. - // (edx + n) can't overflow, as n and edx have opposite signs because - // int32_t(M) is negative. - masm.addl(lhs, edx); + // We actually computed result = ((int32_t(M) * n) >> 32) instead. Since + // (M * n) >> 32 is the same as (result + n), we can correct for the + // overflow. (result + n) can't overflow, as n and |result| have opposite + // signs because int32_t(M) is negative. + masm.addl(lhs, result); } // (M * n) >> (32 + shift) is the truncated division answer if n is // non-negative, as proved in the comments of computeDivisionConstants. We // must add 1 later if n is negative to get the right answer in all cases. - masm.sarl(Imm32(rmc.shiftAmount), edx); + if (rmc.shiftAmount > 0) { +#ifdef JS_CODEGEN_X86 + masm.sarl(Imm32(rmc.shiftAmount), result); +#else + if (rmc.multiplier > INT32_MAX) { + masm.sarl(Imm32(rmc.shiftAmount), result); + } else { + masm.sarq(Imm32(32 + rmc.shiftAmount), result); + } +#endif + } // We'll subtract -1 instead of adding 1, because (n < 0 ? -1 : 0) can be // computed with just a sign-extending shift of 31 bits. - if (ins->canBeNegativeDividend()) { - masm.movl(lhs, eax); - masm.sarl(Imm32(31), eax); - masm.subl(eax, edx); + if (mir->canBeNegativeDividend()) { + masm.movl(lhs, temp); + masm.sarl(Imm32(31), temp); + masm.subl(temp, result); } - // After this, edx contains the correct truncated division result. + // After this, |result| contains the correct truncated division result. if (d < 0) { - masm.negl(edx); + masm.negl(result); } +} + +void CodeGenerator::visitDivConstantI(LDivConstantI* ins) { + Register lhs = ToRegister(ins->numerator()); + Register output = ToRegister(ins->output()); + Register temp = ToRegister(ins->temp0()); + int32_t d = ins->denominator(); + + MDiv* mir = ins->mir(); - if (!isDiv) { - masm.imull(Imm32(-d), edx, eax); - masm.addl(lhs, eax); +#ifdef JS_CODEGEN_X86 + // This emits the division answer into edx. + MOZ_ASSERT(output == edx); + MOZ_ASSERT(temp == eax); +#endif + + if (d == 0) { + if (mir->trapOnError()) { + masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); + } else if (mir->isTruncated()) { + masm.xorl(output, output); + } else { + bailout(ins->snapshot()); + } + return; } - if (!ins->mir()->isTruncated()) { - if (isDiv) { - // This is a division op. Multiply the obtained value by d to check if - // the correct answer is an integer. This cannot overflow, since |d| > 1. - masm.imull(Imm32(d), edx, eax); - masm.cmp32(lhs, eax); - bailoutIf(Assembler::NotEqual, ins->snapshot()); + // Compute the truncated division result in |output|. + DivideWithConstant(masm, ins, output, temp); - // If lhs is zero and the divisor is negative, the answer should have - // been -0. - if (d < 0) { - masm.test32(lhs, lhs); - bailoutIf(Assembler::Zero, ins->snapshot()); - } - } else if (ins->canBeNegativeDividend()) { - // This is a mod op. If the computed value is zero and lhs - // is negative, the answer should have been -0. - Label done; + if (!mir->isTruncated()) { + // This is a division op. Multiply the obtained value by d to check if + // the correct answer is an integer. This cannot overflow, since |d| > 1. + masm.imull(Imm32(d), output, temp); + bailoutCmp32(Assembler::NotEqual, lhs, temp, ins->snapshot()); - masm.cmp32(lhs, Imm32(0)); - masm.j(Assembler::GreaterThanOrEqual, &done); + // If lhs is zero and the divisor is negative, the answer should have + // been -0. + if (d < 0) { + bailoutTest32(Assembler::Zero, lhs, lhs, ins->snapshot()); + } + } +} - masm.test32(eax, eax); - bailoutIf(Assembler::Zero, ins->snapshot()); +void CodeGenerator::visitModConstantI(LModConstantI* ins) { + Register lhs = ToRegister(ins->numerator()); + Register output = ToRegister(ins->output()); + Register temp = ToRegister(ins->temp0()); + int32_t d = ins->denominator(); + + MMod* mir = ins->mir(); + +#ifdef JS_CODEGEN_X86 + // This emits the modulus answer into eax. + MOZ_ASSERT(output == eax); + MOZ_ASSERT(temp == edx); +#endif - masm.bind(&done); + if (d == 0) { + if (mir->trapOnError()) { + masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); + } else if (mir->isTruncated()) { + masm.xorl(output, output); + } else { + bailout(ins->snapshot()); } + return; + } + + // Compute the truncated division result in |temp|. + DivideWithConstant(masm, ins, temp, output); + + // Compute the remainder in |output|: output = lhs - d * temp + masm.imull(Imm32(-d), temp, output); + masm.addl(lhs, output); + + if (!mir->isTruncated() && mir->canBeNegativeDividend()) { + // This is a mod op. If the computed value is zero and lhs + // is negative, the answer should have been -0. + Label done; + masm.branch32(Assembler::GreaterThanOrEqual, lhs, Imm32(0), &done); + bailoutTest32(Assembler::Zero, output, output, ins->snapshot()); + masm.bind(&done); } } @@ -1180,61 +1378,43 @@ void CodeGenerator::visitDivI(LDivI* ins) { Register rhs = ToRegister(ins->rhs()); Register output = ToRegister(ins->output()); - MDiv* mir = ins->mir(); - - MOZ_ASSERT_IF(lhs != rhs, rhs != eax); + MOZ_ASSERT(lhs == eax); + MOZ_ASSERT(rhs != eax); MOZ_ASSERT(rhs != edx); MOZ_ASSERT(remainder == edx); MOZ_ASSERT(output == eax); + MDiv* mir = ins->mir(); + Label done; OutOfLineCode* ool = nullptr; - // Put the lhs in eax, for either the negative overflow case or the regular - // divide case. - if (lhs != eax) { - masm.mov(lhs, eax); - } - // Handle divide by zero. if (mir->canBeDivideByZero()) { - masm.test32(rhs, rhs); if (mir->trapOnError()) { - Label nonZero; - masm.j(Assembler::NonZero, &nonZero); - masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); - masm.bind(&nonZero); + TrapIfDivideByZero(masm, ins, rhs); } else if (mir->canTruncateInfinities()) { - // Truncated division by zero is zero (Infinity|0 == 0) - if (!ool) { - ool = new (alloc()) LambdaOutOfLineCode([=](OutOfLineCode& ool) { - masm.mov(ImmWord(0), output); - masm.jmp(ool.rejoin()); - }); - } - masm.j(Assembler::Zero, ool->entry()); + ool = emitOutOfLineZeroForDivideByZero(rhs, output); } else { MOZ_ASSERT(mir->fallible()); - bailoutIf(Assembler::Zero, ins->snapshot()); + bailoutTest32(Assembler::Zero, rhs, rhs, ins->snapshot()); } } // Handle an integer overflow exception from -2147483648 / -1. if (mir->canBeNegativeOverflow()) { Label notOverflow; - masm.cmp32(lhs, Imm32(INT32_MIN)); - masm.j(Assembler::NotEqual, ¬Overflow); - masm.cmp32(rhs, Imm32(-1)); + masm.branch32(Assembler::NotEqual, lhs, Imm32(INT32_MIN), ¬Overflow); if (mir->trapOnError()) { - masm.j(Assembler::NotEqual, ¬Overflow); + masm.branch32(Assembler::NotEqual, rhs, Imm32(-1), ¬Overflow); masm.wasmTrap(wasm::Trap::IntegerOverflow, mir->trapSiteDesc()); } else if (mir->canTruncateOverflow()) { // (-INT32_MIN)|0 == INT32_MIN and INT32_MIN is already in the // output register (lhs == eax). - masm.j(Assembler::Equal, &done); + masm.branch32(Assembler::Equal, rhs, Imm32(-1), &done); } else { MOZ_ASSERT(mir->fallible()); - bailoutIf(Assembler::Equal, ins->snapshot()); + bailoutCmp32(Assembler::Equal, rhs, Imm32(-1), ins->snapshot()); } masm.bind(¬Overflow); } @@ -1242,24 +1422,18 @@ void CodeGenerator::visitDivI(LDivI* ins) { // Handle negative 0. if (!mir->canTruncateNegativeZero() && mir->canBeNegativeZero()) { Label nonzero; - masm.test32(lhs, lhs); - masm.j(Assembler::NonZero, &nonzero); - masm.cmp32(rhs, Imm32(0)); - bailoutIf(Assembler::LessThan, ins->snapshot()); + masm.branchTest32(Assembler::NonZero, lhs, lhs, &nonzero); + bailoutCmp32(Assembler::LessThan, rhs, Imm32(0), ins->snapshot()); masm.bind(&nonzero); } // Sign extend the lhs into edx to make (edx:eax), since idiv is 64-bit. - if (lhs != eax) { - masm.mov(lhs, eax); - } masm.cdq(); masm.idiv(rhs); if (!mir->canTruncateRemainder()) { // If the remainder is > 0, bailout since this must be a double. - masm.test32(remainder, remainder); - bailoutIf(Assembler::NonZero, ins->snapshot()); + bailoutTest32(Assembler::NonZero, remainder, remainder, ins->snapshot()); } masm.bind(&done); @@ -1344,42 +1518,27 @@ void CodeGenerator::visitModI(LModI* ins) { Register rhs = ToRegister(ins->rhs()); // Required to use idiv. - MOZ_ASSERT_IF(lhs != rhs, rhs != eax); + MOZ_ASSERT(lhs == eax); + MOZ_ASSERT(rhs != eax); MOZ_ASSERT(rhs != edx); MOZ_ASSERT(remainder == edx); MOZ_ASSERT(ToRegister(ins->temp0()) == eax); + MMod* mir = ins->mir(); + Label done; OutOfLineCode* ool = nullptr; ModOverflowCheck* overflow = nullptr; - // Set up eax in preparation for doing a div. - if (lhs != eax) { - masm.mov(lhs, eax); - } - - MMod* mir = ins->mir(); - // Prevent divide by zero. if (mir->canBeDivideByZero()) { - masm.test32(rhs, rhs); - if (mir->isTruncated()) { - if (mir->trapOnError()) { - Label nonZero; - masm.j(Assembler::NonZero, &nonZero); - masm.wasmTrap(wasm::Trap::IntegerDivideByZero, mir->trapSiteDesc()); - masm.bind(&nonZero); - } else { - if (!ool) { - ool = new (alloc()) LambdaOutOfLineCode([=](OutOfLineCode& ool) { - masm.mov(ImmWord(0), edx); - masm.jmp(ool.rejoin()); - }); - } - masm.j(Assembler::Zero, ool->entry()); - } + if (mir->trapOnError()) { + TrapIfDivideByZero(masm, ins, rhs); + } else if (mir->isTruncated()) { + ool = emitOutOfLineZeroForDivideByZero(rhs, remainder); } else { - bailoutIf(Assembler::Zero, ins->snapshot()); + MOZ_ASSERT(mir->fallible()); + bailoutTest32(Assembler::Zero, rhs, rhs, ins->snapshot()); } } @@ -1425,17 +1584,16 @@ void CodeGenerator::visitModI(LModI* ins) { masm.bind(&negative); // Prevent an integer overflow exception from -2147483648 % -1 - masm.cmp32(lhs, Imm32(INT32_MIN)); overflow = new (alloc()) ModOverflowCheck(ins, rhs); - masm.j(Assembler::Equal, overflow->entry()); + masm.branch32(Assembler::Equal, lhs, Imm32(INT32_MIN), overflow->entry()); masm.bind(overflow->rejoin()); + masm.cdq(); masm.idiv(rhs); if (!mir->isTruncated()) { // A remainder of 0 means that the rval must be -0, which is a double. - masm.test32(remainder, remainder); - bailoutIf(Assembler::Zero, ins->snapshot()); + bailoutTest32(Assembler::Zero, remainder, remainder, ins->snapshot()); } } diff --git a/js/src/jit/x86-shared/CodeGenerator-x86-shared.h b/js/src/jit/x86-shared/CodeGenerator-x86-shared.h @@ -84,6 +84,11 @@ class CodeGeneratorX86Shared : public CodeGeneratorShared { void emitTableSwitchDispatch(MTableSwitch* mir, Register index, Register base); + // Emit out-of-line code to zero |output| if |rhs| is zero. Used for truncated + // division and modulus instructions. + OutOfLineCode* emitOutOfLineZeroForDivideByZero(Register rhs, + Register output); + void generateInvalidateEpilogue(); template <typename T> diff --git a/js/src/jit/x86-shared/LIR-x86-shared.h b/js/src/jit/x86-shared/LIR-x86-shared.h @@ -1,130 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- - * vim: set ts=8 sts=2 et sw=2 tw=80: - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef jit_x86_shared_LIR_x86_shared_h -#define jit_x86_shared_LIR_x86_shared_h - -namespace js { -namespace jit { - -class LDivOrModConstantI : public LInstructionHelper<1, 1, 1> { - const int32_t denominator_; - - public: - LIR_HEADER(DivOrModConstantI) - - LDivOrModConstantI(const LAllocation& lhs, int32_t denominator, - const LDefinition& temp) - : LInstructionHelper(classOpcode), denominator_(denominator) { - setOperand(0, lhs); - setTemp(0, temp); - } - - const LAllocation* numerator() { return getOperand(0); } - int32_t denominator() const { return denominator_; } - MBinaryArithInstruction* mir() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - return static_cast<MBinaryArithInstruction*>(mir_); - } - bool canBeNegativeDividend() const { - if (mir_->isMod()) { - return mir_->toMod()->canBeNegativeDividend(); - } - return mir_->toDiv()->canBeNegativeDividend(); - } -}; - -// This class performs a simple x86 'div', yielding either a quotient or -// remainder depending on whether this instruction is defined to output eax -// (quotient) or edx (remainder). -class LUDivOrMod : public LBinaryMath<1> { - public: - LIR_HEADER(UDivOrMod); - - LUDivOrMod(const LAllocation& lhs, const LAllocation& rhs, - const LDefinition& temp) - : LBinaryMath(classOpcode) { - setOperand(0, lhs); - setOperand(1, rhs); - setTemp(0, temp); - } - - const LDefinition* remainder() { return getTemp(0); } - - const char* extraName() const { - return mir()->isTruncated() ? "Truncated" : nullptr; - } - - MBinaryArithInstruction* mir() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - return static_cast<MBinaryArithInstruction*>(mir_); - } - - bool canBeDivideByZero() const { - if (mir_->isMod()) { - return mir_->toMod()->canBeDivideByZero(); - } - return mir_->toDiv()->canBeDivideByZero(); - } - - bool trapOnError() const { - if (mir_->isMod()) { - return mir_->toMod()->trapOnError(); - } - return mir_->toDiv()->trapOnError(); - } - - wasm::TrapSiteDesc trapSiteDesc() const { - if (mir_->isMod()) { - return mir_->toMod()->trapSiteDesc(); - } - return mir_->toDiv()->trapSiteDesc(); - } -}; - -class LUDivOrModConstant : public LInstructionHelper<1, 1, 1> { - const uint32_t denominator_; - - public: - LIR_HEADER(UDivOrModConstant) - - LUDivOrModConstant(const LAllocation& lhs, uint32_t denominator, - const LDefinition& temp) - : LInstructionHelper(classOpcode), denominator_(denominator) { - setOperand(0, lhs); - setTemp(0, temp); - } - - const LAllocation* numerator() { return getOperand(0); } - uint32_t denominator() const { return denominator_; } - MBinaryArithInstruction* mir() const { - MOZ_ASSERT(mir_->isDiv() || mir_->isMod()); - return static_cast<MBinaryArithInstruction*>(mir_); - } - bool canBeNegativeDividend() const { - if (mir_->isMod()) { - return mir_->toMod()->canBeNegativeDividend(); - } - return mir_->toDiv()->canBeNegativeDividend(); - } - bool trapOnError() const { - if (mir_->isMod()) { - return mir_->toMod()->trapOnError(); - } - return mir_->toDiv()->trapOnError(); - } - wasm::TrapSiteDesc trapSiteDesc() const { - if (mir_->isMod()) { - return mir_->toMod()->trapSiteDesc(); - } - return mir_->toDiv()->trapSiteDesc(); - } -}; - -} // namespace jit -} // namespace js - -#endif /* jit_x86_shared_LIR_x86_shared_h */ diff --git a/js/src/jit/x86-shared/Lowering-x86-shared.cpp b/js/src/jit/x86-shared/Lowering-x86-shared.cpp @@ -179,40 +179,48 @@ void LIRGeneratorX86Shared::lowerDivI(MDiv* div) { int32_t shift = FloorLog2(Abs(rhs)); if (rhs != 0 && uint32_t(1) << shift == Abs(rhs)) { LAllocation lhs = useRegisterAtStart(div->lhs()); - LDivPowTwoI* lir; + // When truncated with maybe a non-zero remainder, we have to round the // result toward 0. This requires an extra register to round up/down // whether the left-hand-side is signed. + // + // If the numerator might be signed, and needs adjusting, then an extra + // lhs copy is needed to round the result of the integer division towards + // zero. + // + // Otherwise the numerator is unsigned, so does not need adjusting. bool needRoundNeg = div->canBeNegativeDividend() && div->isTruncated(); - if (!needRoundNeg) { - // Numerator is unsigned, so does not need adjusting. - lir = new (alloc()) LDivPowTwoI(lhs, lhs, shift, rhs < 0); - } else { - // Numerator might be signed, and needs adjusting, and an extra lhs copy - // is needed to round the result of the integer division towards zero. - lir = new (alloc()) - LDivPowTwoI(lhs, useRegister(div->lhs()), shift, rhs < 0); - } + LAllocation lhsCopy = + needRoundNeg ? useRegister(div->lhs()) : LAllocation(); + + auto* lir = new (alloc()) LDivPowTwoI(lhs, lhsCopy, shift, rhs < 0); if (div->fallible()) { assignSnapshot(lir, div->bailoutKind()); } defineReuseInput(lir, div, 0); return; } - if (rhs != 0) { - LDivOrModConstantI* lir; - lir = new (alloc()) - LDivOrModConstantI(useRegister(div->lhs()), rhs, tempFixed(eax)); - if (div->fallible()) { - assignSnapshot(lir, div->bailoutKind()); - } - defineFixed(lir, div, LAllocation(AnyRegister(edx))); - return; + +#ifdef JS_CODEGEN_X86 + auto* lir = new (alloc()) + LDivConstantI(useRegister(div->lhs()), tempFixed(eax), rhs); + if (div->fallible()) { + assignSnapshot(lir, div->bailoutKind()); } + defineFixed(lir, div, LAllocation(AnyRegister(edx))); +#else + auto* lir = + new (alloc()) LDivConstantI(useRegister(div->lhs()), temp(), rhs); + if (div->fallible()) { + assignSnapshot(lir, div->bailoutKind()); + } + define(lir, div); +#endif + return; } - LDivI* lir = new (alloc()) - LDivI(useRegister(div->lhs()), useRegister(div->rhs()), tempFixed(edx)); + auto* lir = new (alloc()) LDivI(useFixedAtStart(div->lhs(), eax), + useRegister(div->rhs()), tempFixed(edx)); if (div->fallible()) { assignSnapshot(lir, div->bailoutKind()); } @@ -224,7 +232,7 @@ void LIRGeneratorX86Shared::lowerModI(MMod* mod) { int32_t rhs = mod->rhs()->toConstant()->toInt32(); int32_t shift = FloorLog2(Abs(rhs)); if (rhs != 0 && uint32_t(1) << shift == Abs(rhs)) { - LModPowTwoI* lir = + auto* lir = new (alloc()) LModPowTwoI(useRegisterAtStart(mod->lhs()), shift); if (mod->fallible()) { assignSnapshot(lir, mod->bailoutKind()); @@ -232,20 +240,27 @@ void LIRGeneratorX86Shared::lowerModI(MMod* mod) { defineReuseInput(lir, mod, 0); return; } - if (rhs != 0) { - LDivOrModConstantI* lir; - lir = new (alloc()) - LDivOrModConstantI(useRegister(mod->lhs()), rhs, tempFixed(edx)); - if (mod->fallible()) { - assignSnapshot(lir, mod->bailoutKind()); - } - defineFixed(lir, mod, LAllocation(AnyRegister(eax))); - return; + +#ifdef JS_CODEGEN_X86 + auto* lir = new (alloc()) + LModConstantI(useRegister(mod->lhs()), tempFixed(edx), rhs); + if (mod->fallible()) { + assignSnapshot(lir, mod->bailoutKind()); } + defineFixed(lir, mod, LAllocation(AnyRegister(eax))); +#else + auto* lir = + new (alloc()) LModConstantI(useRegister(mod->lhs()), temp(), rhs); + if (mod->fallible()) { + assignSnapshot(lir, mod->bailoutKind()); + } + define(lir, mod); +#endif + return; } - LModI* lir = new (alloc()) - LModI(useRegister(mod->lhs()), useRegister(mod->rhs()), tempFixed(eax)); + auto* lir = new (alloc()) LModI(useFixedAtStart(mod->lhs(), eax), + useRegister(mod->rhs()), tempFixed(eax)); if (mod->fallible()) { assignSnapshot(lir, mod->bailoutKind()); } @@ -358,26 +373,35 @@ void LIRGeneratorX86Shared::lowerUDiv(MDiv* div) { uint32_t rhs = div->rhs()->toConstant()->toInt32(); int32_t shift = FloorLog2(rhs); - LAllocation lhs = useRegisterAtStart(div->lhs()); if (rhs != 0 && uint32_t(1) << shift == rhs) { - LDivPowTwoI* lir = new (alloc()) LDivPowTwoI(lhs, lhs, shift, false); + auto* lir = new (alloc()) LDivPowTwoI(useRegisterAtStart(div->lhs()), + LAllocation(), shift, false); if (div->fallible()) { assignSnapshot(lir, div->bailoutKind()); } defineReuseInput(lir, div, 0); } else { - LUDivOrModConstant* lir = new (alloc()) - LUDivOrModConstant(useRegister(div->lhs()), rhs, tempFixed(eax)); +#ifdef JS_CODEGEN_X86 + auto* lir = new (alloc()) + LUDivConstant(useRegister(div->lhs()), tempFixed(eax), rhs); if (div->fallible()) { assignSnapshot(lir, div->bailoutKind()); } defineFixed(lir, div, LAllocation(AnyRegister(edx))); +#else + auto* lir = + new (alloc()) LUDivConstant(useRegister(div->lhs()), temp(), rhs); + if (div->fallible()) { + assignSnapshot(lir, div->bailoutKind()); + } + define(lir, div); +#endif } return; } - LUDivOrMod* lir = new (alloc()) LUDivOrMod( - useRegister(div->lhs()), useRegister(div->rhs()), tempFixed(edx)); + auto* lir = new (alloc()) LUDiv(useFixedAtStart(div->lhs(), eax), + useRegister(div->rhs()), tempFixed(edx)); if (div->fallible()) { assignSnapshot(lir, div->bailoutKind()); } @@ -390,25 +414,34 @@ void LIRGeneratorX86Shared::lowerUMod(MMod* mod) { int32_t shift = FloorLog2(rhs); if (rhs != 0 && uint32_t(1) << shift == rhs) { - LModPowTwoI* lir = + auto* lir = new (alloc()) LModPowTwoI(useRegisterAtStart(mod->lhs()), shift); if (mod->fallible()) { assignSnapshot(lir, mod->bailoutKind()); } defineReuseInput(lir, mod, 0); } else { - LUDivOrModConstant* lir = new (alloc()) - LUDivOrModConstant(useRegister(mod->lhs()), rhs, tempFixed(edx)); +#ifdef JS_CODEGEN_X86 + auto* lir = new (alloc()) + LUModConstant(useRegister(mod->lhs()), tempFixed(edx), rhs); if (mod->fallible()) { assignSnapshot(lir, mod->bailoutKind()); } defineFixed(lir, mod, LAllocation(AnyRegister(eax))); +#else + auto* lir = + new (alloc()) LUModConstant(useRegister(mod->lhs()), temp(), rhs); + if (mod->fallible()) { + assignSnapshot(lir, mod->bailoutKind()); + } + define(lir, mod); +#endif } return; } - LUDivOrMod* lir = new (alloc()) LUDivOrMod( - useRegister(mod->lhs()), useRegister(mod->rhs()), tempFixed(eax)); + auto* lir = new (alloc()) LUMod(useFixedAtStart(mod->lhs(), eax), + useRegister(mod->rhs()), tempFixed(eax)); if (mod->fallible()) { assignSnapshot(lir, mod->bailoutKind()); } diff --git a/js/src/jit/x86/CodeGenerator-x86.cpp b/js/src/jit/x86/CodeGenerator-x86.cpp @@ -886,10 +886,10 @@ void CodeGenerator::visitDivOrModI64(LDivOrModI64* lir) { masm.branch64(Assembler::NotEqual, rhs, Imm64(-1), ¬Overflow); if (mir->isWasmBuiltinModI64()) { masm.xor64(output, output); + masm.jump(&done); } else { masm.wasmTrap(wasm::Trap::IntegerOverflow, lir->trapSiteDesc()); } - masm.jump(&done); masm.bind(¬Overflow); } diff --git a/layout/base/PresShell.cpp b/layout/base/PresShell.cpp @@ -1243,6 +1243,13 @@ void PresShell::Destroy() { weakFrame->Clear(this); } + // Clear the embedding frame only after tearing down the frame tree, since we + // rely on reaching the display root frame from frame destruction. + if (nsSubDocumentFrame* f = GetInProcessEmbedderFrame()) { + f->RemoveEmbeddingPresShell(this); + } + mEmbedderFrame = nullptr; + // Terminate AccessibleCaretEventHub after tearing down the frame tree so that // we don't need to remove caret element's frame in // AccessibleCaret::RemoveCaretElement(). @@ -1284,6 +1291,15 @@ nsRefreshDriver* PresShell::GetRefreshDriver() const { return mPresContext ? mPresContext->RefreshDriver() : nullptr; } +// NOTE(emilio): It'd be ideal if instead of this explicit tracking we could +// rely on mDocument->GetEmbedderElement()->GetPrimaryFrame() + relevant +// null-checks. However, given how things are set up now, the embedder element +// in BrowsingContext / Window get cleared before tearing down the pres shell, +// and RDL relies on getting ahold of it to get the display root. +void PresShell::SetInProcessEmbedderFrame(nsSubDocumentFrame* aFrame) { + mEmbedderFrame = aFrame; +} + void PresShell::SetAuthorStyleDisabled(bool aStyleDisabled) { if (aStyleDisabled != StyleSet()->GetAuthorStyleDisabled()) { StyleSet()->SetAuthorStyleDisabled(aStyleDisabled); @@ -6162,20 +6178,17 @@ void PresShell::RebuildApproximateFrameVisibilityDisplayList( DecApproximateVisibleCount(oldApproximatelyVisibleFrames); } -/* static */ -void PresShell::ClearApproximateFrameVisibilityVisited(nsView* aView, - bool aClear) { - nsViewManager* vm = aView->GetViewManager(); - if (aClear) { - PresShell* presShell = vm->GetPresShell(); - if (!presShell->mApproximateFrameVisibilityVisited) { - presShell->ClearApproximatelyVisibleFramesList(); - } - presShell->mApproximateFrameVisibilityVisited = false; - } - for (nsView* v = aView->GetFirstChild(); v; v = v->GetNextSibling()) { - ClearApproximateFrameVisibilityVisited(v, v->GetViewManager() != vm); +void PresShell::ClearApproximateFrameVisibilityVisited() { + if (!mApproximateFrameVisibilityVisited) { + ClearApproximatelyVisibleFramesList(); } + mApproximateFrameVisibilityVisited = false; + mDocument->EnumerateSubDocuments([](Document& aSubdoc) { + if (auto* ps = aSubdoc.GetPresShell()) { + ps->ClearApproximateFrameVisibilityVisited(); + } + return CallState::Continue; + }); } void PresShell::ClearApproximatelyVisibleFramesList( @@ -6383,7 +6396,7 @@ void PresShell::DoUpdateApproximateFrameVisibility(bool aRemoveOnly) { } RebuildApproximateFrameVisibility(/* aRect = */ nullptr, aRemoveOnly); - ClearApproximateFrameVisibilityVisited(rootFrame->GetView(), true); + ClearApproximateFrameVisibilityVisited(); #ifdef DEBUG_FRAME_VISIBILITY_DISPLAY_LIST // This can be used to debug the frame walker by comparing beforeFrameList @@ -6414,7 +6427,7 @@ void PresShell::DoUpdateApproximateFrameVisibility(bool aRemoveOnly) { RebuildApproximateFrameVisibilityDisplayList(list); - ClearApproximateFrameVisibilityVisited(rootFrame->GetView(), true); + ClearApproximateFrameVisibilityVisited(); list.DeleteAll(&builder); #endif @@ -6971,17 +6984,16 @@ void PresShell::DisableNonTestMouseEvents(bool aDisable) { nsPoint PresShell::GetEventLocation(const WidgetMouseEvent& aEvent) const { nsIFrame* rootFrame = GetRootFrame(); - if (rootFrame) { - RelativeTo relativeTo{rootFrame}; - if (rootFrame->PresContext()->IsRootContentDocumentCrossProcess()) { - relativeTo.mViewportType = ViewportType::Visual; - } - return nsLayoutUtils::GetEventCoordinatesRelativeTo(&aEvent, relativeTo); + if (!rootFrame) { + // Matches old TranslateWidgetToView behavior + return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); } - nsView* rootView = mViewManager->GetRootView(); - return nsLayoutUtils::TranslateWidgetToView(mPresContext, aEvent.mWidget, - aEvent.mRefPoint, rootView); + RelativeTo relativeTo{rootFrame}; + if (rootFrame->PresContext()->IsRootContentDocumentCrossProcess()) { + relativeTo.mViewportType = ViewportType::Visual; + } + return nsLayoutUtils::GetEventCoordinatesRelativeTo(&aEvent, relativeTo); } void PresShell::RecordPointerLocation(WidgetGUIEvent* aEvent) { @@ -9985,22 +9997,24 @@ bool PresShell::EventHandler::AdjustContextMenuKeyEvent( // up in the upper left of the relevant content area before we create // the DOM event. Since we never call InitMouseEvent() on the event, // the client X/Y will be 0,0. We can make use of that if the widget is null. - // Use the root view manager's widget since it's most likely to have one, - // and the coordinates returned by GetCurrentItemAndPositionForElement - // are relative to the widget of the root of the root view manager. + // Use the root widget since it's most likely to exist, and the coordinates + // returned by GetCurrentItemAndPositionForElement are relative to it. nsRootPresContext* rootPC = GetPresContext()->GetRootPresContext(); - aMouseEvent->mRefPoint = LayoutDeviceIntPoint(0, 0); + aMouseEvent->mRefPoint = LayoutDeviceIntPoint(); if (rootPC) { aMouseEvent->mWidget = rootPC->PresShell()->GetRootWidget(); if (aMouseEvent->mWidget) { // default the refpoint to the topleft of our document - nsPoint offset(0, 0); - nsIFrame* rootFrame = FrameConstructor()->GetRootFrame(); - if (rootFrame) { - nsView* view = rootFrame->GetClosestView(&offset); - offset += view->GetOffsetToWidget(aMouseEvent->mWidget); - aMouseEvent->mRefPoint = LayoutDeviceIntPoint::FromAppUnitsToNearest( - offset, GetPresContext()->AppUnitsPerDevPixel()); + nsPoint frameToWidgetOffset; + if (nsIFrame* rootFrame = FrameConstructor()->GetRootFrame()) { + nsIWidget* widget = rootFrame->GetNearestWidget(frameToWidgetOffset); + MOZ_ASSERT(widget, "If rootPC has a widget, so should we"); + auto widgetToWidgetOffset = + nsLayoutUtils::WidgetToWidgetOffset(widget, aMouseEvent->mWidget); + aMouseEvent->mRefPoint = + widgetToWidgetOffset + + LayoutDeviceIntPoint::FromAppUnitsToNearest( + frameToWidgetOffset, GetPresContext()->AppUnitsPerDevPixel()); } } } else { @@ -10133,22 +10147,24 @@ bool PresShell::EventHandler::PrepareToUseCaretPosition( nsPresContext* presContext = GetPresContext(); - // get caret position relative to the closest view + // get caret position relative to the closest widget nsRect caretCoords; nsIFrame* caretFrame = caret->GetGeometry(&caretCoords); if (!caretFrame) { return false; } - nsPoint viewOffset; - nsView* view = caretFrame->GetClosestView(&viewOffset); - if (!view) { + nsPoint widgetOffset; + nsIWidget* widget = caretFrame->GetNearestWidget(widgetOffset); + if (!widget) { return false; } // and then get the caret coords relative to the event widget if (aEventWidget) { - viewOffset += view->GetOffsetToWidget(aEventWidget); + widgetOffset += LayoutDeviceIntPoint::ToAppUnits( + nsLayoutUtils::WidgetToWidgetOffset(widget, aEventWidget), + presContext->AppUnitsPerDevPixel()); } - caretCoords.MoveBy(viewOffset); + caretCoords.MoveBy(widgetOffset); // caret coordinates are in app units, convert to pixels aTargetPt.x = @@ -10238,21 +10254,21 @@ void PresShell::EventHandler::GetCurrentItemAndPositionForElement( focusedContent = item; } - nsIFrame* frame = focusedContent->GetPrimaryFrame(); - if (frame) { + if (nsIFrame* frame = focusedContent->GetPrimaryFrame()) { NS_ASSERTION( frame->PresContext() == GetPresContext(), "handling event for focused content that is not in our document?"); - nsPoint frameOrigin(0, 0); + nsPoint widgetOffset(0, 0); - // Get the frame's origin within its view - nsView* view = frame->GetClosestView(&frameOrigin); - NS_ASSERTION(view, "No view for frame"); + // Get the frame's origin within its closest widget + nsIWidget* widget = frame->GetNearestWidget(widgetOffset); - // View's origin relative the widget + // And make it relative to aRootWidget if (aRootWidget) { - frameOrigin += view->GetOffsetToWidget(aRootWidget); + widgetOffset += LayoutDeviceIntPoint::ToAppUnits( + nsLayoutUtils::WidgetToWidgetOffset(widget, aRootWidget), + frame->PresContext()->AppUnitsPerDevPixel()); } // Start context menu down and to the right from top left of frame @@ -10284,9 +10300,9 @@ void PresShell::EventHandler::GetCurrentItemAndPositionForElement( } } - aTargetPt.x = presContext->AppUnitsToDevPixels(frameOrigin.x); + aTargetPt.x = presContext->AppUnitsToDevPixels(widgetOffset.x); aTargetPt.y = - presContext->AppUnitsToDevPixels(frameOrigin.y + extra + extraTreeY); + presContext->AppUnitsToDevPixels(widgetOffset.y + extra + extraTreeY); } NS_IF_ADDREF(*aTargetToUse = focusedContent); @@ -10327,6 +10343,18 @@ void PresShell::WillPaint() { FlushPendingNotifications(ChangesToFlush(FlushType::InterruptibleLayout, /* aFlushAnimations = */ false, /* aUpdateRelevancy = */ false)); + if (mIsDestroying) { + return; + } + mDocument->EnumerateSubDocuments( + [](Document& aSubdoc) MOZ_CAN_RUN_SCRIPT_BOUNDARY_LAMBDA { + if (RefPtr ps = aSubdoc.GetPresShell()) { + if (!ps->IsUnderHiddenEmbedderElement()) { + ps->WillPaint(); + } + } + return CallState::Continue; + }); } void PresShell::DidPaintWindow() { @@ -10351,26 +10379,9 @@ void PresShell::DidPaintWindow() { } nsSubDocumentFrame* PresShell::GetInProcessEmbedderFrame() const { - if (!mViewManager) { - return nullptr; - } - // We may not have a root frame yet, so use views. - nsView* view = mViewManager->GetRootView(); - if (!view) { - return nullptr; - } - view = view->GetParent(); // anonymous inner view - if (!view) { - return nullptr; - } - view = view->GetParent(); // subdocumentframe's view - if (!view) { - return nullptr; - } - - nsIFrame* f = view->GetFrame(); + nsIFrame* f = mEmbedderFrame.GetFrame(); MOZ_ASSERT_IF(f, f->IsSubDocumentFrame()); - return do_QueryFrame(f); + return static_cast<nsSubDocumentFrame*>(f); } bool PresShell::IsVisible() const { @@ -12231,7 +12242,19 @@ void PresShell::UpdateImageLockingState() { } nsIWidget* PresShell::GetRootWidget() const { - return mViewManager ? mViewManager->GetRootWidget() : nullptr; + if (!mPresContext) { + return nullptr; + } + for (nsPresContext* pc = mPresContext; pc; pc = pc->GetParentPresContext()) { + if (auto* vm = pc->PresShell()->GetViewManager()) { + if (auto* view = vm->GetRootView()) { + if (auto* widget = view->GetWidget()) { + return widget; + } + } + } + } + return nullptr; } PresShell* PresShell::GetRootPresShell() const { diff --git a/layout/base/PresShell.h b/layout/base/PresShell.h @@ -463,6 +463,7 @@ class PresShell final : public nsStubDocumentObserver, // Get the current frame of our embedder, if it's in our same process. nsSubDocumentFrame* GetInProcessEmbedderFrame() const; + void SetInProcessEmbedderFrame(nsSubDocumentFrame*); /** * Get root scroll container frame from the frame constructor. @@ -3157,8 +3158,7 @@ class PresShell final : public nsStubDocumentObserver, void ClearApproximatelyVisibleFramesList( const Maybe<OnNonvisible>& aNonvisibleAction = Nothing()); - static void ClearApproximateFrameVisibilityVisited(nsView* aView, - bool aClear); + void ClearApproximateFrameVisibilityVisited(); static void MarkFramesInListApproximatelyVisible(const nsDisplayList& aList); void MarkFramesInSubtreeApproximatelyVisible(nsIFrame* aFrame, const nsRect& aRect, @@ -3359,6 +3359,9 @@ class PresShell final : public nsStubDocumentObserver, // The focus sequence number of the last processed input event uint64_t mAPZFocusSequenceNumber; + // The nsSubDocumentFrame* that is embedding us. + WeakFrame mEmbedderFrame; + nscoord mLastAnchorScrollPositionY = 0; // Most recent canvas background color. diff --git a/layout/base/nsDocumentViewer.cpp b/layout/base/nsDocumentViewer.cpp @@ -126,11 +126,9 @@ #include "mozilla/dom/ScriptLoader.h" #include "mozilla/dom/WindowGlobalChild.h" -namespace mozilla { -namespace dom { +namespace mozilla::dom { class PrintPreviewResultInfo; -} // namespace dom -} // namespace mozilla +} // namespace mozilla::dom using namespace mozilla; using namespace mozilla::dom; @@ -339,15 +337,9 @@ class nsDocumentViewer final : public nsIDocumentViewer, * Creates a view manager, root view, and widget for the root view, setting * mViewManager and mWindow. * @param aSize the initial size in appunits - * @param aContainerView the container view to hook our root view up - * to as a child, or null if this will be the root view manager */ - void MakeWindow(const nsSize& aSize, nsView* aContainerView); - - /** - * Create our device context - */ - nsresult CreateDeviceContext(nsView* aContainerView); + void MakeWindow(const nsSize& aSize); + nsresult CreateDeviceContext(nsSubDocumentFrame* aContainerFrame); /** * If aDoCreation is true, this creates the device context, creates a @@ -692,7 +684,8 @@ nsresult nsDocumentViewer::InitPresentationStuff(bool aDoInitialReflow) { nsCOMPtr<Document> doc = mDocument; RefPtr<nsPresContext> presContext = mPresContext; RefPtr<nsViewManager> viewManager = mViewManager; - mPresShell = doc->CreatePresShell(presContext, viewManager); + mPresShell = + doc->CreatePresShell(presContext, viewManager, FindContainerFrame()); if (!mPresShell) { return NS_ERROR_FAILURE; } @@ -769,8 +762,8 @@ nsresult nsDocumentViewer::InitPresentationStuff(bool aDoInitialReflow) { static already_AddRefed<nsPresContext> CreatePresContext( Document* aDocument, nsPresContext::nsPresContextType aType, - nsView* aContainerView) { - RefPtr<nsPresContext> result = aContainerView + nsIFrame* aContainerFrame) { + RefPtr<nsPresContext> result = aContainerFrame ? new nsPresContext(aDocument, aType) : new nsRootPresContext(aDocument, aType); @@ -797,11 +790,11 @@ nsresult nsDocumentViewer::InitInternal( nsresult rv = NS_OK; NS_ENSURE_TRUE(mDocument, NS_ERROR_NULL_POINTER); - nsView* containerView = FindContainerView(); + nsSubDocumentFrame* containerFrame = FindContainerFrame(); bool makeCX = false; if (aDoCreation) { - nsresult rv = CreateDeviceContext(containerView); + nsresult rv = CreateDeviceContext(containerFrame); NS_ENSURE_SUCCESS(rv, rv); // XXXbz this is a nasty hack to do with the fact that we create @@ -809,7 +802,7 @@ nsresult nsDocumentViewer::InitInternal( // it in one place (Show()) and require that callers call init(), open(), // show() in that order or something. if (!mPresContext && - (aParentWidget || containerView || mDocument->IsBeingUsedAsImage() || + (aParentWidget || containerFrame || mDocument->IsBeingUsedAsImage() || (mDocument->GetDisplayDocument() && mDocument->GetDisplayDocument()->GetPresShell()))) { // Create presentation context @@ -818,7 +811,7 @@ nsresult nsDocumentViewer::InitInternal( // is calling this method } else { mPresContext = CreatePresContext( - mDocument, nsPresContext::eContext_Galley, containerView); + mDocument, nsPresContext::eContext_Galley, containerFrame); } NS_ENSURE_TRUE(mPresContext, NS_ERROR_OUT_OF_MEMORY); @@ -846,8 +839,7 @@ nsresult nsDocumentViewer::InitInternal( // FlushPendingNotifications() calls down the road... MakeWindow(nsSize(mPresContext->DevPixelsToAppUnits(aBounds.width), - mPresContext->DevPixelsToAppUnits(aBounds.height)), - containerView); + mPresContext->DevPixelsToAppUnits(aBounds.height))); Hide(); #ifdef NS_PRINT_PREVIEW @@ -896,7 +888,6 @@ nsresult nsDocumentViewer::InitInternal( if (aDoCreation && mPresContext) { // The ViewManager and Root View was created above (in // MakeWindow())... - rv = InitPresentationStuff(!makeCX); } @@ -1606,32 +1597,9 @@ nsDocumentViewer::Destroy() { mSHEntry->SetSticky(mIsSticky); mIsSticky = true; - // Remove our root view from the view hierarchy. - if (mPresShell) { - nsViewManager* vm = mPresShell->GetViewManager(); - if (vm) { - nsView* rootView = vm->GetRootView(); - - if (rootView) { - nsView* rootViewParent = rootView->GetParent(); - if (rootViewParent) { - nsView* subdocview = rootViewParent->GetParent(); - if (subdocview) { - nsIFrame* f = subdocview->GetFrame(); - if (f) { - nsSubDocumentFrame* s = do_QueryFrame(f); - if (s) { - s->ClearDisplayItems(); - } - } - } - nsViewManager* parentVM = rootViewParent->GetViewManager(); - if (parentVM) { - parentVM->RemoveChild(rootView); - } - } - } - } + // Clear our display items. + if (nsSubDocumentFrame* f = FindContainerFrame()) { + f->ClearDisplayItems(); } Hide(); @@ -2073,16 +2041,16 @@ nsDocumentViewer::Show() { } } - nsView* containerView = FindContainerView(); + nsSubDocumentFrame* containerFrame = FindContainerFrame(); - nsresult rv = CreateDeviceContext(containerView); + nsresult rv = CreateDeviceContext(containerFrame); NS_ENSURE_SUCCESS(rv, rv); // Create presentation context NS_ASSERTION(!mPresContext, "Shouldn't have a prescontext if we have no shell!"); mPresContext = CreatePresContext(mDocument, nsPresContext::eContext_Galley, - containerView); + containerFrame); NS_ENSURE_TRUE(mPresContext, NS_ERROR_OUT_OF_MEMORY); rv = mPresContext->Init(mDeviceContext); @@ -2092,8 +2060,7 @@ nsDocumentViewer::Show() { } MakeWindow(nsSize(mPresContext->DevPixelsToAppUnits(mBounds.width), - mPresContext->DevPixelsToAppUnits(mBounds.height)), - containerView); + mPresContext->DevPixelsToAppUnits(mBounds.height))); if (mPresContext) { Hide(); @@ -2208,7 +2175,7 @@ nsDocumentViewer::ClearHistoryEntry() { //------------------------------------------------------- -void nsDocumentViewer::MakeWindow(const nsSize& aSize, nsView* aContainerView) { +void nsDocumentViewer::MakeWindow(const nsSize& aSize) { if (GetIsPrintPreview()) { return; } @@ -2218,8 +2185,7 @@ void nsDocumentViewer::MakeWindow(const nsSize& aSize, nsView* aContainerView) { // The root view is always at 0,0. nsRect tbounds(nsPoint(), aSize); // Create a view - nsView* view = mViewManager->CreateView(tbounds, aContainerView); - MOZ_ASSERT(view); + nsView* view = mViewManager->CreateView(tbounds, nullptr); // Create a widget if we were given a parent widget or don't have a // container view that we can hook up to without a widget. @@ -2227,7 +2193,7 @@ void nsDocumentViewer::MakeWindow(const nsSize& aSize, nsView* aContainerView) { // because when they're displayed, they're painted into *another* document's // widget. if (!mDocument->IsResourceDoc()) { - MOZ_ASSERT_IF(!aContainerView, mParentWidget); + MOZ_ASSERT_IF(!FindContainerFrame(), mParentWidget); if (mParentWidget) { // Reuse the top level parent widget. view->AttachToTopLevelWidget(mParentWidget); @@ -2256,7 +2222,7 @@ void nsDocumentViewer::DetachFromTopLevelWidget() { mAttachedToParent = false; } -nsView* nsDocumentViewer::FindContainerView() { +nsSubDocumentFrame* nsDocumentViewer::FindContainerFrame() { if (!mContainer) { return nullptr; } @@ -2287,22 +2253,20 @@ nsView* nsDocumentViewer::FindContainerView() { return nullptr; } - NS_ASSERTION(subdocFrame->GetView(), "Subdoc frames must have views"); - return static_cast<nsSubDocumentFrame*>(subdocFrame)->EnsureInnerView(); + return static_cast<nsSubDocumentFrame*>(subdocFrame); } -nsresult nsDocumentViewer::CreateDeviceContext(nsView* aContainerView) { +nsresult nsDocumentViewer::CreateDeviceContext( + nsSubDocumentFrame* aContainerFrame) { MOZ_ASSERT(!mPresShell && !mWindow, "This will screw up our existing presentation"); MOZ_ASSERT(mDocument, "Gotta have a document here"); - Document* doc = mDocument->GetDisplayDocument(); - if (doc) { - NS_ASSERTION(!aContainerView, + if (Document* doc = mDocument->GetDisplayDocument()) { + NS_ASSERTION(!aContainerFrame, "External resource document embedded somewhere?"); // We want to use our display document's device context if possible - nsPresContext* ctx = doc->GetPresContext(); - if (ctx) { + if (nsPresContext* ctx = doc->GetPresContext()) { mDeviceContext = ctx->DeviceContext(); return NS_OK; } @@ -2311,8 +2275,8 @@ nsresult nsDocumentViewer::CreateDeviceContext(nsView* aContainerView) { // Create a device context even if we already have one, since our widget // might have changed. nsIWidget* widget = nullptr; - if (aContainerView) { - widget = aContainerView->GetNearestWidget(nullptr); + if (aContainerFrame) { + widget = aContainerFrame->GetNearestWidget(); } if (!widget) { widget = mParentWidget; @@ -3345,14 +3309,13 @@ NS_IMETHODIMP nsDocumentViewer::SetPrintSettingsForSubdocument( NS_ENSURE_SUCCESS(rv, rv); mPresContext = CreatePresContext( - mDocument, nsPresContext::eContext_PrintPreview, FindContainerView()); + mDocument, nsPresContext::eContext_PrintPreview, FindContainerFrame()); mPresContext->SetPrintSettings(aPrintSettings); rv = mPresContext->Init(mDeviceContext); NS_ENSURE_SUCCESS(rv, rv); MakeWindow(nsSize(mPresContext->DevPixelsToAppUnits(mBounds.width), - mPresContext->DevPixelsToAppUnits(mBounds.height)), - FindContainerView()); + mPresContext->DevPixelsToAppUnits(mBounds.height))); MOZ_TRY(InitPresentationStuff(true)); } @@ -3388,7 +3351,7 @@ NS_IMETHODIMP nsDocumentViewer::SetPageModeForTesting( NS_ENSURE_STATE(mDocument); if (aPageMode) { mPresContext = CreatePresContext( - mDocument, nsPresContext::eContext_PageLayout, FindContainerView()); + mDocument, nsPresContext::eContext_PageLayout, FindContainerFrame()); NS_ENSURE_TRUE(mPresContext, NS_ERROR_OUT_OF_MEMORY); mPresContext->SetPaginatedScrolling(true); mPresContext->SetPrintSettings(aPrintSettings); @@ -3448,18 +3411,8 @@ void nsDocumentViewer::DestroyPresShell() { } void nsDocumentViewer::InvalidatePotentialSubDocDisplayItem() { - if (mViewManager) { - if (nsView* rootView = mViewManager->GetRootView()) { - if (nsView* rootViewParent = rootView->GetParent()) { - if (nsView* subdocview = rootViewParent->GetParent()) { - if (nsIFrame* f = subdocview->GetFrame()) { - if (nsSubDocumentFrame* s = do_QueryFrame(f)) { - s->MarkNeedsDisplayItemRebuild(); - } - } - } - } - } + if (nsSubDocumentFrame* f = FindContainerFrame()) { + f->MarkNeedsDisplayItemRebuild(); } } diff --git a/layout/base/nsLayoutUtils.cpp b/layout/base/nsLayoutUtils.cpp @@ -1211,37 +1211,6 @@ nsIFrame* nsLayoutUtils::GetLastSibling(nsIFrame* aFrame) { } // static -nsView* nsLayoutUtils::FindSiblingViewFor(nsView* aParentView, - nsIFrame* aFrame) { - nsIFrame* parentViewFrame = aParentView->GetFrame(); - nsIContent* parentViewContent = - parentViewFrame ? parentViewFrame->GetContent() : nullptr; - for (nsView* insertBefore = aParentView->GetFirstChild(); insertBefore; - insertBefore = insertBefore->GetNextSibling()) { - nsIFrame* f = insertBefore->GetFrame(); - if (!f) { - // this view could be some anonymous view attached to a meaningful parent - for (nsView* searchView = insertBefore->GetParent(); searchView; - searchView = searchView->GetParent()) { - f = searchView->GetFrame(); - if (f) { - break; - } - } - NS_ASSERTION(f, "Can't find a frame anywhere!"); - } - if (!f || !aFrame->GetContent() || !f->GetContent() || - nsContentUtils::CompareTreePosition<TreeKind::Flat>( - aFrame->GetContent(), f->GetContent(), parentViewContent) > 0) { - // aFrame's content is after f's content (or we just don't know), - // so put our view before f's view - return insertBefore; - } - } - return nullptr; -} - -// static ScrollContainerFrame* nsLayoutUtils::GetScrollContainerFrameFor( const nsIFrame* aScrolledFrame) { nsIFrame* frame = aScrolledFrame->GetParent(); @@ -1516,15 +1485,21 @@ nsPoint GetEventCoordinatesRelativeTo(nsIWidget* aWidget, rootFrame = f; } - nsView* rootView = rootFrame->GetView(); - if (!rootView) { - return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); - } - - nsPoint widgetToView = nsLayoutUtils::TranslateWidgetToView( - rootFrame->PresContext(), aWidget, aPoint, rootView); + nsPoint widgetToRoot = [&] { + nsPresContext* pc = rootFrame->PresContext(); + nsPoint widgetOffset; + nsIWidget* widget = rootFrame->GetNearestWidget(widgetOffset); + if (NS_WARN_IF(!widget)) { + return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); + } + LayoutDeviceIntPoint widgetPoint = + aPoint + nsLayoutUtils::WidgetToWidgetOffset(aWidget, widget); + nsPoint widgetAppUnits(pc->DevPixelsToAppUnits(widgetPoint.x), + pc->DevPixelsToAppUnits(widgetPoint.y)); + return widgetAppUnits - widgetOffset; + }(); - if (widgetToView == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { + if (widgetToRoot == nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE)) { return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); } @@ -1532,20 +1507,20 @@ nsPoint GetEventCoordinatesRelativeTo(nsIWidget* aWidget, // is in. int32_t rootAPD = rootFrame->PresContext()->AppUnitsPerDevPixel(); int32_t localAPD = frame->PresContext()->AppUnitsPerDevPixel(); - widgetToView = widgetToView.ScaleToOtherAppUnits(rootAPD, localAPD); + widgetToRoot = widgetToRoot.ScaleToOtherAppUnits(rootAPD, localAPD); /* If we encountered a transform, we can't do simple arithmetic to figure * out how to convert back to aFrame's coordinates and must use the CTM. */ if (transformFound || frame->IsInSVGTextSubtree()) { return nsLayoutUtils::TransformRootPointToFrame(ViewportType::Visual, - aFrame, widgetToView); + aFrame, widgetToRoot); } /* Otherwise, all coordinate systems are translations of one another, * so we can just subtract out the difference. */ - return widgetToView - frame->GetOffsetToCrossDoc(rootFrame); + return widgetToRoot - frame->GetOffsetToCrossDoc(rootFrame); } nsPoint nsLayoutUtils::GetEventCoordinatesRelativeTo( @@ -2384,6 +2359,19 @@ nsRect nsLayoutUtils::TransformFrameRectToAncestor( result, aAncestor.mFrame->PresContext()->AppUnitsPerDevPixel()); } +Maybe<nsPoint> nsLayoutUtils::FrameToWidgetOffset(const nsIFrame* aFrame, + nsIWidget* aWidget) { + nsPoint toNearestOffset; + auto* nearest = aFrame->GetNearestWidget(toNearestOffset); + if (!nearest) { + return {}; + } + return Some(toNearestOffset + + LayoutDeviceIntPoint::ToAppUnits( + WidgetToWidgetOffset(nearest, aWidget), + aFrame->PresContext()->AppUnitsPerDevPixel())); +} + LayoutDeviceIntPoint nsLayoutUtils::WidgetToWidgetOffset(nsIWidget* aFrom, nsIWidget* aTo) { if (aFrom == aTo) { @@ -2394,23 +2382,6 @@ LayoutDeviceIntPoint nsLayoutUtils::WidgetToWidgetOffset(nsIWidget* aFrom, return fromOffset - toOffset; } -nsPoint nsLayoutUtils::TranslateWidgetToView(nsPresContext* aPresContext, - nsIWidget* aWidget, - const LayoutDeviceIntPoint& aPt, - nsView* aView) { - nsPoint viewOffset; - nsIWidget* viewWidget = aView->GetNearestWidget(&viewOffset); - if (!viewWidget) { - return nsPoint(NS_UNCONSTRAINEDSIZE, NS_UNCONSTRAINEDSIZE); - } - - LayoutDeviceIntPoint widgetPoint = - aPt + WidgetToWidgetOffset(aWidget, viewWidget); - nsPoint widgetAppUnits(aPresContext->DevPixelsToAppUnits(widgetPoint.x), - aPresContext->DevPixelsToAppUnits(widgetPoint.y)); - return widgetAppUnits - viewOffset; -} - LayoutDeviceIntPoint nsLayoutUtils::TranslateViewToWidget( nsPresContext* aPresContext, nsView* aView, nsPoint aPt, ViewportType aViewportType, nsIWidget* aWidget) { diff --git a/layout/base/nsLayoutUtils.h b/layout/base/nsLayoutUtils.h @@ -740,19 +740,6 @@ class nsLayoutUtils { int32_t* aOffset); /** - * Translate from widget coordinates to the view's coordinates - * @param aPresContext the PresContext for the view - * @param aWidget the widget - * @param aPt the point relative to the widget - * @param aView view to which returned coordinates are relative - * @return the point in the view's coordinates - */ - static nsPoint TranslateWidgetToView(nsPresContext* aPresContext, - nsIWidget* aWidget, - const mozilla::LayoutDeviceIntPoint& aPt, - nsView* aView); - - /** * Translate from view coordinates to the widget's coordinates. * @param aPresContext the PresContext for the view * @param aView the view @@ -768,6 +755,9 @@ class nsLayoutUtils { static mozilla::LayoutDeviceIntPoint WidgetToWidgetOffset( nsIWidget* aFromWidget, nsIWidget* aToWidget); + static mozilla::Maybe<nsPoint> FrameToWidgetOffset(const nsIFrame* aFrame, + nsIWidget* aWidget); + enum class FrameForPointOption { /** * When set, paint suppression is ignored, so we'll return non-root page diff --git a/layout/generic/FrameClasses.py b/layout/generic/FrameClasses.py @@ -117,7 +117,7 @@ FRAME_CLASSES = [ Frame("nsPageSequenceFrame", "PageSequence", COMMON), Frame("nsSliderFrame", "Slider", COMMON), Frame("nsSplitterFrame", "SimpleXULLeaf", COMMON | LEAF), - Frame("nsSubDocumentFrame", "SubDocument", REPLACED_SIZING | LEAF | MAY_HAVE_VIEW), + Frame("nsSubDocumentFrame", "SubDocument", REPLACED_SIZING | LEAF), Frame("PrintedSheetFrame", "PrintedSheet", COMMON), Frame("SVGAFrame", "SVGA", SVG_CONTAINER), Frame("SVGClipPathFrame", "SVGClipPath", SVG_RENDERING_OBSERVER_CONTAINER), diff --git a/layout/generic/crashtests/1999326.html b/layout/generic/crashtests/1999326.html @@ -0,0 +1,11 @@ +<style> +*::first-line {} +</style> +<script> +document.addEventListener("DOMContentLoaded", () => { + a.href = "x" + a.text = "A" +}) +</script> +<s lang="zh-CN"> +<a id="a" style="float: right"> diff --git a/layout/generic/crashtests/crashtests.list b/layout/generic/crashtests/crashtests.list @@ -825,3 +825,4 @@ load 1978475-1.html load 1990258.html load 1990319.html load 1998521.html +load 1999326.html diff --git a/layout/generic/nsSubDocumentFrame.cpp b/layout/generic/nsSubDocumentFrame.cpp @@ -30,6 +30,7 @@ #include "nsAttrValueInlines.h" #include "nsCOMPtr.h" #include "nsContentUtils.h" +#include "nsDeviceContext.h" #include "nsDisplayList.h" #include "nsFrameSetFrame.h" #include "nsGenericHTMLElement.h" @@ -39,6 +40,7 @@ #include "nsIDocShell.h" #include "nsIDocumentViewer.h" #include "nsIObjectLoadingContent.h" +#include "nsIWeakReferenceUtils.h" #include "nsLayoutUtils.h" #include "nsNameSpaceManager.h" #include "nsObjectLoadingContent.h" @@ -48,22 +50,12 @@ #include "nsStyleConsts.h" #include "nsStyleStruct.h" #include "nsStyleStructInlines.h" -#include "nsView.h" -#include "nsViewManager.h" using namespace mozilla; using namespace mozilla::dom; using namespace mozilla::gfx; using namespace mozilla::layers; -static Document* GetDocumentFromView(nsView* aView) { - MOZ_ASSERT(aView, "null view"); - - nsViewManager* vm = aView->GetViewManager(); - PresShell* presShell = vm ? vm->GetPresShell() : nullptr; - return presShell ? presShell->GetDocument() : nullptr; -} - static void PropagateIsUnderHiddenEmbedderElement(nsFrameLoader* aFrameLoader, bool aValue) { if (!aFrameLoader) { @@ -80,8 +72,6 @@ static void PropagateIsUnderHiddenEmbedderElement(nsFrameLoader* aFrameLoader, nsSubDocumentFrame::nsSubDocumentFrame(ComputedStyle* aStyle, nsPresContext* aPresContext) : nsAtomicContainerFrame(aStyle, aPresContext, kClassID), - mOuterView(nullptr), - mInnerView(nullptr), mIsInline(false), mPostedReflowCallback(false), mDidCreateDoc(false), @@ -114,6 +104,34 @@ class AsyncFrameInit : public Runnable { WeakFrame mFrame; }; +void nsSubDocumentFrame::EnsureEmbeddingPresShell(class PresShell* aPs) { + MOZ_ASSERT(aPs); + nsWeakPtr weakRef = do_GetWeakReference(aPs); + if (!mInProcessPresShells.Contains(weakRef)) { + aPs->SetInProcessEmbedderFrame(this); + mInProcessPresShells.AppendElement(std::move(weakRef)); + } +} + +void nsSubDocumentFrame::AddEmbeddingPresShell(class PresShell* aPs) { + MOZ_ASSERT(aPs); + nsWeakPtr weakRef = do_GetWeakReference(aPs); + MOZ_ASSERT(!mInProcessPresShells.Contains(weakRef)); + aPs->SetInProcessEmbedderFrame(this); + mInProcessPresShells.AppendElement(std::move(weakRef)); +} + +void nsSubDocumentFrame::RemoveEmbeddingPresShell(class PresShell* aPs) { + MOZ_ASSERT(aPs); + nsWeakPtr weakRef = do_GetWeakReference(aPs); + MOZ_ASSERT(mInProcessPresShells.Contains(weakRef)); + aPs->SetInProcessEmbedderFrame(nullptr); + if (mLastPaintedPresShell == weakRef) { + mLastPaintedPresShell = nullptr; + } + mInProcessPresShells.RemoveElement(weakRef); +} + void nsSubDocumentFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsIFrame* aPrevInFlow) { MOZ_ASSERT(aContent); @@ -122,29 +140,16 @@ void nsSubDocumentFrame::Init(nsIContent* aContent, nsContainerFrame* aParent, nsAtomicContainerFrame::Init(aContent, aParent, aPrevInFlow); - // CreateView() creates this frame's view, stored in mOuterView. It needs to - // be created first since it's the parent of the inner view, stored in - // mInnerView. - CreateView(); - EnsureInnerView(); - - // Set the primary frame now so that nsDocumentViewer::FindContainerView - // called from within EndSwapDocShellsForViews below can find it if needed. aContent->SetPrimaryFrame(this); // If we have a detached subdoc's root view on our frame loader, re-insert it // into the view tree. This happens when we've been reframed, and ensures the // presentation persists across reframes. if (RefPtr<nsFrameLoader> frameloader = FrameLoader()) { - bool hadFrame = false; - nsIFrame* detachedFrame = frameloader->GetDetachedSubdocFrame(&hadFrame); - frameloader->SetDetachedSubdocFrame(nullptr); - nsView* detachedView = detachedFrame ? detachedFrame->GetView() : nullptr; - if (detachedView) { - // Restore stashed presentation. - InsertViewsInReverseOrder(detachedView, mInnerView); - EndSwapDocShellsForViews(mInnerView->GetFirstChild()); - } else if (hadFrame) { + mInProcessPresShells = frameloader->TakeDetachedSubdocs(); + const bool anyLiveShell = FixUpInProcessPresShellsAfterAttach(); + if (!mInProcessPresShells.IsEmpty() && !anyLiveShell) { + mInProcessPresShells.Clear(); // Presentation is for a different document, don't restore it. frameloader->Hide(); } @@ -189,8 +194,7 @@ void nsSubDocumentFrame::ShowViewer() { if (!frameloader->IsRemoteFrame() && !PresContext()->IsDynamic()) { // We let the printing code take care of loading the document and - // initializing the shell; just create the inner view for it to use. - (void)EnsureInnerView(); + // initializing the shell. } else { AutoWeakFrame weakThis(this); mCallingShow = true; @@ -211,94 +215,36 @@ void nsSubDocumentFrame::ShowViewer() { } } -void nsSubDocumentFrame::CreateView() { - MOZ_ASSERT(!GetView()); - - nsView* parentView = GetParent()->GetClosestView(); - MOZ_ASSERT(parentView, "no parent with view"); - - nsViewManager* viewManager = parentView->GetViewManager(); - MOZ_ASSERT(viewManager, "null view manager"); - - nsView* view = viewManager->CreateView(GetRect(), parentView); - SyncFrameViewProperties(view); - - nsView* insertBefore = nsLayoutUtils::FindSiblingViewFor(parentView, this); - // we insert this view 'above' the insertBefore view, unless insertBefore is - // null, in which case we want to call with aAbove == false to insert at the - // beginning in document order - viewManager->InsertChild(parentView, view, insertBefore, - insertBefore != nullptr); - - // Remember our view - SetView(view); +Document* nsSubDocumentFrame::GetExtantSubdocument() { + nsIDocShell* ds = GetExtantDocShell(); + return ds ? ds->GetExtantDocument() : nullptr; +} - NS_FRAME_LOG(NS_FRAME_TRACE_CALLS, - ("nsIFrame::CreateView: frame=%p view=%p", this, view)); +mozilla::PresShell* nsSubDocumentFrame::GetSubdocumentPresShell() { + Document* doc = GetExtantSubdocument(); + return doc ? doc->GetPresShell() : nullptr; } nsIFrame* nsSubDocumentFrame::GetSubdocumentRootFrame() { - if (!mInnerView) { - return nullptr; - } - nsView* subdocView = mInnerView->GetFirstChild(); - return subdocView ? subdocView->GetFrame() : nullptr; + mozilla::PresShell* ps = GetSubdocumentPresShell(); + return ps ? ps->GetRootFrame() : nullptr; } mozilla::PresShell* nsSubDocumentFrame::GetSubdocumentPresShellForPainting( uint32_t aFlags) { - if (!mInnerView) { - return nullptr; - } - - nsView* subdocView = mInnerView->GetFirstChild(); - if (!subdocView) { - return nullptr; - } - - mozilla::PresShell* presShell = nullptr; - - nsIFrame* subdocRootFrame = subdocView->GetFrame(); - if (subdocRootFrame) { - presShell = subdocRootFrame->PresShell(); - } - - // If painting is suppressed in the presshell, we try to look for a better - // presshell to use. - if (!presShell || (presShell->IsPaintingSuppressed() && - !(aFlags & IGNORE_PAINT_SUPPRESSION))) { - // During page transition mInnerView will sometimes have two children, the - // first being the new page that may not have any frame, and the second - // being the old page that will probably have a frame. - nsView* nextView = subdocView->GetNextSibling(); - nsIFrame* frame = nullptr; - if (nextView) { - frame = nextView->GetFrame(); - } - if (frame) { - mozilla::PresShell* presShellForNextView = frame->PresShell(); - if (!presShell || (presShellForNextView && - !presShellForNextView->IsPaintingSuppressed() && - StaticPrefs::layout_show_previous_page())) { - subdocView = nextView; - subdocRootFrame = frame; - presShell = presShellForNextView; - } - } - if (!presShell) { - // If we don't have a frame we use this roundabout way to get the pres - // shell. - if (!mFrameLoader) { - return nullptr; - } - nsIDocShell* docShell = mFrameLoader->GetDocShell(IgnoreErrors()); - if (!docShell) { - return nullptr; - } - presShell = docShell->GetPresShell(); + mozilla::PresShell* presShell = GetSubdocumentPresShell(); + if (presShell && (!presShell->IsPaintingSuppressed() || + (aFlags & IGNORE_PAINT_SUPPRESSION))) { + return presShell; + } + // If painting is suppressed in the presshell or there's no presShell, we try + // to look for a better presshell to use. + if (StaticPrefs::layout_show_previous_page()) { + RefPtr<mozilla::PresShell> old = do_QueryReferent(mLastPaintedPresShell); + if (old && old->GetInProcessEmbedderFrame() == this) { + return old; } } - return presShell; } @@ -319,12 +265,14 @@ nsRect nsSubDocumentFrame::GetDestRect(const nsRect& aConstraintRect) const { LayoutDeviceIntSize nsSubDocumentFrame::GetInitialSubdocumentSize() const { if (RefPtr<nsFrameLoader> frameloader = FrameLoader()) { - nsIFrame* detachedFrame = frameloader->GetDetachedSubdocFrame(); - if (nsView* view = detachedFrame ? detachedFrame->GetView() : nullptr) { - nsSize size = view->GetBounds().Size(); - nsPresContext* presContext = detachedFrame->PresContext(); - return LayoutDeviceIntSize(presContext->AppUnitsToDevPixels(size.width), - presContext->AppUnitsToDevPixels(size.height)); + for (const auto& detachedShell : frameloader->GetDetachedSubdocs()) { + if (RefPtr<mozilla::PresShell> ps = do_QueryReferent(detachedShell)) { + if (nsPresContext* pc = ps->GetPresContext()) { + return LayoutDeviceIntSize( + pc->AppUnitsToDevPixels(pc->GetVisibleArea().width), + pc->AppUnitsToDevPixels(pc->GetVisibleArea().height)); + } + } } } // Pick some default size for now. Using 10x10 because that's what the @@ -399,7 +347,7 @@ void nsSubDocumentFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, // If we're passing pointer events to children then we have to descend into // subdocuments no matter what, to determine which parts are transparent for // hit-testing or event regions. - if (!mInnerView || !aBuilder->GetDescendIntoSubdocuments()) { + if (!aBuilder->GetDescendIntoSubdocuments()) { return; } @@ -420,6 +368,10 @@ void nsSubDocumentFrame::BuildDisplayList(nsDisplayListBuilder* aBuilder, return; } + if (aBuilder->IsForPainting() && !aBuilder->IsIgnoringPaintSuppression()) { + mLastPaintedPresShell = do_GetWeakReference(presShell); + } + if (aBuilder->IsInFilter()) { Document* outerDoc = PresShell()->GetDocument(); Document* innerDoc = presShell->GetDocument(); @@ -701,16 +653,18 @@ void nsSubDocumentFrame::Reflow(nsPresContext* aPresContext, nsPoint offset = nsPoint(aReflowInput.ComputedPhysicalBorderPadding().left, aReflowInput.ComputedPhysicalBorderPadding().top); - if (mInnerView) { + if (nsCOMPtr<nsIDocShell> ds = GetExtantDocShell()) { const nsMargin& bp = aReflowInput.ComputedPhysicalBorderPadding(); nsSize innerSize(aDesiredSize.Width() - bp.LeftRight(), aDesiredSize.Height() - bp.TopBottom()); // Size & position the view according to 'object-fit' & 'object-position'. - nsRect destRect = GetDestRect(nsRect(offset, innerSize)); - nsViewManager* vm = mInnerView->GetViewManager(); - vm->MoveViewTo(mInnerView, destRect.x, destRect.y); - vm->ResizeView(mInnerView, nsRect(nsPoint(0, 0), destRect.Size())); + const nsRect destRect = GetDestRect(nsRect(offset, innerSize)); + auto rect = LayoutDeviceIntRect::FromAppUnitsToInside( + destRect, PresContext()->AppUnitsPerDevPixel()); + mExtraOffset = destRect.TopLeft(); + nsDocShell::Cast(ds)->SetPositionAndSize(0, 0, rect.width, rect.height, + nsIBaseWindow::eDelayResize); } aDesiredSize.SetOverflowAreasToDesiredBounds(); @@ -943,7 +897,7 @@ class nsHideViewer final : public Runnable { // Either the frame has been constructed by now, or it never will be, // either way we want to clear the stashed views. - mFrameLoader->SetDetachedSubdocFrame(nullptr); + mFrameLoader->SetDetachedSubdocs({}); nsSubDocumentFrame* frame = do_QueryFrame(mFrameElement->GetPrimaryFrame()); if (!frame || frame->FrameLoader() != mFrameLoader) { @@ -964,8 +918,6 @@ class nsHideViewer final : public Runnable { const bool mHideViewerIfFrameless; }; -static nsView* BeginSwapDocShellsForViews(nsView* aSibling); - void nsSubDocumentFrame::Destroy(DestroyContext& aContext) { if (mPostedReflowCallback) { PresShell()->CancelReflowCallback(this); @@ -978,11 +930,8 @@ void nsSubDocumentFrame::Destroy(DestroyContext& aContext) { if (RefPtr<nsFrameLoader> frameloader = FrameLoader()) { ClearDisplayItems(); - nsView* detachedViews = - ::BeginSwapDocShellsForViews(mInnerView->GetFirstChild()); - - frameloader->SetDetachedSubdocFrame( - detachedViews ? detachedViews->GetFrame() : nullptr); + PrepareInProcessPresShellsForDetach(); + frameloader->SetDetachedSubdocs(std::move(mInProcessPresShells)); // We call nsFrameLoader::HideViewer() in a script runner so that we can // safely determine whether the frame is being reframed or destroyed. @@ -1052,6 +1001,10 @@ nsIDocShell* nsSubDocumentFrame::GetDocShell() const { return mFrameLoader->GetDocShell(IgnoreErrors()); } +nsIDocShell* nsSubDocumentFrame::GetExtantDocShell() const { + return mFrameLoader ? mFrameLoader->GetExistingDocShell() : nullptr; +} + static void DestroyDisplayItemDataForFrames(nsIFrame* aFrame) { // Destroying a WebRenderUserDataTable can cause destruction of other objects // which can remove frame properties in their destructor. If we delete a frame @@ -1076,52 +1029,6 @@ static void DestroyDisplayItemDataForFrames(nsIFrame* aFrame) { } } -static CallState BeginSwapDocShellsForDocument(Document& aDocument) { - if (PresShell* presShell = aDocument.GetPresShell()) { - // Disable painting while the views are detached, see bug 946929. - presShell->SetNeverPainting(true); - - if (nsIFrame* rootFrame = presShell->GetRootFrame()) { - ::DestroyDisplayItemDataForFrames(rootFrame); - } - } - aDocument.EnumerateSubDocuments(BeginSwapDocShellsForDocument); - return CallState::Continue; -} - -static nsView* BeginSwapDocShellsForViews(nsView* aSibling) { - // Collect the removed sibling views in reverse order in 'removedViews'. - nsView* removedViews = nullptr; - while (aSibling) { - if (Document* doc = ::GetDocumentFromView(aSibling)) { - ::BeginSwapDocShellsForDocument(*doc); - } - nsView* next = aSibling->GetNextSibling(); - aSibling->GetViewManager()->RemoveChild(aSibling); - aSibling->SetNextSibling(removedViews); - removedViews = aSibling; - aSibling = next; - } - return removedViews; -} - -/* static */ -void nsSubDocumentFrame::InsertViewsInReverseOrder(nsView* aSibling, - nsView* aParent) { - MOZ_ASSERT(aParent, "null view"); - MOZ_ASSERT(!aParent->GetFirstChild(), "inserting into non-empty list"); - - nsViewManager* vm = aParent->GetViewManager(); - while (aSibling) { - nsView* next = aSibling->GetNextSibling(); - aSibling->SetNextSibling(nullptr); - // true means 'after' in document order which is 'before' in view order, - // so this call prepends the child, thus reversing the siblings as we go. - vm->InsertChild(aParent, aSibling, nullptr, true); - aSibling = next; - } -} - nsresult nsSubDocumentFrame::BeginSwapDocShells(nsIFrame* aOther) { if (!aOther || !aOther->IsSubDocumentFrame()) { return NS_ERROR_NOT_IMPLEMENTED; @@ -1136,23 +1043,17 @@ nsresult nsSubDocumentFrame::BeginSwapDocShells(nsIFrame* aOther) { ClearDisplayItems(); other->ClearDisplayItems(); - if (mInnerView && other->mInnerView) { - nsView* ourSubdocViews = mInnerView->GetFirstChild(); - nsView* ourRemovedViews = ::BeginSwapDocShellsForViews(ourSubdocViews); - nsView* otherSubdocViews = other->mInnerView->GetFirstChild(); - nsView* otherRemovedViews = ::BeginSwapDocShellsForViews(otherSubdocViews); + PrepareInProcessPresShellsForDetach(); + other->PrepareInProcessPresShellsForDetach(); - InsertViewsInReverseOrder(ourRemovedViews, other->mInnerView); - InsertViewsInReverseOrder(otherRemovedViews, mInnerView); - } mFrameLoader.swap(other->mFrameLoader); return NS_OK; } static CallState EndSwapDocShellsForDocument(Document& aDocument) { - // Our docshell and view trees have been updated for the new hierarchy. - // Now also update all nsDeviceContext::mWidget to that of the - // container view in the new hierarchy. + // Our docshell trees have been updated for the new hierarchy. Now also update + // all nsDeviceContext::mWidget to that of the container view in the new + // hierarchy. if (nsCOMPtr<nsIDocShell> ds = aDocument.GetDocShell()) { nsCOMPtr<nsIDocumentViewer> viewer; ds->GetDocViewer(getter_AddRefs(viewer)); @@ -1163,8 +1064,12 @@ static CallState EndSwapDocShellsForDocument(Document& aDocument) { } nsDeviceContext* dc = pc ? pc->DeviceContext() : nullptr; if (dc) { - nsView* v = viewer->FindContainerView(); - dc->Init(v ? v->GetNearestWidget(nullptr) : nullptr); + nsSubDocumentFrame* f = viewer->FindContainerFrame(); + nsIWidget* widget = f ? f->GetNearestWidget() : nullptr; + if (widget) { + widget = widget->GetTopLevelWidget(); + } + dc->Init(widget); } viewer = viewer->GetPreviousViewer(); } @@ -1174,64 +1079,65 @@ static CallState EndSwapDocShellsForDocument(Document& aDocument) { return CallState::Continue; } -/* static */ -void nsSubDocumentFrame::EndSwapDocShellsForViews(nsView* aSibling) { - for (; aSibling; aSibling = aSibling->GetNextSibling()) { - if (Document* doc = ::GetDocumentFromView(aSibling)) { - ::EndSwapDocShellsForDocument(*doc); +static CallState BeginSwapDocShellsForDocument(Document& aDocument) { + if (PresShell* presShell = aDocument.GetPresShell()) { + // Disable painting while the presentation shell is detached. + presShell->SetNeverPainting(true); + + if (nsIFrame* rootFrame = presShell->GetRootFrame()) { + ::DestroyDisplayItemDataForFrames(rootFrame); } - nsIFrame* frame = aSibling->GetFrame(); - if (frame) { - nsIFrame* parent = nsLayoutUtils::GetCrossDocParentFrameInProcess(frame); - if (parent->HasAnyStateBits(NS_FRAME_IN_POPUP)) { - nsIFrame::AddInPopupStateBitToDescendants(frame); - } else { - nsIFrame::RemoveInPopupStateBitFromDescendants(frame); - } - if (frame->HasInvalidFrameInSubtree()) { - while (parent && - !parent->HasAnyStateBits(NS_FRAME_DESCENDANT_NEEDS_PAINT | - NS_FRAME_IS_NONDISPLAY)) { - parent->AddStateBits(NS_FRAME_DESCENDANT_NEEDS_PAINT); - parent = nsLayoutUtils::GetCrossDocParentFrameInProcess(parent); - } + } + aDocument.EnumerateSubDocuments(BeginSwapDocShellsForDocument); + return CallState::Continue; +} + +void nsSubDocumentFrame::PrepareInProcessPresShellsForDetach() { + for (const auto& shell : mInProcessPresShells) { + if (RefPtr<class PresShell> ps = do_QueryReferent(shell)) { + BeginSwapDocShellsForDocument(*ps->GetDocument()); + } + } +} + +bool nsSubDocumentFrame::FixUpInProcessPresShellsAfterAttach() { + bool anyLiveShell = false; + for (auto& shell : mInProcessPresShells) { + if (RefPtr<mozilla::PresShell> ps = do_QueryReferent(shell)) { + if (ps && !ps->IsDestroying()) { + anyLiveShell = true; + ps->SetInProcessEmbedderFrame(this); + EndSwapDocShellsForDocument(*ps->GetDocument()); } } } + return anyLiveShell; } void nsSubDocumentFrame::EndSwapDocShells(nsIFrame* aOther) { - nsSubDocumentFrame* other = static_cast<nsSubDocumentFrame*>(aOther); - AutoWeakFrame weakThis(this); - AutoWeakFrame weakOther(aOther); + auto* other = static_cast<nsSubDocumentFrame*>(aOther); - if (mInnerView) { - EndSwapDocShellsForViews(mInnerView->GetFirstChild()); - } - if (other->mInnerView) { - EndSwapDocShellsForViews(other->mInnerView->GetFirstChild()); - } + mInProcessPresShells.SwapElements(other->mInProcessPresShells); + FixUpInProcessPresShellsAfterAttach(); + other->FixUpInProcessPresShellsAfterAttach(); // Now make sure we reflow both frames, in case their contents // determine their size. // And repaint them, for good measure, in case there's nothing // interesting that happens during reflow. - if (weakThis.IsAlive()) { - PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, - NS_FRAME_IS_DIRTY); - InvalidateFrameSubtree(); - PropagateIsUnderHiddenEmbedderElement( - PresShell()->IsUnderHiddenEmbedderElement() || - !StyleVisibility()->IsVisible()); - } - if (weakOther.IsAlive()) { - other->PresShell()->FrameNeedsReflow( - other, IntrinsicDirty::FrameAndAncestors, NS_FRAME_IS_DIRTY); - other->InvalidateFrameSubtree(); - other->PropagateIsUnderHiddenEmbedderElement( - other->PresShell()->IsUnderHiddenEmbedderElement() || - !other->StyleVisibility()->IsVisible()); - } + PresShell()->FrameNeedsReflow(this, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); + InvalidateFrameSubtree(); + PropagateIsUnderHiddenEmbedderElement( + PresShell()->IsUnderHiddenEmbedderElement() || + !StyleVisibility()->IsVisible()); + + other->PresShell()->FrameNeedsReflow(other, IntrinsicDirty::FrameAndAncestors, + NS_FRAME_IS_DIRTY); + other->InvalidateFrameSubtree(); + other->PropagateIsUnderHiddenEmbedderElement( + other->PresShell()->IsUnderHiddenEmbedderElement() || + !other->StyleVisibility()->IsVisible()); } void nsSubDocumentFrame::ClearDisplayItems() { @@ -1241,33 +1147,6 @@ void nsSubDocumentFrame::ClearDisplayItems() { } } -nsView* nsSubDocumentFrame::EnsureInnerView() { - if (mInnerView) { - return mInnerView; - } - - // create, init, set the parent of the view - nsView* outerView = GetView(); - NS_ASSERTION(outerView, "Must have an outer view already"); - nsRect viewBounds(0, 0, 0, 0); // size will be fixed during reflow - - nsViewManager* viewMan = outerView->GetViewManager(); - nsView* innerView = viewMan->CreateView(viewBounds, outerView); - if (!innerView) { - NS_ERROR("Could not create inner view"); - return nullptr; - } - mInnerView = innerView; - viewMan->InsertChild(outerView, innerView, nullptr, true); - - return mInnerView; -} - -nsPoint nsSubDocumentFrame::GetExtraOffset() const { - MOZ_ASSERT(mInnerView); - return mInnerView->GetPosition(); -} - void nsSubDocumentFrame::SubdocumentIntrinsicSizeOrRatioChanged() { const nsStylePosition* pos = StylePosition(); const auto anchorResolutionParams = AnchorPosResolutionParams::From(this); diff --git a/layout/generic/nsSubDocumentFrame.h b/layout/generic/nsSubDocumentFrame.h @@ -55,6 +55,8 @@ class nsSubDocumentFrame final : public nsAtomicContainerFrame, mozilla::IntrinsicSize GetIntrinsicSize() override; mozilla::AspectRatio GetIntrinsicRatio() const override; + const nsPoint& GetExtraOffset() const { return mExtraOffset; } + SizeComputationResult ComputeSize( gfxContext* aRenderingContext, mozilla::WritingMode aWM, const mozilla::LogicalSize& aCBSize, nscoord aAvailableISize, @@ -89,14 +91,12 @@ class nsSubDocumentFrame final : public nsAtomicContainerFrame, bool aIgnoreContainment = false) const; nsIDocShell* GetDocShell() const; + nsIDocShell* GetExtantDocShell() const; nsresult BeginSwapDocShells(nsIFrame* aOther); void EndSwapDocShells(nsIFrame* aOther); - static void InsertViewsInReverseOrder(nsView* aSibling, nsView* aParent); - static void EndSwapDocShellsForViews(nsView* aView); - - nsView* EnsureInnerView(); - nsPoint GetExtraOffset() const; + mozilla::dom::Document* GetExtantSubdocument(); + mozilla::PresShell* GetSubdocumentPresShell(); nsIFrame* GetSubdocumentRootFrame(); enum { IGNORE_PAINT_SUPPRESSION = 0x1 }; mozilla::PresShell* GetSubdocumentPresShellForPainting(uint32_t aFlags); @@ -151,6 +151,10 @@ class nsSubDocumentFrame final : public nsAtomicContainerFrame, const Maybe<nsRect>& GetVisibleRect() const { return mVisibleRect; } void SetVisibleRect(const Maybe<nsRect>& aRect) { mVisibleRect = aRect; } + void AddEmbeddingPresShell(mozilla::PresShell*); + void EnsureEmbeddingPresShell(mozilla::PresShell*); + void RemoveEmbeddingPresShell(mozilla::PresShell*); + protected: friend class AsyncFrameInit; @@ -160,6 +164,11 @@ class nsSubDocumentFrame final : public nsAtomicContainerFrame, void PropagateIsUnderHiddenEmbedderElement(bool aValue); void UpdateEmbeddedBrowsingContextDependentData(); + // Makes sure that all the live pres shells are pointing to `this`. Returns + // true if there's any live shells. + bool FixUpInProcessPresShellsAfterAttach(); + void PrepareInProcessPresShellsForDetach(); + bool IsInline() const { return mIsInline; } // Show our document viewer. The document viewer is hidden via a script @@ -167,24 +176,27 @@ class nsSubDocumentFrame final : public nsAtomicContainerFrame, // being reframed. void ShowViewer(); - nsView* GetViewInternal() const override { return mOuterView; } - void SetViewInternal(nsView* aView) override { mOuterView = aView; } - void CreateView(); - mutable RefPtr<nsFrameLoader> mFrameLoader; - nsView* mOuterView; - nsView* mInnerView; - + // The in-process pres shells that we're currently embedding. May be multiple + // because of in-process BFCache. + // TODO(emilio): Maybe that's not relevant anymore? We definitely hit that + // code-path, but maybe it could be simplified nowadays? + AutoTArray<nsWeakPtr, 1> mInProcessPresShells; // When process-switching a remote tab, we might temporarily paint the old // one. Maybe<RemoteFramePaintData> mRetainedRemoteFrame; + nsWeakPtr mLastPaintedPresShell; // The raster scale from our last paint. mozilla::gfx::MatrixScales mRasterScale; // The visible rect from our last paint. Maybe<nsRect> mVisibleRect; + // The extra offset from our padding box to the child, needed to deal with + // object-fit and co. + nsPoint mExtraOffset; + bool mIsInline : 1; bool mPostedReflowCallback : 1; bool mDidCreateDoc : 1; diff --git a/layout/generic/nsTextFrame.cpp b/layout/generic/nsTextFrame.cpp @@ -3563,20 +3563,16 @@ static int32_t GetFrameLineNum(nsIFrame* aFrame, nsILineIterator* aLineIter) { if (!aLineIter) { return -1; } - int32_t n = aLineIter->FindLineContaining(aFrame); - if (n >= 0) { - return n; - } - // If we didn't find the frame directly, but its parent is an inline, - // we want the line that the inline ancestor is on. - nsIFrame* ancestor = aFrame->GetParent(); - while (ancestor && ancestor->IsInlineFrame()) { - n = aLineIter->FindLineContaining(ancestor); + // If we don't find the frame directly, but its parent is an inline or other + // "line participant" (e.g. nsFirstLineFrame), we want the line that the + // inline ancestor is on. + do { + int32_t n = aLineIter->FindLineContaining(aFrame); if (n >= 0) { return n; } - ancestor = ancestor->GetParent(); - } + aFrame = aFrame->GetParent(); + } while (aFrame && aFrame->IsLineParticipant()); return -1; } diff --git a/layout/mathml/nsMathMLOperators.cpp b/layout/mathml/nsMathMLOperators.cpp @@ -6,6 +6,7 @@ #include "nsMathMLOperators.h" +#include "mozilla/StaticPrefs_mathml.h" #include "mozilla/intl/UnicodeProperties.h" #include "nsCOMPtr.h" #include "nsCRT.h" @@ -16,6 +17,8 @@ #include "nsTArray.h" #include "nsTHashMap.h" +using namespace mozilla; + // operator dictionary entry struct OperatorData { OperatorData(void) : mFlags(0), mLeadingSpace(0.0f), mTrailingSpace(0.0f) {} @@ -56,7 +59,8 @@ static void SetBooleanProperty(OperatorData* aOperatorData, nsString aName) { aOperatorData->mFlags |= NS_MATHML_OPERATOR_STRETCHY; } else if (aName.EqualsLiteral("fence")) { aOperatorData->mFlags |= NS_MATHML_OPERATOR_FENCE; - } else if (aName.EqualsLiteral("accent")) { + } else if (!StaticPrefs::mathml_operator_dictionary_accent_disabled() && + aName.EqualsLiteral("accent")) { aOperatorData->mFlags |= NS_MATHML_OPERATOR_ACCENT; } else if (aName.EqualsLiteral("largeop")) { aOperatorData->mFlags |= NS_MATHML_OPERATOR_LARGEOP; @@ -427,7 +431,7 @@ bool nsMathMLOperators::LookupOperatorWithFallback(const nsString& aOperator, /* static */ bool nsMathMLOperators::IsMirrorableOperator(const nsString& aOperator) { if (auto codePoint = ToUnicodeCodePoint(aOperator)) { - return mozilla::intl::UnicodeProperties::IsMirrored(codePoint); + return intl::UnicodeProperties::IsMirrored(codePoint); } return false; } @@ -436,7 +440,7 @@ bool nsMathMLOperators::IsMirrorableOperator(const nsString& aOperator) { nsString nsMathMLOperators::GetMirroredOperator(const nsString& aOperator) { nsString result; if (auto codePoint = ToUnicodeCodePoint(aOperator)) { - result.Assign(mozilla::intl::UnicodeProperties::CharMirror(codePoint)); + result.Assign(intl::UnicodeProperties::CharMirror(codePoint)); } return result; } diff --git a/layout/mathml/nsMathMLmoFrame.cpp b/layout/mathml/nsMathMLmoFrame.cpp @@ -11,10 +11,12 @@ #include "gfxContext.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPrefs_mathml.h" +#include "mozilla/dom/Document.h" #include "mozilla/dom/MathMLElement.h" #include "nsCSSValue.h" #include "nsContentUtils.h" #include "nsFrameSelection.h" +#include "nsGkAtoms.h" #include "nsLayoutUtils.h" #include "nsPresContext.h" @@ -225,11 +227,31 @@ void nsMathMLmoFrame::ProcessOperatorData() { } // see if the accent attribute is there - mContent->AsElement()->GetAttr(nsGkAtoms::accent, value); - if (value.LowerCaseEqualsLiteral("true")) { - mEmbellishData.flags |= NS_MATHML_EMBELLISH_ACCENT; - } else if (value.LowerCaseEqualsLiteral("false")) { - mEmbellishData.flags &= ~NS_MATHML_EMBELLISH_ACCENT; + if (mContent->AsElement()->GetAttr(nsGkAtoms::accent, value)) { + [&]() { + AutoTArray<nsString, 2> params; + auto parentName = GetParent()->GetContent()->NodeInfo()->NameAtom(); + if (parentName == nsGkAtoms::mover) { + params.AppendElement(u"accent"); + params.AppendElement(u"mover"); + } else if (parentName == nsGkAtoms::munder) { + params.AppendElement(u"accentunder"); + params.AppendElement(u"munder"); + } else if (parentName == nsGkAtoms::munderover) { + params.AppendElement(u"accent/accentunder"); + params.AppendElement(u"munderover"); + } else { + return; + } + PresContext()->Document()->WarnOnceAbout( + dom::DeprecatedOperations::eMathML_DeprecatedMoExplicitAccent, + false, params); + }(); + if (value.LowerCaseEqualsLiteral("true")) { + mEmbellishData.flags |= NS_MATHML_EMBELLISH_ACCENT; + } else if (value.LowerCaseEqualsLiteral("false")) { + mEmbellishData.flags &= ~NS_MATHML_EMBELLISH_ACCENT; + } } // see if the movablelimits attribute is there diff --git a/layout/mathml/nsMathMLmunderoverFrame.cpp b/layout/mathml/nsMathMLmunderoverFrame.cpp @@ -15,6 +15,7 @@ #include "mozilla/StaticPrefs_mathml.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/MathMLElement.h" +#include "nsIMathMLFrame.h" #include "nsLayoutUtils.h" #include "nsMathMLmmultiscriptsFrame.h" #include "nsPresContext.h" @@ -231,6 +232,13 @@ XXX The winner is the outermost setting in conflicting settings like these: } else if (value.LowerCaseEqualsLiteral("false")) { mEmbellishData.flags &= ~NS_MATHML_EMBELLISH_ACCENTUNDER; } + } else if (NS_MATHML_EMBELLISH_IS_ACCENTUNDER(mEmbellishData.flags)) { + AutoTArray<nsString, 1> params; + params.AppendElement(mContent->NodeInfo()->NodeName()); + PresContext()->Document()->WarnOnceAbout( + dom::DeprecatedOperations:: + eMathML_DeprecatedMunderNonExplicitAccentunder, + false, params); } } @@ -252,6 +260,12 @@ XXX The winner is the outermost setting in conflicting settings like these: } else if (value.LowerCaseEqualsLiteral("false")) { mEmbellishData.flags &= ~NS_MATHML_EMBELLISH_ACCENTOVER; } + } else if (NS_MATHML_EMBELLISH_IS_ACCENTOVER(mEmbellishData.flags)) { + AutoTArray<nsString, 1> params; + params.AppendElement(mContent->NodeInfo()->NodeName()); + PresContext()->Document()->WarnOnceAbout( + dom::DeprecatedOperations::eMathML_DeprecatedMoverNonExplicitAccent, + false, params); } } diff --git a/layout/printing/nsPrintJob.cpp b/layout/printing/nsPrintJob.cpp @@ -40,6 +40,7 @@ #include "nsPrintObject.h" #include "nsQueryObject.h" #include "nsReadableUtils.h" +#include "nsSubDocumentFrame.h" #include "nsView.h" // Print Options @@ -719,11 +720,9 @@ nsresult nsPrintJob::ReconstructAndReflow() { bool documentIsTopLevel = true; if (po->mParent) { nsSize adjSize; - bool doReturn; - nsresult rv = SetRootView(po, doReturn, documentIsTopLevel, adjSize); - - MOZ_ASSERT(!documentIsTopLevel, "How could this happen?"); - + bool doReturn = false; + documentIsTopLevel = false; + nsresult rv = SetRootView(po, documentIsTopLevel, doReturn, adjSize); if (NS_FAILED(rv) || doReturn) { return rv; } @@ -1181,26 +1180,12 @@ nsresult nsPrintJob::UpdateSelectionAndShrinkPrintObject( return NS_OK; } -nsView* nsPrintJob::GetParentViewForRoot() { - if (mIsCreatingPrintPreview) { - if (nsCOMPtr<nsIDocumentViewer> viewer = - do_QueryInterface(mDocViewerPrint)) { - return viewer->FindContainerView(); - } - } - return nullptr; -} - -nsresult nsPrintJob::SetRootView(nsPrintObject* aPO, bool& doReturn, - bool& documentIsTopLevel, nsSize& adjSize) { +nsresult nsPrintJob::SetRootView(nsPrintObject* aPO, bool aDocumentIsTopLevel, + bool& doReturn, nsSize& adjSize) { bool canCreateScrollbars = true; nsView* rootView; - nsView* parentView = nullptr; - - doReturn = false; - - if (aPO->mParent && aPO->mParent->PrintingIsEnabled()) { + if (!aDocumentIsTopLevel) { nsIFrame* frame = aPO->mContent ? aPO->mContent->GetPrimaryFrame() : nullptr; // Without a frame, this document can't be displayed; therefore, there is no @@ -1215,38 +1200,28 @@ nsresult nsPrintJob::SetRootView(nsPrintObject* aPO, bool& doReturn, // zoom this would be wrong as we use the same mPrt->mPrintDC for all // subdocuments. adjSize = frame->GetContentRect().Size(); - documentIsTopLevel = false; // presshell exists because parent is printable // the top nsPrintObject's widget will always have scrollbars if (frame && frame->IsSubDocumentFrame()) { - nsView* view = frame->GetView(); - NS_ENSURE_TRUE(view, NS_ERROR_FAILURE); - view = view->GetFirstChild(); - NS_ENSURE_TRUE(view, NS_ERROR_FAILURE); - parentView = view; canCreateScrollbars = false; } } else { adjSize = mPrt->mPrintDC->GetDeviceSurfaceDimensions(); - documentIsTopLevel = true; - parentView = GetParentViewForRoot(); } - if (aPO->mViewManager->GetRootView()) { - // Reuse the root view that is already on the root frame. - rootView = aPO->mViewManager->GetRootView(); + if ((rootView = aPO->mViewManager->GetRootView())) { // Remove it from its existing parent if necessary aPO->mViewManager->RemoveChild(rootView); - rootView->SetParent(parentView); + rootView->SetParent(nullptr); } else { // Create a child window of the parent that is our "root view/window" - nsRect tbounds = nsRect(nsPoint(0, 0), adjSize); - rootView = aPO->mViewManager->CreateView(tbounds, parentView); + nsRect tbounds = nsRect(nsPoint(), adjSize); + rootView = aPO->mViewManager->CreateView(tbounds, nullptr); NS_ENSURE_TRUE(rootView, NS_ERROR_OUT_OF_MEMORY); } - if (mIsCreatingPrintPreview && documentIsTopLevel) { + if (mIsCreatingPrintPreview && aDocumentIsTopLevel) { aPO->mPresContext->SetPaginatedScrolling(canCreateScrollbars); } @@ -1275,9 +1250,21 @@ nsresult nsPrintJob::ReflowPrintObject(const UniquePtr<nsPrintObject>& aPO) { nsPresContext::nsPresContextType type = mIsCreatingPrintPreview ? nsPresContext::eContext_PrintPreview : nsPresContext::eContext_Print; - const bool shouldBeRoot = - (!aPO->mParent || !aPO->mParent->PrintingIsEnabled()) && - !GetParentViewForRoot(); + const bool documentIsTopLevel = + !aPO->mParent || !aPO->mParent->PrintingIsEnabled(); + auto* embedderFrame = [&]() -> nsSubDocumentFrame* { + if (documentIsTopLevel) { + if (nsCOMPtr<nsIDocumentViewer> viewer = + do_QueryInterface(mDocViewerPrint)) { + return viewer->FindContainerFrame(); + } + } else if (aPO->mContent) { + return do_QueryFrame(aPO->mContent->GetPrimaryFrame()); + } + return nullptr; + }(); + + const bool shouldBeRoot = documentIsTopLevel && !embedderFrame; aPO->mPresContext = shouldBeRoot ? new nsRootPresContext(aPO->mDocument, type) : new nsPresContext(aPO->mDocument, type); aPO->mPresContext->SetPrintSettings(mPrintSettings); @@ -1288,11 +1275,8 @@ nsresult nsPrintJob::ReflowPrintObject(const UniquePtr<nsPrintObject>& aPO) { aPO->mViewManager = new nsViewManager(printData->mPrintDC); bool doReturn = false; - bool documentIsTopLevel = false; nsSize adjSize; - - nsresult rv = SetRootView(aPO.get(), doReturn, documentIsTopLevel, adjSize); - + nsresult rv = SetRootView(aPO.get(), documentIsTopLevel, doReturn, adjSize); if (NS_FAILED(rv) || doReturn) { return rv; } @@ -1338,7 +1322,8 @@ nsresult nsPrintJob::ReflowPrintObject(const UniquePtr<nsPrintObject>& aPO) { RefPtr<nsPresContext> presContext = aPO->mPresContext; RefPtr<nsViewManager> viewManager = aPO->mViewManager; - aPO->mPresShell = doc->CreatePresShell(presContext, viewManager); + aPO->mPresShell = + doc->CreatePresShell(presContext, viewManager, embedderFrame); if (!aPO->mPresShell) { return NS_ERROR_FAILURE; } diff --git a/layout/printing/nsPrintJob.h b/layout/printing/nsPrintJob.h @@ -24,6 +24,7 @@ // Classes class nsIFrame; +class nsSubDocumentFrame; class nsIPrintSettings; class nsPrintData; class nsPagePrintTimer; @@ -34,7 +35,6 @@ class nsPrintObject; class nsIDocShell; class nsPageSequenceFrame; class nsPIDOMWindowOuter; -class nsView; namespace mozilla { class PresShell; @@ -223,9 +223,8 @@ class nsPrintJob final : public nsIWebProgressListener, bool ShouldResumePrint() const; - nsresult SetRootView(nsPrintObject* aPO, bool& aDoReturn, - bool& aDocumentIsTopLevel, nsSize& aAdjSize); - nsView* GetParentViewForRoot(); + nsresult SetRootView(nsPrintObject* aPO, bool aDocumentIsTopLevel, + bool& aDoReturn, nsSize& aAdjSize); void UpdateZoomRatio(nsPrintObject* aPO); MOZ_CAN_RUN_SCRIPT nsresult ReconstructAndReflow(); MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult UpdateSelectionAndShrinkPrintObject( diff --git a/layout/xul/nsMenuPopupFrame.cpp b/layout/xul/nsMenuPopupFrame.cpp @@ -341,8 +341,7 @@ void nsMenuPopupFrame::CreateWidget() { LayoutDeviceIntRect nsMenuPopupFrame::CalcWidgetBounds() const { return nsView::CalcWidgetBounds( GetRect(), PresContext()->AppUnitsPerDevPixel(), - PresShell()->GetViewManager()->GetRootView(), nullptr, - widget::WindowType::Popup, + PresShell()->GetRootFrame(), nullptr, widget::WindowType::Popup, nsLayoutUtils::GetFrameTransparency(this, this)); } diff --git a/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragment.kt b/mobile/android/android-components/components/feature/prompts/src/main/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragment.kt @@ -3,33 +3,35 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ package mozilla.components.feature.prompts.dialog -import android.annotation.SuppressLint -import android.app.AlertDialog import android.app.DatePickerDialog import android.app.Dialog import android.app.TimePickerDialog -import android.content.Context import android.content.DialogInterface import android.content.DialogInterface.BUTTON_NEGATIVE import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.DialogInterface.BUTTON_POSITIVE +import android.icu.util.TimeZone import android.os.Bundle import android.text.format.DateFormat -import android.view.LayoutInflater import android.view.View import android.widget.DatePicker import android.widget.TimePicker import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting.Companion.PRIVATE +import androidx.appcompat.app.AlertDialog +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointBackward +import com.google.android.material.datepicker.DateValidatorPointForward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat import mozilla.components.feature.prompts.R -import mozilla.components.feature.prompts.ext.day import mozilla.components.feature.prompts.ext.hour import mozilla.components.feature.prompts.ext.millisecond import mozilla.components.feature.prompts.ext.minute -import mozilla.components.feature.prompts.ext.month import mozilla.components.feature.prompts.ext.second import mozilla.components.feature.prompts.ext.toCalendar -import mozilla.components.feature.prompts.ext.year import mozilla.components.feature.prompts.widget.MonthAndYearPicker import mozilla.components.feature.prompts.widget.TimePrecisionPicker import mozilla.components.support.utils.TimePicker.shouldShowSecondsPicker @@ -77,55 +79,29 @@ internal class TimePickerDialogFragment : } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val context = requireContext() - val dialog = when (selectionType) { - SELECTION_TYPE_TIME -> createTimePickerDialog(context) - SELECTION_TYPE_DATE -> initialDate.toCalendar().let { cal -> - DatePickerDialog( - context, - this@TimePickerDialogFragment, - cal.year, - cal.month, - cal.day, - ).apply { setMinMaxDate(datePicker) } + return when (selectionType) { + SELECTION_TYPE_DATE, SELECTION_TYPE_DATE_AND_TIME -> { + createMaterialDatePickerDialog(isDateTimePicker = selectionType == SELECTION_TYPE_DATE_AND_TIME) + Dialog(requireContext()) } - SELECTION_TYPE_DATE_AND_TIME -> AlertDialog.Builder(context) - .setView(inflateDateTimePicker(LayoutInflater.from(context))) - .create() - .also { - it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this) - it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this) - } - SELECTION_TYPE_MONTH -> AlertDialog.Builder(context) - .setTitle(R.string.mozac_feature_prompts_set_month) - .setView(inflateDateMonthPicker()) - .create() - .also { - it.setButton(BUTTON_POSITIVE, context.getString(R.string.mozac_feature_prompts_set_date), this) - it.setButton(BUTTON_NEGATIVE, context.getString(R.string.mozac_feature_prompts_cancel), this) + + SELECTION_TYPE_TIME -> { + val step = stepSize?.toFloat() + if (shouldShowSecondsPicker(step) && step != null) { + createTimeStepPickerDialog(step) + } else { + createMaterialTimePickerDialog(selectedDate.time) + Dialog(requireContext()) } - else -> throw IllegalArgumentException() - } + } - dialog.also { - it.setCancelable(true) - it.setButton(BUTTON_NEUTRAL, context.getString(R.string.mozac_feature_prompts_clear), this) + SELECTION_TYPE_MONTH -> createMonthPickerDialog() + else -> throw IllegalArgumentException("Invalid selection type: $selectionType") } - - return dialog - } - - /** - * Called when the user touches outside of the dialog. - */ - override fun onCancel(dialog: DialogInterface) { - super.onCancel(dialog) - onClick(dialog, BUTTON_NEGATIVE) } override fun onStart() { super.onStart() - val alertDialog = dialog if (alertDialog is AlertDialog) { // We want to call the extension function after the show() call on the dialog, @@ -134,74 +110,103 @@ internal class TimePickerDialogFragment : } } - // Create the appropriate time picker dialog for the given step value. - private fun createTimePickerDialog(context: Context): AlertDialog { - // Create the Android time picker dialog - fun createTimePickerDialog(): AlertDialog { - return initialDate.toCalendar().let { cal -> - TimePickerDialog( - context, - this, - cal.hour, - cal.minute, - DateFormat.is24HourFormat(context), + private fun createMaterialTimePickerDialog(dateSelection: Long? = null) { + val calendar = initialDate.toCalendar() + val is24Hour = DateFormat.is24HourFormat(requireContext()) + val timeFormat = if (is24Hour) TimeFormat.CLOCK_24H else TimeFormat.CLOCK_12H + + val timePicker = MaterialTimePicker.Builder() + .setTimeFormat(timeFormat) + .setHour(calendar.hour) + .setMinute(calendar.minute) + .setTitleText(R.string.mozac_feature_prompts_set_time) + .build() + + timePicker.addOnPositiveButtonClickListener { + val finalCalendar = ( + dateSelection?.let { + Calendar.getInstance().apply { + timeInMillis = it - TimeZone.getDefault().getOffset(it) + } + } ?: selectedDate.toCalendar() ) - } + + finalCalendar.set(Calendar.HOUR_OF_DAY, timePicker.hour) + finalCalendar.set(Calendar.MINUTE, timePicker.minute) + finalCalendar.set(Calendar.SECOND, 0) + finalCalendar.set(Calendar.MILLISECOND, 0) + selectedDate = finalCalendar.time + + feature?.onConfirm(sessionId, promptRequestUID, selectedDate) + dismiss() + } + timePicker.addOnNegativeButtonClickListener { + feature?.onCancel(sessionId, promptRequestUID) + dismiss() } + timePicker.addOnCancelListener { + feature?.onCancel(sessionId, promptRequestUID) + dismiss() + } + timePicker.show(parentFragmentManager, timePicker.toString()) + } - // Create the custom time picker dialog - fun createTimeStepPickerDialog(stepValue: Float): AlertDialog { - return AlertDialog.Builder(context) - .setTitle(R.string.mozac_feature_prompts_set_time) - .setView( - TimePrecisionPicker( - context = requireContext(), - selectedTime = initialDate.toCalendar(), - maxTime = maximumDate?.toCalendar() - ?: TimePrecisionPicker.getDefaultMaxTime(), - minTime = minimumDate?.toCalendar() - ?: TimePrecisionPicker.getDefaultMinTime(), - stepValue = stepValue, - timeSetListener = this, - ), - ) - .create() - .also { - it.setButton( - BUTTON_POSITIVE, - context.getString(R.string.mozac_feature_prompts_set_date), - this, - ) - it.setButton( - BUTTON_NEGATIVE, - context.getString(R.string.mozac_feature_prompts_cancel), - this, - ) - } + private fun createMaterialDatePickerDialog(isDateTimePicker: Boolean = false) { + val constraintsBuilder = CalendarConstraints.Builder().apply { + minimumDate?.let { setValidator(DateValidatorPointForward.from(it.time)) } + maximumDate?.let { setValidator(DateValidatorPointBackward.before(it.time)) } } + val initialUtcTime = initialDate.time + TimeZone.getDefault().getOffset(initialDate.time) + + val datePicker = MaterialDatePicker.Builder.datePicker() + .setSelection(initialUtcTime) + .setPositiveButtonText(R.string.mozac_feature_prompts_set_date) + .setNegativeButtonText(R.string.mozac_feature_prompts_cancel) + .setCalendarConstraints(constraintsBuilder.build()) + .build() - return if (!shouldShowSecondsPicker(stepSize?.toFloat())) { - createTimePickerDialog() - } else { - stepSize?.let { - createTimeStepPickerDialog(it.toFloat()) - } ?: createTimePickerDialog() + datePicker.addOnPositiveButtonClickListener { selection -> + if (isDateTimePicker) { + // For the date-time picker, we dismiss the date picker first + // and then show the time picker. + datePicker.dismiss() + createMaterialTimePickerDialog(selection) + } else { + // For the date-only picker, we confirm the selection and dismiss everything. + val millis = (selection ?: 0L) - TimeZone.getDefault().getOffset(selection ?: 0L) + + selectedDate = Date(millis) + feature?.onConfirm(sessionId, promptRequestUID, selectedDate) + datePicker.dismiss() + dismissAllowingStateLoss() + } } + datePicker.addOnNegativeButtonClickListener { + feature?.onCancel(sessionId, promptRequestUID) + dismiss() + } + datePicker.addOnCancelListener { + feature?.onCancel(sessionId, promptRequestUID) + dismiss() + } + datePicker.show(parentFragmentManager, datePicker.toString()) + } + + private fun createMonthPickerDialog(): AlertDialog { + val view = inflateDateMonthPicker() + return buildDialogWithView(view, titleResId = R.string.mozac_feature_prompts_set_month) } - @SuppressLint("InflateParams") - private fun inflateDateTimePicker(inflater: LayoutInflater): View { - val view = inflater.inflate(R.layout.mozac_feature_prompts_date_time_picker, null) - val datePicker = view.findViewById<DatePicker>(R.id.date_picker) - val dateTimePicker = view.findViewById<TimePicker>(R.id.datetime_picker) - val cal = initialDate.toCalendar() + private fun buildDialogWithView(view: View, titleResId: Int? = null): AlertDialog { + val builder = MaterialAlertDialogBuilder(requireContext()) + .setView(view) + .setPositiveButton(R.string.mozac_feature_prompts_set_date, this) + .setNegativeButton(R.string.mozac_feature_prompts_cancel, this) + .setNeutralButton(R.string.mozac_feature_prompts_clear, this) - // Bind date picker - setMinMaxDate(datePicker) - datePicker.init(cal.year, cal.month, cal.day, this) - initTimePicker(dateTimePicker, cal) + titleResId?.let { builder.setTitle(it) } - return view + return builder.create() } private fun inflateDateMonthPicker(): View { @@ -214,72 +219,68 @@ internal class TimePickerDialogFragment : ) } - private fun initTimePicker(picker: TimePicker, cal: Calendar) { - picker.hour = cal.hour - picker.minute = cal.minute - picker.setIs24HourView(DateFormat.is24HourFormat(requireContext())) - picker.setOnTimeChangedListener(this) + fun createTimeStepPickerDialog(stepValue: Float): AlertDialog { + val view = TimePrecisionPicker( + context = requireContext(), + selectedTime = initialDate.toCalendar(), + maxTime = maximumDate?.toCalendar() ?: TimePrecisionPicker.getDefaultMaxTime(), + minTime = minimumDate?.toCalendar() ?: TimePrecisionPicker.getDefaultMinTime(), + stepValue = stepValue, + timeSetListener = this, + ) + return buildDialogWithView(view, titleResId = R.string.mozac_feature_prompts_set_time) } - private fun setMinMaxDate(datePicker: DatePicker) { - minimumDate?.let { - datePicker.minDate = it.time - } - maximumDate?.let { - datePicker.maxDate = it.time + override fun onClick(dialog: DialogInterface?, which: Int) { + when (which) { + BUTTON_POSITIVE -> feature?.onConfirm(sessionId, promptRequestUID, selectedDate) + BUTTON_NEGATIVE -> feature?.onCancel(sessionId, promptRequestUID) + BUTTON_NEUTRAL -> feature?.onClear(sessionId, promptRequestUID) } } - override fun onTimeSet( - picker: TimePrecisionPicker, - hour: Int, - minute: Int, - second: Int, - millisecond: Int, - ) { + override fun onTimeChanged(picker: TimePicker?, hourOfDay: Int, minute: Int) { val calendar = selectedDate.toCalendar() - calendar.hour = hour - calendar.minute = minute - calendar.second = second - calendar.millisecond = millisecond + calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) + calendar.set(Calendar.MINUTE, minute) selectedDate = calendar.time } + override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { + onTimeChanged(view, hourOfDay, minute) + onClick(null, BUTTON_POSITIVE) + } + override fun onDateChanged(view: DatePicker?, year: Int, monthOfYear: Int, dayOfMonth: Int) { val calendar = Calendar.getInstance() calendar.set(year, monthOfYear, dayOfMonth) selectedDate = calendar.time } - override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) { - onDateChanged(view, year, month, dayOfMonth) - onClick(null, BUTTON_POSITIVE) - } - override fun onDateSet(picker: MonthAndYearPicker, month: Int, year: Int) { onDateChanged(null, year, month, 0) } - override fun onTimeChanged(picker: TimePicker?, hourOfDay: Int, minute: Int) { + override fun onTimeSet( + picker: TimePrecisionPicker, + hour: Int, + minute: Int, + second: Int, + millisecond: Int, + ) { val calendar = selectedDate.toCalendar() - calendar.set(Calendar.HOUR_OF_DAY, hourOfDay) - calendar.set(Calendar.MINUTE, minute) + calendar.hour = hour + calendar.minute = minute + calendar.second = second + calendar.millisecond = millisecond selectedDate = calendar.time } - override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) { - onTimeChanged(view, hourOfDay, minute) + override fun onDateSet(view: DatePicker?, year: Int, month: Int, dayOfMonth: Int) { + onDateChanged(view, year, month, dayOfMonth) onClick(null, BUTTON_POSITIVE) } - override fun onClick(dialog: DialogInterface?, which: Int) { - when (which) { - BUTTON_POSITIVE -> feature?.onConfirm(sessionId, promptRequestUID, selectedDate) - BUTTON_NEGATIVE -> feature?.onCancel(sessionId, promptRequestUID) - BUTTON_NEUTRAL -> feature?.onClear(sessionId, promptRequestUID) - } - } - companion object { /** * A builder method for creating a [TimePickerDialogFragment] diff --git a/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt b/mobile/android/android-components/components/feature/prompts/src/test/java/mozilla/components/feature/prompts/dialog/TimePickerDialogFragmentTest.kt @@ -4,15 +4,13 @@ package mozilla.components.feature.prompts.dialog -import android.app.AlertDialog -import android.app.DatePickerDialog -import android.app.TimePickerDialog import android.content.DialogInterface.BUTTON_NEUTRAL import android.content.DialogInterface.BUTTON_POSITIVE import android.os.Looper.getMainLooper import android.widget.DatePicker import android.widget.NumberPicker import android.widget.TimePicker +import androidx.appcompat.app.AlertDialog import androidx.test.ext.junit.runners.AndroidJUnit4 import mozilla.components.feature.prompts.R import mozilla.components.feature.prompts.dialog.TimePickerDialogFragment.Companion.SELECTION_TYPE_DATE_AND_TIME @@ -44,75 +42,13 @@ import java.util.Date @RunWith(AndroidJUnit4::class) class TimePickerDialogFragmentTest { - @Mock private lateinit var mockFeature: Prompter - @Before fun setup() { + testContext.setTheme(com.google.android.material.R.style.Theme_MaterialComponents_Light) openMocks(this) } @Test - fun `build dialog`() { - val initialDate = "2019-11-29".toDate("yyyy-MM-dd") - val minDate = "2019-11-28".toDate("yyyy-MM-dd") - val maxDate = "2019-11-30".toDate("yyyy-MM-dd") - val fragment = spy( - TimePickerDialogFragment.newInstance("sessionId", "uid", true, initialDate, minDate, maxDate), - ) - - doReturn(appCompatContext).`when`(fragment).requireContext() - - val dialog = fragment.onCreateDialog(null) - dialog.show() - - val datePicker = (dialog as DatePickerDialog).datePicker - assertEquals("sessionId", fragment.sessionId) - assertEquals("uid", fragment.promptRequestUID) - assertEquals(2019, datePicker.year) - assertEquals(11, datePicker.month + 1) - assertEquals(29, datePicker.dayOfMonth) - assertEquals(minDate, Date(datePicker.minDate)) - assertEquals(maxDate, Date(datePicker.maxDate)) - } - - @Test - fun `Clicking on positive, neutral and negative button notifies the feature`() { - val initialDate = "2019-11-29".toDate("yyyy-MM-dd") - val fragment = spy( - TimePickerDialogFragment.newInstance("sessionId", "uid", false, initialDate, null, null), - ) - fragment.feature = mockFeature - - doReturn(appCompatContext).`when`(fragment).requireContext() - - val dialog = fragment.onCreateDialog(null) - dialog.show() - - val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE) - positiveButton.performClick() - shadowOf(getMainLooper()).idle() - - verify(mockFeature).onConfirm(eq("sessionId"), eq("uid"), any()) - - val neutralButton = dialog.getButton(BUTTON_NEUTRAL) - neutralButton.performClick() - shadowOf(getMainLooper()).idle() - - verify(mockFeature).onClear("sessionId", "uid") - } - - @Test - fun `touching outside of the dialog must notify the feature onCancel`() { - val fragment = spy( - TimePickerDialogFragment.newInstance("sessionId", "uid", true, Date(), null, null), - ) - fragment.feature = mockFeature - doReturn(testContext).`when`(fragment).requireContext() - fragment.onCancel(mock()) - verify(mockFeature).onCancel("sessionId", "uid") - } - - @Test fun `onTimeChanged must update the selectedDate`() { val dialogPicker = TimePickerDialogFragment.newInstance("sessionId", "uid", false, Date(), null, null) @@ -125,43 +61,6 @@ class TimePickerDialogFragmentTest { } @Test - fun `building a date and time picker`() { - val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm") - val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm") - val maxDate = "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm") - val fragment = spy( - TimePickerDialogFragment.newInstance( - "sessionId", - "uid", - true, - initialDate, - minDate, - maxDate, - SELECTION_TYPE_DATE_AND_TIME, - ), - ) - - doReturn(appCompatContext).`when`(fragment).requireContext() - - val dialog = fragment.onCreateDialog(null) - dialog.show() - - val datePicker = dialog.findViewById<DatePicker>(R.id.date_picker) - - assertEquals(2018, datePicker.year) - assertEquals(6, datePicker.month + 1) - assertEquals(12, datePicker.dayOfMonth) - - assertEquals(minDate, Date(datePicker.minDate)) - assertEquals(maxDate, Date(datePicker.maxDate)) - - val timePicker = dialog.findViewById<TimePicker>(R.id.datetime_picker) - - assertEquals(19, timePicker.hour) - assertEquals(30, timePicker.minute) - } - - @Test fun `building a month picker`() { val initialDate = "2018-06".toDate("yyyy-MM") val minDate = "2018-04".toDate("yyyy-MM") @@ -207,30 +106,6 @@ class TimePickerDialogFragmentTest { assertEquals(7, selectedDate.month) } - @Test - fun `building a time picker`() { - val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm") - val minDate = "2018-06-07T00:00".toDate("yyyy-MM-dd'T'HH:mm") - val maxDate = "2018-06-14T00:00".toDate("yyyy-MM-dd'T'HH:mm") - val fragment = spy( - TimePickerDialogFragment.newInstance( - "sessionId", - "uid", - true, - initialDate, - minDate, - maxDate, - SELECTION_TYPE_TIME, - ), - ) - - doReturn(appCompatContext).`when`(fragment).requireContext() - - val dialog = fragment.onCreateDialog(null) - dialog.show() - assertTrue(dialog is TimePickerDialog) - } - @Test(expected = IllegalArgumentException::class) fun `creating a TimePickerDialogFragment with an invalid type selection will throw an exception`() { val initialDate = "2018-06-12T19:30".toDate("yyyy-MM-dd'T'HH:mm") diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/WebControlsTest.kt @@ -59,14 +59,10 @@ class WebControlsTest : TestSetup() { clickPageObject(itemWithResId("submitDate")) verifyNoDateIsSelected() clickPageObject(itemWithResId("calendar")) - clickPageObject(itemWithDescription("$currentDay $currentMonth $currentYear")) - clickPageObject(itemContainingText("OK")) + clickPageObject(itemWithDescription("$currentMonth $currentDay")) + clickPageObject(itemContainingText("Set")) clickPageObject(itemWithResId("submitDate")) verifySelectedDate() - clickPageObject(itemWithResId("calendar")) - clickPageObject(itemContainingText("CLEAR")) - clickPageObject(itemWithResId("submitDate")) - verifyNoDateIsSelected() } } @@ -78,7 +74,7 @@ class WebControlsTest : TestSetup() { navigationToolbar { }.enterURLAndEnterToBrowser(htmlControlsPage.url) { clickPageObject(itemWithResId("clock")) - clickPageObject(itemContainingText("CANCEL")) + clickPageObject(itemContainingText("Cancel")) clickPageObject(itemWithResId("submitTime")) verifyNoTimeIsSelected(hour, minute) clickPageObject(itemWithResId("clock")) @@ -86,10 +82,6 @@ class WebControlsTest : TestSetup() { clickPageObject(itemContainingText("OK")) clickPageObject(itemWithResId("submitTime")) verifySelectedTime(hour, minute) - clickPageObject(itemWithResId("clock")) - clickPageObject(itemContainingText("CLEAR")) - clickPageObject(itemWithResId("submitTime")) - verifyNoTimeIsSelected(hour, minute) } } diff --git a/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt b/mobile/android/fenix/app/src/androidTest/java/org/mozilla/fenix/ui/robots/BrowserRobot.kt @@ -10,7 +10,6 @@ import android.content.Context import android.net.Uri import android.os.SystemClock import android.util.Log -import android.widget.TimePicker import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertAny import androidx.compose.ui.test.assertIsDisplayed @@ -32,10 +31,8 @@ import androidx.test.espresso.Espresso.onView import androidx.test.espresso.action.ViewActions.click import androidx.test.espresso.action.ViewActions.longClick import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.PickerActions import androidx.test.espresso.matcher.RootMatchers.isDialog import androidx.test.espresso.matcher.ViewMatchers.Visibility -import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.withContentDescription import androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility @@ -770,11 +767,9 @@ class BrowserRobot { fun selectTime(hour: Int, minute: Int) { Log.i(TAG, "selectTime: Trying to select time picker hour: $hour and minute: $minute") - onView( - isAssignableFrom(TimePicker::class.java), - ).inRoot( - isDialog(), - ).perform(PickerActions.setTime(hour, minute)) + itemWithDescription("$hour o'clock").click() + waitForAppWindowToBeUpdated() + itemWithDescription("$minute minutes").click() Log.i(TAG, "selectTime: Selected time picker hour: $hour and minute: $minute") } diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/doh/root/DohSettingsScreen.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/settings/doh/root/DohSettingsScreen.kt @@ -332,6 +332,7 @@ private fun DohSelection( if (state.selectedProvider is Provider.Custom && state.isCustomProviderDialogOn) { AlertDialogAddCustomProvider( + customProviderUrl = state.selectedProvider.url, customProviderErrorState = state.customProviderErrorState, onCustomCancelClicked = { onCustomCancelClicked() }, onCustomAddClicked = { url -> @@ -443,11 +444,12 @@ private fun buildProviderMenuItems( @Composable private fun AlertDialogAddCustomProvider( + customProviderUrl: String, customProviderErrorState: CustomProviderErrorState, onCustomCancelClicked: () -> Unit, onCustomAddClicked: (String) -> Unit, ) { - var customProviderInput by remember { mutableStateOf("") } + var customProviderInput by remember { mutableStateOf(customProviderUrl) } val onCustomProviderInputChange: (String) -> Unit = { it -> customProviderInput = it } val nonHttpsString = stringResource(R.string.preference_doh_provider_custom_dialog_error_https) val invalidString = stringResource(R.string.preference_doh_provider_custom_dialog_error_invalid) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/banner/TabsTrayBanner.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/banner/TabsTrayBanner.kt @@ -8,8 +8,11 @@ package org.mozilla.fenix.tabstray.ui.banner import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CenterAlignedTopAppBar @@ -75,6 +78,7 @@ private val TabIndicatorRoundedCornerDp = 100.dp * @param syncedTabCount The total number of open synced tabs. * @param selectionMode [TabsTrayState.Mode] indicating the current selection mode (e.g., normal, multi-select). * @param isInDebugMode True for debug variant or if secret menu is enabled for this session. + * @param statusBarHeight The height of the system status bar. * @param shouldShowTabAutoCloseBanner Whether the tab auto-close banner should be displayed. * @param shouldShowLockPbmBanner Whether the lock private browsing mode banner should be displayed. * @param scrollBehavior Defines how the [TabPageBanner] should behave when the content under it is scrolled. @@ -101,6 +105,7 @@ fun TabsTrayBanner( syncedTabCount: Int, selectionMode: Mode, isInDebugMode: Boolean, + statusBarHeight: Dp, shouldShowTabAutoCloseBanner: Boolean, shouldShowLockPbmBanner: Boolean, scrollBehavior: TopAppBarScrollBehavior, @@ -158,6 +163,7 @@ fun TabsTrayBanner( normalTabCount = normalTabCount, privateTabCount = privateTabCount, syncedTabCount = syncedTabCount, + statusBarHeight = statusBarHeight, scrollBehavior = scrollBehavior, onTabPageIndicatorClicked = onTabPageIndicatorClicked, ) @@ -167,6 +173,8 @@ fun TabsTrayBanner( !hasAcknowledgedAutoCloseBanner && showTabAutoCloseBanner -> { onTabAutoCloseBannerShown() + BannerPadding(scrollBehavior = scrollBehavior, statusBarHeight = statusBarHeight) + HorizontalDivider() Banner( @@ -185,6 +193,8 @@ fun TabsTrayBanner( } !hasAcknowledgedPbmLockBanner && shouldShowLockPbmBanner -> { + BannerPadding(scrollBehavior = scrollBehavior, statusBarHeight = statusBarHeight) + // After this bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1965545 // is resolved, we should swap the button 1 and button 2 click actions. Banner( @@ -206,6 +216,18 @@ fun TabsTrayBanner( } } +@Composable +private fun BannerPadding( + scrollBehavior: TopAppBarScrollBehavior, + statusBarHeight: Dp, +) { + val padding by remember(statusBarHeight, scrollBehavior.state.collapsedFraction) { + derivedStateOf { statusBarHeight * scrollBehavior.state.collapsedFraction } + } + + Spacer(modifier = Modifier.height(padding)) +} + /** * Banner displayed when in [Mode.Normal]. * @@ -213,6 +235,7 @@ fun TabsTrayBanner( * @param normalTabCount The amount of open Normal tabs. * @param privateTabCount The amount of open Private tabs. * @param syncedTabCount The amount of synced tabs. + * @param statusBarHeight The height of the system status bar. * @param scrollBehavior Defines how the [TabPageBanner] should behave when the content under it is scrolled. * @param onTabPageIndicatorClicked Invoked when the user clicks on a tab page button. Passes along the * [Page] that was clicked. @@ -224,6 +247,7 @@ private fun TabPageBanner( normalTabCount: Int, privateTabCount: Int, syncedTabCount: Int, + statusBarHeight: Dp, scrollBehavior: TopAppBarScrollBehavior, onTabPageIndicatorClicked: (Page) -> Unit, ) { @@ -232,95 +256,104 @@ private fun TabPageBanner( CenterAlignedTopAppBar( title = { - PrimaryTabRow( - selectedTabIndex = selectedTabIndex, - modifier = Modifier.fillMaxWidth(), - contentColor = MaterialTheme.colorScheme.primary, - containerColor = Color.Transparent, - indicator = { - TabRowDefaults.PrimaryIndicator( - modifier = Modifier.tabIndicatorOffset( - selectedTabIndex = selectedTabIndex, - matchContentSize = true, - ), - width = Dp.Unspecified, - shape = RoundedCornerShape( - topStart = TabIndicatorRoundedCornerDp, - topEnd = TabIndicatorRoundedCornerDp, - ), - ) - }, - divider = {}, - ) { - val privateTabDescription = stringResource( - id = R.string.tabs_header_private_tabs_counter_title, - privateTabCount.toString(), - ) - val normalTabDescription = stringResource( - id = R.string.tabs_header_normal_tabs_counter_title, - normalTabCount.toString(), - ) - val syncedTabDescription = stringResource( - id = R.string.tabs_header_synced_tabs_counter_title, - syncedTabCount.toString(), - ) - - Tab( - selected = selectedPage == Page.PrivateTabs, - onClick = { onTabPageIndicatorClicked(Page.PrivateTabs) }, + Column { + Spacer( modifier = Modifier - .testTag(TabsTrayTestTag.PRIVATE_TABS_PAGE_BUTTON) - .semantics { - contentDescription = privateTabDescription - } - .height(RowHeight), - unselectedContentColor = inactiveColor, + .height(statusBarHeight) + .fillMaxWidth(), + ) + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, + modifier = Modifier.fillMaxWidth(), + contentColor = MaterialTheme.colorScheme.primary, + containerColor = Color.Transparent, + indicator = { + TabRowDefaults.PrimaryIndicator( + modifier = Modifier.tabIndicatorOffset( + selectedTabIndex = selectedTabIndex, + matchContentSize = true, + ), + width = Dp.Unspecified, + shape = RoundedCornerShape( + topStart = TabIndicatorRoundedCornerDp, + topEnd = TabIndicatorRoundedCornerDp, + ), + ) + }, + divider = {}, ) { - Text( - text = stringResource(id = R.string.tabs_header_private_tabs_title), - style = FirefoxTheme.typography.button, + val privateTabDescription = stringResource( + id = R.string.tabs_header_private_tabs_counter_title, + privateTabCount.toString(), ) - } - - Tab( - selected = selectedPage == Page.NormalTabs, - onClick = { onTabPageIndicatorClicked(Page.NormalTabs) }, - modifier = Modifier - .testTag(TabsTrayTestTag.NORMAL_TABS_PAGE_BUTTON) - .semantics { - contentDescription = normalTabDescription - } - .height(RowHeight), - unselectedContentColor = inactiveColor, - ) { - Text( - text = stringResource(R.string.tabs_header_normal_tabs_title), - style = FirefoxTheme.typography.button, + val normalTabDescription = stringResource( + id = R.string.tabs_header_normal_tabs_counter_title, + normalTabCount.toString(), ) - } - - Tab( - selected = selectedPage == Page.SyncedTabs, - onClick = { onTabPageIndicatorClicked(Page.SyncedTabs) }, - modifier = Modifier - .testTag(TabsTrayTestTag.SYNCED_TABS_PAGE_BUTTON) - .semantics { - contentDescription = syncedTabDescription - } - .height(RowHeight), - unselectedContentColor = inactiveColor, - ) { - Text( - text = stringResource(id = R.string.tabs_header_synced_tabs_title), - style = FirefoxTheme.typography.button, + val syncedTabDescription = stringResource( + id = R.string.tabs_header_synced_tabs_counter_title, + syncedTabCount.toString(), ) + + Tab( + selected = selectedPage == Page.PrivateTabs, + onClick = { onTabPageIndicatorClicked(Page.PrivateTabs) }, + modifier = Modifier + .testTag(TabsTrayTestTag.PRIVATE_TABS_PAGE_BUTTON) + .semantics { + contentDescription = privateTabDescription + } + .height(RowHeight), + unselectedContentColor = inactiveColor, + ) { + Text( + text = stringResource(id = R.string.tabs_header_private_tabs_title), + style = FirefoxTheme.typography.button, + ) + } + + Tab( + selected = selectedPage == Page.NormalTabs, + onClick = { onTabPageIndicatorClicked(Page.NormalTabs) }, + modifier = Modifier + .testTag(TabsTrayTestTag.NORMAL_TABS_PAGE_BUTTON) + .semantics { + contentDescription = normalTabDescription + } + .height(RowHeight), + unselectedContentColor = inactiveColor, + ) { + Text( + text = stringResource(R.string.tabs_header_normal_tabs_title), + style = FirefoxTheme.typography.button, + ) + } + + Tab( + selected = selectedPage == Page.SyncedTabs, + onClick = { onTabPageIndicatorClicked(Page.SyncedTabs) }, + modifier = Modifier + .testTag(TabsTrayTestTag.SYNCED_TABS_PAGE_BUTTON) + .semantics { + contentDescription = syncedTabDescription + } + .height(RowHeight), + unselectedContentColor = inactiveColor, + ) { + Text( + text = stringResource(id = R.string.tabs_header_synced_tabs_title), + style = FirefoxTheme.typography.button, + ) + } } } }, - expandedHeight = RowHeight, + expandedHeight = RowHeight + statusBarHeight, colors = TopAppBarDefaults.centerAlignedTopAppBarColors( scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, ), + // Allow this TopAppBar to be drawn behind the status bar instead of stopping at it. + windowInsets = TopAppBarDefaults.windowInsets.only(WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior, ) } @@ -540,6 +573,7 @@ private fun TabsTrayBannerPreviewRoot( syncedTabCount = 0, selectionMode = state.mode, isInDebugMode = false, + statusBarHeight = 50.dp, shouldShowTabAutoCloseBanner = shouldShowTabAutoCloseBanner, shouldShowLockPbmBanner = shouldShowLockPbmBanner, scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior(), diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/TabLayout.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabpage/TabLayout.kt @@ -335,7 +335,6 @@ private fun TabList( .padding( start = TabListPadding, end = TabListPadding, - top = TabListPadding, ) .clip(TabListCornerShape) .background(MaterialTheme.colorScheme.surface) diff --git a/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt b/mobile/android/fenix/app/src/main/java/org/mozilla/fenix/tabstray/ui/tabstray/TabsTray.kt @@ -6,9 +6,17 @@ package org.mozilla.fenix.tabstray.ui.tabstray +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.BottomAppBarDefaults @@ -68,6 +76,7 @@ import org.mozilla.fenix.tabstray.ui.syncedtabs.OnTabCloseClick as OnSyncedTabCl * BottomAppBar. */ private val ScaffoldFabOffsetCorrection = 4.dp +private const val SPACER_BACKGROUND_ALPHA = 0.75f /** * Top-level UI for displaying the Tabs Tray feature. @@ -188,6 +197,11 @@ fun TabsTray( .sumOf { deviceSection: SyncedTabsListItem.DeviceSection -> deviceSection.tabs.size } } + val systemBarsInsets = WindowInsets.systemBars.asPaddingValues() + val statusBarHeight = remember(systemBarsInsets) { + systemBarsInsets.calculateTopPadding() + } + val topAppBarScrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val bottomAppBarScrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() @@ -216,6 +230,7 @@ fun TabsTray( syncedTabCount = syncedTabCount, selectionMode = tabsTrayState.mode, isInDebugMode = isInDebugMode, + statusBarHeight = statusBarHeight, shouldShowTabAutoCloseBanner = shouldShowTabAutoCloseBanner, shouldShowLockPbmBanner = shouldShowLockPbmBanner, scrollBehavior = topAppBarScrollBehavior, @@ -261,71 +276,83 @@ fun TabsTray( floatingActionButtonPosition = FabPosition.EndOverlay, containerColor = MaterialTheme.colorScheme.surface, ) { paddingValues -> - HorizontalPager( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), - state = pagerState, - beyondViewportPageCount = 2, - userScrollEnabled = false, - ) { position -> - when (Page.positionToPage(position)) { - Page.NormalTabs -> { - NormalTabsPage( - normalTabs = tabsTrayState.normalTabs, - inactiveTabs = tabsTrayState.inactiveTabs, - selectedTabId = tabsTrayState.selectedTabId, - selectionMode = tabsTrayState.mode, - inactiveTabsExpanded = tabsTrayState.inactiveTabsExpanded, - displayTabsInGrid = displayTabsInGrid, - onTabClose = onTabClose, - onTabClick = onTabClick, - onTabLongClick = onTabLongClick, - shouldShowInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog, - onInactiveTabsHeaderClick = onInactiveTabsHeaderClick, - onDeleteAllInactiveTabsClick = onDeleteAllInactiveTabsClick, - onInactiveTabsAutoCloseDialogShown = onInactiveTabsAutoCloseDialogShown, - onInactiveTabAutoCloseDialogCloseButtonClick = onInactiveTabAutoCloseDialogCloseButtonClick, - onEnableInactiveTabAutoCloseClick = onEnableInactiveTabAutoCloseClick, - onInactiveTabClick = onInactiveTabClick, - onInactiveTabClose = onInactiveTabClose, - onMove = onMove, - shouldShowInactiveTabsCFR = shouldShowInactiveTabsCFR, - onInactiveTabsCFRShown = onInactiveTabsCFRShown, - onInactiveTabsCFRClick = onInactiveTabsCFRClick, - onInactiveTabsCFRDismiss = onInactiveTabsCFRDismiss, - onTabDragStart = { - tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode) - }, - ) - } + Box { + HorizontalPager( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + state = pagerState, + beyondViewportPageCount = 2, + userScrollEnabled = false, + ) { position -> + when (Page.positionToPage(position)) { + Page.NormalTabs -> { + NormalTabsPage( + normalTabs = tabsTrayState.normalTabs, + inactiveTabs = tabsTrayState.inactiveTabs, + selectedTabId = tabsTrayState.selectedTabId, + selectionMode = tabsTrayState.mode, + inactiveTabsExpanded = tabsTrayState.inactiveTabsExpanded, + displayTabsInGrid = displayTabsInGrid, + onTabClose = onTabClose, + onTabClick = onTabClick, + onTabLongClick = onTabLongClick, + shouldShowInactiveTabsAutoCloseDialog = shouldShowInactiveTabsAutoCloseDialog, + onInactiveTabsHeaderClick = onInactiveTabsHeaderClick, + onDeleteAllInactiveTabsClick = onDeleteAllInactiveTabsClick, + onInactiveTabsAutoCloseDialogShown = onInactiveTabsAutoCloseDialogShown, + onInactiveTabAutoCloseDialogCloseButtonClick = onInactiveTabAutoCloseDialogCloseButtonClick, + onEnableInactiveTabAutoCloseClick = onEnableInactiveTabAutoCloseClick, + onInactiveTabClick = onInactiveTabClick, + onInactiveTabClose = onInactiveTabClose, + onMove = onMove, + shouldShowInactiveTabsCFR = shouldShowInactiveTabsCFR, + onInactiveTabsCFRShown = onInactiveTabsCFRShown, + onInactiveTabsCFRClick = onInactiveTabsCFRClick, + onInactiveTabsCFRDismiss = onInactiveTabsCFRDismiss, + onTabDragStart = { + tabsTrayStore.dispatch(TabsTrayAction.ExitSelectMode) + }, + ) + } - Page.PrivateTabs -> { - PrivateTabsPage( - privateTabs = tabsTrayState.privateTabs, - selectedTabId = tabsTrayState.selectedTabId, - selectionMode = tabsTrayState.mode, - displayTabsInGrid = displayTabsInGrid, - privateTabsLocked = isPbmLocked, - onTabClose = onTabClose, - onTabClick = onTabClick, - onTabLongClick = onTabLongClick, - onMove = onMove, - onUnlockPbmClick = onUnlockPbmClick, - ) - } + Page.PrivateTabs -> { + PrivateTabsPage( + privateTabs = tabsTrayState.privateTabs, + selectedTabId = tabsTrayState.selectedTabId, + selectionMode = tabsTrayState.mode, + displayTabsInGrid = displayTabsInGrid, + privateTabsLocked = isPbmLocked, + onTabClose = onTabClose, + onTabClick = onTabClick, + onTabLongClick = onTabLongClick, + onMove = onMove, + onUnlockPbmClick = onUnlockPbmClick, + ) + } - Page.SyncedTabs -> { - SyncedTabsPage( - isSignedIn = isSignedIn, - syncedTabs = tabsTrayState.syncedTabs, - onTabClick = onSyncedTabClick, - onTabClose = onSyncedTabClose, - onSignInClick = onSignInClick, - ) + Page.SyncedTabs -> { + SyncedTabsPage( + isSignedIn = isSignedIn, + syncedTabs = tabsTrayState.syncedTabs, + onTabClick = onSyncedTabClick, + onTabClose = onSyncedTabClose, + onSignInClick = onSignInClick, + ) + } } } } + Spacer( + Modifier + .height(statusBarHeight) + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surface.copy( + SPACER_BACKGROUND_ALPHA, + ), + ), + ) } } @@ -526,9 +553,9 @@ private class TabsTrayStateParameterProvider : PreviewParameterProvider<TabsTray showInactiveTabsAutoCloseDialog = true, ), // TabsTray Private Tabs Preview - TabsTrayPreviewModel( - selectedPage = Page.PrivateTabs, - privateTabs = generateFakeTabsList(isPrivate = true), + TabsTrayPreviewModel( + selectedPage = Page.PrivateTabs, + privateTabs = generateFakeTabsList(isPrivate = true), ), // TabsTray Synced Tab Preview TabsTrayPreviewModel( @@ -569,7 +596,10 @@ private data class TabsTrayPreviewModel( val isSignedIn: Boolean = true, ) -private fun generateFakeTabsList(tabCount: Int = 10, isPrivate: Boolean = false): List<TabSessionState> = +private fun generateFakeTabsList( + tabCount: Int = 10, + isPrivate: Boolean = false, +): List<TabSessionState> = List(tabCount) { index -> TabSessionState( id = "tabId$index-$isPrivate", diff --git a/mobile/android/fenix/app/src/main/res/color/time_picker_button_background_tint.xml b/mobile/android/fenix/app/src/main/res/color/time_picker_button_background_tint.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- This Source Code Form is subject to the terms of the Mozilla Public + - License, v. 2.0. If a copy of the MPL was not distributed with this + - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:color="?attr/colorSecondaryContainer" android:state_checked="true"/> +</selector> diff --git a/mobile/android/fenix/app/src/main/res/values/styles.xml b/mobile/android/fenix/app/src/main/res/values/styles.xml @@ -16,13 +16,12 @@ <style name="NormalThemeBase" parent="Theme.Material3.DayNight.NoActionBar"> <item name="preferenceTheme">@style/PreferenceTheme</item> - + <item name="materialTimePickerTheme">@style/Normal.MaterialTimePicker</item> + <item name="materialCalendarTheme">@style/MaterialCalendar</item> <!-- Android system styling --> <item name="searchViewStyle">@style/SearchViewStyle</item> <item name="autoCompleteTextViewStyle">@style/AutoCompleteTextViewStyle</item> - <item name="android:textAlignment">viewStart</item> <item name="android:windowContentTransitions">true</item> - <item name="android:datePickerDialogTheme">@style/DatePickerDialogStyle</item> <item name="android:windowAnimationStyle">@style/WindowAnimationTransition</item> <item name="android:progressBarStyleHorizontal">@style/progressBarStyleHorizontal</item> <item name="android:statusBarColor">@android:color/transparent</item> @@ -200,32 +199,6 @@ <item name="tabCounterTintColor">?attr/textPrimary</item> </style> - <!-- A theme to fix DatePickerDialogs year picker text alignment - https://bugzilla.mozilla.org/show_bug.cgi?id=1986252 --> - <style name="DatePickerDialogStyle" parent="ThemeOverlay.MaterialComponents.Dialog"> - <item name="android:textAlignment">gravity</item> - <item name="android:windowBackground">?attr/layer1</item> - <item name="android:colorEdgeEffect">@color/accent_normal_theme</item> - <item name="android:colorAccent">@color/fx_mobile_text_color_primary</item> - <item name="android:textColorPrimary">@color/state_list_text_color</item> - <item name="android:textColorSecondary">@color/secondary_state_list_text_color</item> - <item name="android:colorForeground">@color/toggle_off_track_normal_theme</item> - </style> - - <style name="PrivateDatePickerDialogStyle" parent="ThemeOverlay.MaterialComponents.Dialog"> - <item name="android:textAlignment">gravity</item> - <!-- For some reason in private mode it was trying to load @drawable/? - as the window background. This resolved to #424242. Hard coding - this until we know what color we want private mode dialogs to be. - see: https://bugzilla.mozilla.org/show_bug.cgi?id=1986252--> - <item name="android:windowBackground">#424242</item> - <item name="android:colorEdgeEffect">@color/accent_private_theme</item> - <item name="android:colorAccent">@color/fx_mobile_private_text_color_primary</item> - <item name="android:textColorPrimary">@color/state_list_text_color</item> - <item name="android:textColorSecondary">@color/secondary_state_list_text_color</item> - <item name="android:colorForeground">@color/toggle_off_track_dark_theme</item> - </style> - <!-- A theme derived from the normal activity theme, but to look and behave like a dialog --> <style name="DialogActivityTheme" parent="NormalTheme"> <item name="android:windowElevation">16dp</item> @@ -261,6 +234,25 @@ <item name="materialAlertDialogBodyTextStyle">@style/MaterialAlertDialog.App.Body.Text.Private</item> </style> + <style name="Normal.MaterialTimePicker" parent="ThemeOverlay.Material3.MaterialTimePicker"> + <item name="materialButtonOutlinedStyle">@style/My.Widget.MaterialComponents.TimePicker.Button</item> + </style> + + <style name="Private.MaterialTimePicker" parent="ThemeOverlay.Material3.MaterialTimePicker"> + <item name="materialClockStyle">@style/Private.MaterialTimePickerClock</item> + <item name="materialButtonOutlinedStyle">@style/My.Widget.MaterialComponents.TimePicker.Button</item> + </style> + + <style name="My.Widget.MaterialComponents.TimePicker.Button" parent="Widget.Material3.MaterialTimePicker.Button"> + <item name="backgroundTint">@color/time_picker_button_background_tint</item> + </style> + + <style name="Private.MaterialTimePickerClock" parent="Widget.MaterialComponents.TimePicker.Clock"> + <item name="clockFaceBackgroundColor">?attr/colorSurfaceContainerHighest</item> + <item name="clockHandColor">?attr/colorPrimary</item> + <item name="clockNumberTextColor">?attr/colorOnSurface</item> + </style> + <style name="MaterialAlertDialog.App.Body.Text.Private" parent="MaterialAlertDialog.App.Body.Text"> <item name="android:textColor">@color/fx_mobile_private_text_color_primary</item> </style> @@ -295,6 +287,23 @@ <item name="android:gravity">center</item> </style> + <style name="MaterialCalendar" parent="ThemeOverlay.Material3.MaterialCalendar"> + <item name="materialCalendarHeaderDivider">@style/MaterialCalendar.HeaderDivider</item> + <item name="materialCalendarHeaderTitle">@style/MaterialCalendar.Title.Text</item> + <item name="buttonBarPositiveButtonStyle">@style/DialogButtonStyle</item> + <item name="buttonBarNegativeButtonStyle">@style/DialogButtonStyle</item> + </style> + + <style name="MaterialCalendar.Title.Text" parent="@style/Widget.Material3.MaterialCalendar.HeaderTitle"> + <item name="android:textColor">?attr/colorOnSurface</item> + <item name="android:titleTextStyle">?attr/textAppearanceHeadlineLarge</item> + </style> + + <style name="MaterialCalendar.HeaderDivider" parent="Widget.Material3.MaterialCalendar.HeaderDivider"> + <item name="android:visibility">visible</item> + <item name="android:background">?attr/colorOutlineVariant</item> + </style> + <style name="MaterialAlertDialogShapeAppearance" parent=""> <item name="cornerFamily">rounded</item> <item name="cornerSize">@dimen/material_dialog_corner_radius</item> @@ -308,13 +317,13 @@ <style name="PrivateThemeBase" parent="Theme.Material3.DayNight.NoActionBar"> <!-- Android system styling --> + <item name="materialTimePickerTheme">@style/Private.MaterialTimePicker</item> + <item name="materialCalendarTheme">@style/MaterialCalendar</item> <item name="searchViewStyle">@style/SearchViewStyle</item> <item name="android:textColorLink">@color/fx_mobile_private_text_color_accent</item> <item name="preferenceTheme">@style/PreferenceTheme</item> <item name="checkboxStyle">@style/App.Widget.CompoundButton.CheckBox</item> <item name="autoCompleteTextViewStyle">@style/AutoCompleteTextViewStyle</item> - <item name="android:textAlignment">viewStart</item> - <item name="android:datePickerDialogTheme">@style/PrivateDatePickerDialogStyle</item> <item name="android:windowContentTransitions">true</item> <item name="android:windowAnimationStyle">@style/WindowAnimationTransition</item> <item name="android:progressBarStyleHorizontal">@style/progressBarStyleHorizontal</item> diff --git a/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/MenuStoreTest.kt b/mobile/android/fenix/app/src/test/java/org/mozilla/fenix/components/menu/MenuStoreTest.kt @@ -140,9 +140,9 @@ class MenuStoreTest { } assertEquals(firefoxTab, newState.browserMenuState!!.selectedTab) - assertNull(state.browserMenuState.bookmarkState.guid) - assertFalse(state.browserMenuState.bookmarkState.isBookmarked) - assertFalse(state.browserMenuState.isPinned) + assertNull(newState.browserMenuState.bookmarkState.guid) + assertFalse(newState.browserMenuState.bookmarkState.isBookmarked) + assertFalse(newState.browserMenuState.isPinned) val bookmarkState = BookmarkState(guid = "id", isBookmarked = true) val isPinned = true diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/WebControlsTest.kt @@ -169,7 +169,7 @@ class WebControlsTest : TestSetup() { progressBar.waitUntilGone(waitingTime) clickCalendarForm() selectDate() - clickButtonWithText("OK") + clickButtonWithText("Set") clickSubmitDateButton() verifySelectedDate() } diff --git a/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/BrowserRobot.kt b/mobile/android/focus-android/app/src/androidTest/java/org/mozilla/focus/activity/robots/BrowserRobot.kt @@ -291,12 +291,10 @@ class BrowserRobot { fun clickCalendarForm() = clickPageObject(webPageItemWithResourceId("calendar")) fun selectDate() { - mDevice.findObject(UiSelector().resourceId("android:id/month_view")).waitForExists(waitingTime) - mDevice.findObject( UiSelector() .textContains("$currentDay") - .descriptionContains("$currentDay $currentMonth $currentYear"), + .descriptionContains("$currentMonth $currentDay"), ).click() } diff --git a/mobile/locales/l10n-changesets.json b/mobile/locales/l10n-changesets.json @@ -6,7 +6,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "an": { "pin": false, @@ -15,7 +15,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ar": { "pin": false, @@ -24,7 +24,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ast": { "pin": false, @@ -33,7 +33,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "az": { "pin": false, @@ -42,7 +42,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "be": { "pin": false, @@ -51,7 +51,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bg": { "pin": false, @@ -60,7 +60,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bn": { "pin": false, @@ -69,7 +69,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "br": { "pin": false, @@ -78,7 +78,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "bs": { "pin": false, @@ -87,7 +87,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ca": { "pin": false, @@ -96,7 +96,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "cak": { "pin": false, @@ -105,7 +105,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "cs": { "pin": false, @@ -114,7 +114,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "cy": { "pin": false, @@ -123,7 +123,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "da": { "pin": false, @@ -132,7 +132,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "de": { "pin": false, @@ -141,7 +141,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "dsb": { "pin": false, @@ -150,7 +150,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "el": { "pin": false, @@ -159,7 +159,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "en-CA": { "pin": false, @@ -168,7 +168,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "en-GB": { "pin": false, @@ -177,7 +177,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "eo": { "pin": false, @@ -186,7 +186,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-AR": { "pin": false, @@ -195,7 +195,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-CL": { "pin": false, @@ -204,7 +204,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-ES": { "pin": false, @@ -213,7 +213,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "es-MX": { "pin": false, @@ -222,7 +222,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "et": { "pin": false, @@ -231,7 +231,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "eu": { "pin": false, @@ -240,7 +240,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fa": { "pin": false, @@ -249,7 +249,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ff": { "pin": false, @@ -258,7 +258,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fi": { "pin": false, @@ -267,7 +267,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fr": { "pin": false, @@ -276,7 +276,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "fy-NL": { "pin": false, @@ -285,7 +285,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ga-IE": { "pin": false, @@ -294,7 +294,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gd": { "pin": false, @@ -303,7 +303,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gl": { "pin": false, @@ -312,7 +312,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gn": { "pin": false, @@ -321,7 +321,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "gu-IN": { "pin": false, @@ -330,7 +330,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "he": { "pin": false, @@ -339,7 +339,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hi-IN": { "pin": false, @@ -348,7 +348,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hr": { "pin": false, @@ -357,7 +357,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hsb": { "pin": false, @@ -366,7 +366,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hu": { "pin": false, @@ -375,7 +375,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "hy-AM": { "pin": false, @@ -384,7 +384,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ia": { "pin": false, @@ -393,7 +393,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "id": { "pin": false, @@ -402,7 +402,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "is": { "pin": false, @@ -411,7 +411,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "it": { "pin": false, @@ -420,7 +420,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ja": { "pin": false, @@ -429,7 +429,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ka": { "pin": false, @@ -438,7 +438,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "kab": { "pin": false, @@ -447,7 +447,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "kk": { "pin": false, @@ -456,7 +456,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "km": { "pin": false, @@ -465,7 +465,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "kn": { "pin": false, @@ -474,7 +474,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ko": { "pin": false, @@ -483,7 +483,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lij": { "pin": false, @@ -492,7 +492,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lo": { "pin": false, @@ -501,7 +501,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lt": { "pin": false, @@ -510,7 +510,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ltg": { "pin": false, @@ -519,7 +519,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "lv": { "pin": false, @@ -528,7 +528,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "meh": { "pin": false, @@ -537,7 +537,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "mix": { "pin": false, @@ -546,7 +546,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ml": { "pin": false, @@ -555,7 +555,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "mr": { "pin": false, @@ -564,7 +564,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ms": { "pin": false, @@ -573,7 +573,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "my": { "pin": false, @@ -582,7 +582,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "nb-NO": { "pin": false, @@ -591,7 +591,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ne-NP": { "pin": false, @@ -600,7 +600,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "nl": { "pin": false, @@ -609,7 +609,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "nn-NO": { "pin": false, @@ -618,7 +618,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "oc": { "pin": false, @@ -627,7 +627,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pa-IN": { "pin": false, @@ -636,7 +636,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pl": { "pin": false, @@ -645,7 +645,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pt-BR": { "pin": false, @@ -654,7 +654,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "pt-PT": { "pin": false, @@ -663,7 +663,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "rm": { "pin": false, @@ -672,7 +672,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ro": { "pin": false, @@ -681,7 +681,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ru": { "pin": false, @@ -690,7 +690,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sk": { "pin": false, @@ -699,7 +699,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sl": { "pin": false, @@ -708,7 +708,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "son": { "pin": false, @@ -717,7 +717,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sq": { "pin": false, @@ -726,7 +726,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sr": { "pin": false, @@ -735,7 +735,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "sv-SE": { "pin": false, @@ -744,7 +744,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ta": { "pin": false, @@ -753,7 +753,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "te": { "pin": false, @@ -762,7 +762,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "th": { "pin": false, @@ -771,7 +771,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "tl": { "pin": false, @@ -780,7 +780,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "tr": { "pin": false, @@ -789,7 +789,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "trs": { "pin": false, @@ -798,7 +798,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "uk": { "pin": false, @@ -807,7 +807,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "ur": { "pin": false, @@ -816,7 +816,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "uz": { "pin": false, @@ -825,7 +825,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "vi": { "pin": false, @@ -834,7 +834,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "wo": { "pin": false, @@ -843,7 +843,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "xh": { "pin": false, @@ -852,7 +852,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "zam": { "pin": false, @@ -861,7 +861,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "zh-CN": { "pin": false, @@ -870,7 +870,7 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" }, "zh-TW": { "pin": false, @@ -879,6 +879,6 @@ "android-arm", "android-multilocale" ], - "revision": "f2a1a7115a5614ee279b545fcbbdb8b33cd76d3e" + "revision": "e301addeabd26c741eb474a4a17ae29944494fa2" } } \ No newline at end of file diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml @@ -11105,6 +11105,12 @@ value: true mirror: always +# Whether to remove the accent property from the operator dictionary. +- name: mathml.operator_dictionary_accent.disabled + type: bool + value: false + mirror: always + # Whether to disable the MathML3 support for the mathvariant attribute. For # MathML Core, support is restricted to the <mi> element and to value "normal". # Corresponding automatic italicization on single-char <mi> element is also @@ -17113,6 +17119,13 @@ type: RelaxedAtomicBool value: false mirror: always + + # Whether \Device\KsecDD is closed in content process. We are closing this + # using a pref to give users a work-around if they have issues. +- name: security.sandbox.content.close-ksecdd-handle + type: RelaxedAtomicBool + value: true + mirror: always #endif #if defined(XP_LINUX) && defined(MOZ_SANDBOX) diff --git a/mozglue/baseprofiler/core/platform.cpp b/mozglue/baseprofiler/core/platform.cpp @@ -1618,10 +1618,9 @@ static void MaybeWriteRawStartTimeValue(SpliceableJSONWriter& aWriter, #endif #ifdef XP_WIN - Maybe<uint64_t> startTimeQPC = aStartTime.RawQueryPerformanceCounterValue(); - if (startTimeQPC) - aWriter.DoubleProperty("startTimeAsQueryPerformanceCounterValue", - static_cast<double>(*startTimeQPC)); + uint64_t startTimeQPC = aStartTime.RawQueryPerformanceCounterValue(); + aWriter.DoubleProperty("startTimeAsQueryPerformanceCounterValue", + static_cast<double>(startTimeQPC)); #endif } diff --git a/mozglue/misc/TimeStamp.h b/mozglue/misc/TimeStamp.h @@ -21,19 +21,9 @@ template <typename T> struct ParamTraits; } // namespace IPC -#ifdef XP_WIN -// defines TimeStampValue as a complex value keeping both -// GetTickCount and QueryPerformanceCounter values -# include "TimeStamp_windows.h" - -# include "mozilla/Maybe.h" // For TimeStamp::RawQueryPerformanceCounterValue -#endif - namespace mozilla { -#ifndef XP_WIN -typedef uint64_t TimeStampValue; -#endif +using TimeStampValue = uint64_t; class TimeStamp; class TimeStampTests; @@ -53,11 +43,7 @@ class BaseTimeDurationPlatformUtils { * Instances of this class represent the length of an interval of time. * Negative durations are allowed, meaning the end is before the start. * - * Internally the duration is stored as a int64_t in units of - * PR_TicksPerSecond() when building with NSPR interval timers, or a - * system-dependent unit when building with system clocks. The - * system-dependent unit must be constant, otherwise the semantics of - * this class would be broken. + * Internally the duration is stored as a system-dependent unit. * * The ValueCalculator template parameter determines how arithmetic * operations are performed on the integer count of ticks (mValue). @@ -355,11 +341,8 @@ typedef BaseTimeDuration<TimeDurationValueCalculator> TimeDuration; * to a TimeStamp to get a new TimeStamp. You can't do something * meaningless like add two TimeStamps. * - * Internally this is implemented as either a wrapper around - * - high-resolution, monotonic, system clocks if they exist on this - * platform - * - PRIntervalTime otherwise. We detect wraparounds of - * PRIntervalTime and work around them. + * Internally this is implemented as a wrapper around high-resolution, + * monotonic, platform-dependent system clocks. * * This class is similar to C++11's time_point, however it is * explicitly nullable and provides an IsNull() method. time_point @@ -371,20 +354,6 @@ typedef BaseTimeDuration<TimeDurationValueCalculator> TimeDuration; * Note that, since TimeStamp objects are small, prefer to pass them by value * unless there is a specific reason not to do so. */ -#if defined(XP_WIN) -// If this static_assert fails then possibly the warning comment below is no -// longer valid and should be removed. -static_assert(sizeof(TimeStampValue) > 8); -#endif -/* - * WARNING: On Windows, each TimeStamp is represented internally by two - * different raw values (one from GTC and one from QPC) and which value gets - * used for a given operation depends on whether both operands have QPC values - * or not. This duality of values can lead to some surprising results when - * mixing TimeStamps with and without QPC values, such as comparisons being - * non-transitive (ie, a > b > c might not imply a > c). See bug 1829983 for - * more details/an example. - */ class TimeStamp { public: using DurationType = TimeDuration; @@ -433,13 +402,21 @@ class TimeStamp { * * Now() is trying to ensure the best possible precision on each platform, * at least one millisecond. - * - * NowLoRes() has been introduced to workaround performance problems of - * QueryPerformanceCounter on the Windows platform. NowLoRes() is giving - * lower precision, usually 15.6 ms, but with very good performance benefit. - * Use it for measurements of longer times, like >200ms timeouts. */ static TimeStamp Now() { return Now(true); } + + /** + * Return a (coarse) timestamp reflecting the current elapsed system time. + * NowLoRes() behaves different depending on the OS: + * + * Windows: NowLoRes() == Now(), uses always QueryPerformanceCounter. + * MacOS: NowLoRes() == Now(), uses always mach_absolute_time. + * Posix: If the kernel supports CLOCK_MONOTONIC_COARSE use that, + * CLOCK_MONOTONIC otherwise. + * + * Used to promise better performance, which might still be true only for + * Posix. + */ static TimeStamp NowLoRes() { return Now(false); } /** @@ -481,10 +458,8 @@ class TimeStamp { #endif #ifdef XP_WIN - Maybe<uint64_t> RawQueryPerformanceCounterValue() const { - // mQPC is stored in `mt` i.e. QueryPerformanceCounter * 1000 - // so divide out the 1000 - return mValue.mHasQPC ? Some(mValue.mQPC / 1000ULL) : Nothing(); + uint64_t RawQueryPerformanceCounterValue() const { + return static_cast<uint64_t>(mValue); } #endif diff --git a/mozglue/misc/TimeStamp_darwin.cpp b/mozglue/misc/TimeStamp_darwin.cpp @@ -26,17 +26,15 @@ #include "mozilla/TimeStamp.h" #include "mozilla/Uptime.h" -// Estimate of the smallest duration of time we can measure. -static uint64_t sResolution; -static uint64_t sResolutionSigDigs; +// Each tick is significant, so we have a resolution of 1. +static constexpr uint64_t kResolution = 1; -static const uint64_t kNsPerMs = 1000000; static const uint64_t kUsPerSec = 1000000; static const double kNsPerMsd = 1000000.0; static const double kNsPerSecd = 1000000000.0; static bool gInitialized = false; -static double sNsPerTick; +static double sNsPerTickd; static uint64_t ClockTime() { // mach_absolute_time is it when it comes to ticks on the Mac. Other calls @@ -49,53 +47,25 @@ static uint64_t ClockTime() { return mach_absolute_time(); } -static uint64_t ClockResolutionNs() { - uint64_t start = ClockTime(); - uint64_t end = ClockTime(); - uint64_t minres = (end - start); - - // 10 total trials is arbitrary: what we're trying to avoid by - // looping is getting unlucky and being interrupted by a context - // switch or signal, or being bitten by paging/cache effects - for (int i = 0; i < 9; ++i) { - start = ClockTime(); - end = ClockTime(); - - uint64_t candidate = (start - end); - if (candidate < minres) { - minres = candidate; - } - } - - if (0 == minres) { - // measurable resolution is either incredibly low, ~1ns, or very - // high. fall back on NSPR's resolution assumption - minres = 1 * kNsPerMs; - } - - return minres; -} - namespace mozilla { double BaseTimeDurationPlatformUtils::ToSeconds(int64_t aTicks) { MOZ_ASSERT(gInitialized, "calling TimeDuration too early"); - return (aTicks * sNsPerTick) / kNsPerSecd; + return (aTicks * sNsPerTickd) / kNsPerSecd; } double BaseTimeDurationPlatformUtils::ToSecondsSigDigits(int64_t aTicks) { MOZ_ASSERT(gInitialized, "calling TimeDuration too early"); - // don't report a value < mResolution ... - int64_t valueSigDigs = sResolution * (aTicks / sResolution); - // and chop off insignificant digits - valueSigDigs = sResolutionSigDigs * (valueSigDigs / sResolutionSigDigs); - return (valueSigDigs * sNsPerTick) / kNsPerSecd; + // As we fix the resolution to 1, all digits are significant and there are + // no extra calculations needed. Ensure we do not change this inadvertedly. + static_assert(kResolution == 1); + return ToSeconds(aTicks); } int64_t BaseTimeDurationPlatformUtils::TicksFromMilliseconds( double aMilliseconds) { MOZ_ASSERT(gInitialized, "calling TimeDuration too early"); - double result = (aMilliseconds * kNsPerMsd) / sNsPerTick; + double result = (aMilliseconds * kNsPerMsd) / sNsPerTickd; // NOTE: this MUST be a >= test, because int64_t(double(INT64_MAX)) // overflows and gives INT64_MIN. if (result >= double(INT64_MAX)) { @@ -109,7 +79,7 @@ int64_t BaseTimeDurationPlatformUtils::TicksFromMilliseconds( int64_t BaseTimeDurationPlatformUtils::ResolutionInTicks() { MOZ_ASSERT(gInitialized, "calling TimeDuration too early"); - return static_cast<int64_t>(sResolution); + return static_cast<int64_t>(kResolution); } void TimeStamp::Startup() { @@ -126,15 +96,7 @@ void TimeStamp::Startup() { MOZ_RELEASE_ASSERT(false, "mach_timebase_info failed"); } - sNsPerTick = double(timebaseInfo.numer) / timebaseInfo.denom; - - sResolution = ClockResolutionNs(); - - // find the number of significant digits in sResolution, for the - // sake of ToSecondsSigDigits() - for (sResolutionSigDigs = 1; !(sResolutionSigDigs == sResolution || - 10 * sResolutionSigDigs > sResolution); - sResolutionSigDigs *= 10); + sNsPerTickd = double(timebaseInfo.numer) / timebaseInfo.denom; gInitialized = true; } @@ -146,7 +108,7 @@ TimeStamp TimeStamp::Now(bool aHighResolution) { } uint64_t TimeStamp::RawMachAbsoluteTimeNanoseconds() const { - return static_cast<uint64_t>(double(mValue) * sNsPerTick); + return static_cast<uint64_t>(double(mValue) * sNsPerTickd); } // Computes and returns the process uptime in microseconds. diff --git a/mozglue/misc/TimeStamp_windows.cpp b/mozglue/misc/TimeStamp_windows.cpp @@ -4,548 +4,111 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -// Implement TimeStamp::Now() with QueryPerformanceCounter() controlled with -// values of GetTickCount64(). - #include "mozilla/DynamicallyLinkedFunctionPtr.h" -#include "mozilla/MathAlgorithms.h" #include "mozilla/TimeStamp.h" -#include "mozilla/Uptime.h" - -#include <stdio.h> -#include <stdlib.h> #include <intrin.h> #include <windows.h> -// To enable logging define to your favorite logging API -#define LOG(x) - -class AutoCriticalSection { - public: - explicit AutoCriticalSection(LPCRITICAL_SECTION aSection) - : mSection(aSection) { - ::EnterCriticalSection(mSection); - } - ~AutoCriticalSection() { ::LeaveCriticalSection(mSection); } - - private: - LPCRITICAL_SECTION mSection; -}; - -// Estimate of the smallest duration of time we can measure. -static volatile ULONGLONG sResolution; -static volatile ULONGLONG sResolutionSigDigs; -static const double kNsPerSecd = 1000000000.0; -static const LONGLONG kNsPerMillisec = 1000000; - -// ---------------------------------------------------------------------------- -// Global constants -// ---------------------------------------------------------------------------- - -// Tolerance to failures settings. +// Historical note: We used to sample both QueryPerformanceCounter (QPC) and +// GetTickCount (GTC) timestamps in the past, as very early implementations of +// QPC were buggy. We had heuristics to determine if QPC is unreliable and +// would have switched to GTC in case, which could cause unexpected time +// travels between QPC and GPC values when that occured. // -// What is the interval we want to have failure free. -// in [ms] -static const uint32_t kFailureFreeInterval = 5000; -// How many failures we are willing to tolerate in the interval. -static const uint32_t kMaxFailuresPerInterval = 4; -// What is the threshold to treat fluctuations as actual failures. -// in [ms] -static const uint32_t kFailureThreshold = 50; - -// If we are not able to get the value of GTC time increment, use this value -// which is the most usual increment. -static const DWORD kDefaultTimeIncrement = 156001; +// Since Windows 8 together with the then modern CPUs, QPC became both reliable +// and almost as fast as GTC timestamps and provides a much higher resolution. +// QPC in general exists long enough even on older systems than Windows 8, such +// that we can just always rely on it, as we do in rust. // ---------------------------------------------------------------------------- // Global variables, not changing at runtime // ---------------------------------------------------------------------------- -// Result of QueryPerformanceFrequency -// We use default of 1 for the case we can't use QueryPerformanceCounter -// to make mt/ms conversions work despite that. -static uint64_t sFrequencyPerSec = 1; - -namespace mozilla { - -MFBT_API uint64_t GetQueryPerformanceFrequencyPerSec() { - return sFrequencyPerSec; -} - -} // namespace mozilla - -// How much we are tolerant to GTC occasional loose of resoltion. -// This number says how many multiples of the minimal GTC resolution -// detected on the system are acceptable. This number is empirical. -static const LONGLONG kGTCTickLeapTolerance = 4; - -// Base tolerance (more: "inability of detection" range) threshold is calculated -// dynamically, and kept in sGTCResolutionThreshold. -// -// Schematically, QPC worked "100%" correctly if ((GTC_now - GTC_epoch) - -// (QPC_now - QPC_epoch)) was in [-sGTCResolutionThreshold, -// sGTCResolutionThreshold] interval every time we'd compared two time stamps. -// If not, then we check the overflow behind this basic threshold -// is in kFailureThreshold. If not, we condider it as a QPC failure. If too -// many failures in short time are detected, QPC is considered faulty and -// disabled. -// -// Kept in [mt] -static LONGLONG sGTCResolutionThreshold; - -// If QPC is found faulty for two stamps in this interval, we engage -// the fault detection algorithm. For duration larger then this limit -// we bypass using durations calculated from QPC when jitter is detected, -// but don't touch the sUseQPC flag. -// -// Value is in [ms]. -static const uint32_t kHardFailureLimit = 2000; -// Conversion to [mt] -static LONGLONG sHardFailureLimit; - -// Conversion of kFailureFreeInterval and kFailureThreshold to [mt] -static LONGLONG sFailureFreeInterval; -static LONGLONG sFailureThreshold; - -// ---------------------------------------------------------------------------- -// Systemm status flags -// ---------------------------------------------------------------------------- - -// Flag for stable TSC that indicates platform where QPC is stable. -static bool sHasStableTSC = false; - -// ---------------------------------------------------------------------------- -// Global state variables, changing at runtime -// ---------------------------------------------------------------------------- - -// Initially true, set to false when QPC is found unstable and never -// returns back to true since that time. -static bool volatile sUseQPC = true; +// Result of QueryPerformanceFrequency, set only once on startup. +static double sTicksPerSecd; +static double sTicksPerMsd; // ---------------------------------------------------------------------------- -// Global lock +// Useful constants // ---------------------------------------------------------------------------- -// Thread spin count before entering the full wait state for sTimeStampLock. -// Inspired by Rob Arnold's work on PRMJ_Now(). -static const DWORD kLockSpinCount = 4096; - -// Common mutex (thanks the relative complexity of the logic, this is better -// then using CMPXCHG8B.) -// It is protecting the globals bellow. -static CRITICAL_SECTION sTimeStampLock; - -// ---------------------------------------------------------------------------- -// Global lock protected variables -// ---------------------------------------------------------------------------- - -// Timestamp in future until QPC must behave correctly. -// Set to now + kFailureFreeInterval on first QPC failure detection. -// Set to now + E * kFailureFreeInterval on following errors, -// where E is number of errors detected during last kFailureFreeInterval -// milliseconds, calculated simply as: -// E = (sFaultIntoleranceCheckpoint - now) / kFailureFreeInterval + 1. -// When E > kMaxFailuresPerInterval -> disable QPC. -// -// Kept in [mt] -static ULONGLONG sFaultIntoleranceCheckpoint = 0; +static constexpr double kMsPerSecd = 1000.0; +// Note: Resolution used to be sampled based on a loop of QPC calls. +// While it is true that on most systems we cannot expect to subsequently +// sample QPC values as fast as the QPC frequency, we still will get that +// as resolution of the sampled values, that is we have 1 tick resolution. +static constexpr LONGLONG kResolution = 1; namespace mozilla { -// Result is in [mt] +// Result is in ticks. static inline ULONGLONG PerformanceCounter() { LARGE_INTEGER pc; - ::QueryPerformanceCounter(&pc); - - // QueryPerformanceCounter may slightly jitter (not be 100% monotonic.) - // This is a simple go-backward protection for such a faulty hardware. - AutoCriticalSection lock(&sTimeStampLock); - - static decltype(LARGE_INTEGER::QuadPart) last; - if (last > pc.QuadPart) { - return last * 1000ULL; - } - last = pc.QuadPart; - return pc.QuadPart * 1000ULL; + MOZ_ALWAYS_TRUE(::QueryPerformanceCounter(&pc)); + return pc.QuadPart; } -static void InitThresholds() { - DWORD timeAdjustment = 0, timeIncrement = 0; - BOOL timeAdjustmentDisabled; - GetSystemTimeAdjustment(&timeAdjustment, &timeIncrement, - &timeAdjustmentDisabled); - - LOG(("TimeStamp: timeIncrement=%d [100ns]", timeIncrement)); - - if (!timeIncrement) { - timeIncrement = kDefaultTimeIncrement; - } - - // Ceiling to a millisecond - // Example values: 156001, 210000 - DWORD timeIncrementCeil = timeIncrement; - // Don't want to round up if already rounded, values will be: 156000, 209999 - timeIncrementCeil -= 1; - // Convert to ms, values will be: 15, 20 - timeIncrementCeil /= 10000; - // Round up, values will be: 16, 21 - timeIncrementCeil += 1; - // Convert back to 100ns, values will be: 160000, 210000 - timeIncrementCeil *= 10000; - - // How many milli-ticks has the interval rounded up - LONGLONG ticksPerGetTickCountResolutionCeiling = - (int64_t(timeIncrementCeil) * sFrequencyPerSec) / 10000LL; - - // GTC may jump by 32 (2*16) ms in two steps, therefor use the ceiling value. - sGTCResolutionThreshold = - LONGLONG(kGTCTickLeapTolerance * ticksPerGetTickCountResolutionCeiling); - - sHardFailureLimit = ms2mt(kHardFailureLimit); - sFailureFreeInterval = ms2mt(kFailureFreeInterval); - sFailureThreshold = ms2mt(kFailureThreshold); -} - -static void InitResolution() { - // 10 total trials is arbitrary: what we're trying to avoid by - // looping is getting unlucky and being interrupted by a context - // switch or signal, or being bitten by paging/cache effects - - ULONGLONG minres = ~0ULL; - if (sUseQPC) { - int loops = 10; - do { - ULONGLONG start = PerformanceCounter(); - ULONGLONG end = PerformanceCounter(); - - ULONGLONG candidate = (end - start); - if (candidate < minres) { - minres = candidate; - } - } while (--loops && minres); - - if (0 == minres) { - minres = 1; - } - } else { - // GetTickCount has only ~16ms known resolution - minres = ms2mt(16); - } - - // Converting minres that is in [mt] to nanosecods, multiplicating - // the argument to preserve resolution. - ULONGLONG result = mt2ms(minres * kNsPerMillisec); - if (0 == result) { - result = 1; - } - - sResolution = result; - - // find the number of significant digits in mResolution, for the - // sake of ToSecondsSigDigits() - ULONGLONG sigDigs; - for (sigDigs = 1; !(sigDigs == result || 10 * sigDigs > result); - sigDigs *= 10); - - sResolutionSigDigs = sigDigs; -} - -// ---------------------------------------------------------------------------- -// TimeStampValue implementation -// ---------------------------------------------------------------------------- -MFBT_API TimeStampValue& TimeStampValue::operator+=(const int64_t aOther) { - mGTC += aOther; - mQPC += aOther; - return *this; -} - -MFBT_API TimeStampValue& TimeStampValue::operator-=(const int64_t aOther) { - mGTC -= aOther; - mQPC -= aOther; - return *this; -} - -// If the duration is less then two seconds, perform check of QPC stability -// by comparing both GTC and QPC calculated durations of this and aOther. -MFBT_API uint64_t TimeStampValue::CheckQPC(const TimeStampValue& aOther) const { - uint64_t deltaGTC = mGTC - aOther.mGTC; - - if (!mHasQPC || !aOther.mHasQPC) { // Both not holding QPC - return deltaGTC; - } - - uint64_t deltaQPC = mQPC - aOther.mQPC; - - if (sHasStableTSC) { // For stable TSC there is no need to check - return deltaQPC; - } - - // Check QPC is sane before using it. - int64_t diff = DeprecatedAbs(int64_t(deltaQPC) - int64_t(deltaGTC)); - if (diff <= sGTCResolutionThreshold) { - return deltaQPC; - } - - // Treat absolutely for calibration purposes - int64_t duration = DeprecatedAbs(int64_t(deltaGTC)); - int64_t overflow = diff - sGTCResolutionThreshold; - - LOG(("TimeStamp: QPC check after %llums with overflow %1.4fms", - mt2ms(duration), mt2ms_f(overflow))); - - if (overflow <= sFailureThreshold) { // We are in the limit, let go. - return deltaQPC; - } - - // QPC deviates, don't use it, since now this method may only return deltaGTC. - - if (!sUseQPC) { // QPC already disabled, no need to run the fault tolerance - // algorithm. - return deltaGTC; - } - - LOG(("TimeStamp: QPC jittered over failure threshold")); - - if (duration < sHardFailureLimit) { - // Interval between the two time stamps is very short, consider - // QPC as unstable and record a failure. - uint64_t now = ms2mt(GetTickCount64()); - - AutoCriticalSection lock(&sTimeStampLock); - - if (sFaultIntoleranceCheckpoint && sFaultIntoleranceCheckpoint > now) { - // There's already been an error in the last fault intollerant interval. - // Time since now to the checkpoint actually holds information on how many - // failures there were in the failure free interval we have defined. - uint64_t failureCount = - (sFaultIntoleranceCheckpoint - now + sFailureFreeInterval - 1) / - sFailureFreeInterval; - if (failureCount > kMaxFailuresPerInterval) { - sUseQPC = false; - LOG(("TimeStamp: QPC disabled")); - } else { - // Move the fault intolerance checkpoint more to the future, prolong it - // to reflect the number of detected failures. - ++failureCount; - sFaultIntoleranceCheckpoint = now + failureCount * sFailureFreeInterval; - LOG(("TimeStamp: recording %dth QPC failure", failureCount)); - } - } else { - // Setup fault intolerance checkpoint in the future for first detected - // error. - sFaultIntoleranceCheckpoint = now + sFailureFreeInterval; - LOG(("TimeStamp: recording 1st QPC failure")); - } - } - - return deltaGTC; -} - -MFBT_API uint64_t -TimeStampValue::operator-(const TimeStampValue& aOther) const { - if (IsNull() && aOther.IsNull()) { - return uint64_t(0); - } - - return CheckQPC(aOther); +static void InitConstants() { + // Query the frequency from QPC and rely on it for all values. + LARGE_INTEGER freq; + bool hasQPC = ::QueryPerformanceFrequency(&freq); + MOZ_RELEASE_ASSERT(hasQPC); + sTicksPerSecd = double(freq.QuadPart); + sTicksPerMsd = sTicksPerSecd / kMsPerSecd; } -class TimeStampValueTests { - // Check that nullity is set/not set correctly. - static_assert(TimeStampValue{0}.IsNull()); - static_assert(!TimeStampValue{1}.IsNull()); - - // Check that we ignore GTC when both TimeStampValues have QPC. (In each of - // these tests, looking at GTC would give a different result.) - static_assert(TimeStampValue{1, 2, true} < TimeStampValue{1, 3, true}); - static_assert(!(TimeStampValue{1, 2, true} == TimeStampValue{1, 3, true})); - - static_assert(TimeStampValue{2, 2, true} < TimeStampValue{1, 3, true}); - static_assert(TimeStampValue{2, 2, true} <= TimeStampValue{1, 3, true}); - static_assert(!(TimeStampValue{2, 2, true} > TimeStampValue{1, 3, true})); - - static_assert(TimeStampValue{1, 3, true} > TimeStampValue{1, 2, true}); - static_assert(!(TimeStampValue{1, 3, true} == TimeStampValue{1, 2, true})); - - static_assert(TimeStampValue{1, 3, true} > TimeStampValue{2, 2, true}); - static_assert(TimeStampValue{1, 3, true} >= TimeStampValue{2, 2, true}); - static_assert(!(TimeStampValue{1, 3, true} < TimeStampValue{2, 2, true})); - - static_assert(TimeStampValue{1, 3, true} == TimeStampValue{2, 3, true}); - static_assert(!(TimeStampValue{1, 3, true} < TimeStampValue{2, 3, true})); - - static_assert(TimeStampValue{1, 2, true} != TimeStampValue{1, 3, true}); - static_assert(!(TimeStampValue{1, 2, true} == TimeStampValue{1, 3, true})); - - // Check that, if either TimeStampValue doesn't have QPC, we only look at the - // GTC values. These are the same cases as above, except that we accept the - // opposite results because we turn off QPC on one or both of the - // TimeStampValue's. - static_assert(TimeStampValue{1, 2, false} == TimeStampValue{1, 3, true}); - static_assert(TimeStampValue{1, 2, true} == TimeStampValue{1, 3, false}); - static_assert(TimeStampValue{1, 2, false} == TimeStampValue{1, 3, false}); - - static_assert(TimeStampValue{2, 2, false} > TimeStampValue{1, 3, true}); - static_assert(TimeStampValue{2, 2, true} > TimeStampValue{1, 3, false}); - static_assert(TimeStampValue{2, 2, false} > TimeStampValue{1, 3, false}); - - static_assert(TimeStampValue{1, 3, false} == TimeStampValue{1, 2, true}); - static_assert(TimeStampValue{1, 3, true} == TimeStampValue{1, 2, false}); - static_assert(TimeStampValue{1, 3, false} == TimeStampValue{1, 2, false}); - - static_assert(TimeStampValue{1, 3, false} < TimeStampValue{2, 2, true}); - static_assert(TimeStampValue{1, 3, true} < TimeStampValue{2, 2, false}); - static_assert(TimeStampValue{1, 3, false} < TimeStampValue{2, 2, false}); - - static_assert(TimeStampValue{1, 3, false} < TimeStampValue{2, 3, true}); - static_assert(TimeStampValue{1, 3, true} < TimeStampValue{2, 3, false}); - static_assert(TimeStampValue{1, 3, false} < TimeStampValue{2, 3, false}); - - static_assert(TimeStampValue{1, 2, false} == TimeStampValue{1, 3, true}); - static_assert(TimeStampValue{1, 2, true} == TimeStampValue{1, 3, false}); - static_assert(TimeStampValue{1, 2, false} == TimeStampValue{1, 3, false}); -}; - // ---------------------------------------------------------------------------- // TimeDuration and TimeStamp implementation // ---------------------------------------------------------------------------- MFBT_API double BaseTimeDurationPlatformUtils::ToSeconds(int64_t aTicks) { - // Converting before arithmetic avoids blocked store forward - return double(aTicks) / (double(sFrequencyPerSec) * 1000.0); + return double(aTicks) / sTicksPerSecd; } MFBT_API double BaseTimeDurationPlatformUtils::ToSecondsSigDigits( int64_t aTicks) { - // don't report a value < mResolution ... - LONGLONG resolution = sResolution; - LONGLONG resolutionSigDigs = sResolutionSigDigs; - LONGLONG valueSigDigs = resolution * (aTicks / resolution); - // and chop off insignificant digits - valueSigDigs = resolutionSigDigs * (valueSigDigs / resolutionSigDigs); - return double(valueSigDigs) / kNsPerSecd; + // As we fix the resolution to 1, all digits are significant and there are + // no extra calculations needed. Ensure we do not change this inadvertedly. + static_assert(kResolution == 1); + return ToSeconds(aTicks); } MFBT_API int64_t BaseTimeDurationPlatformUtils::TicksFromMilliseconds(double aMilliseconds) { - double result = ms2mt(aMilliseconds); + double result = sTicksPerMsd * aMilliseconds; // NOTE: this MUST be a >= test, because int64_t(double(INT64_MAX)) // overflows and gives INT64_MIN. if (result >= double(INT64_MAX)) { return INT64_MAX; - } else if (result <= double(INT64_MIN)) { + } + if (result <= double(INT64_MIN)) { return INT64_MIN; } - return result; + return (int64_t)result; } MFBT_API int64_t BaseTimeDurationPlatformUtils::ResolutionInTicks() { - return static_cast<int64_t>(sResolution); -} - -static bool HasStableTSC() { -#if defined(_M_ARM64) - // AArch64 defines that its system counter run at a constant rate - // regardless of the current clock frequency of the system. See "The - // Generic Timer", section D7, in the ARMARM for ARMv8. - return true; -#else - union { - int regs[4]; - struct { - int nIds; - char cpuString[12]; - }; - } cpuInfo; - - __cpuid(cpuInfo.regs, 0); - // Only allow Intel or AMD CPUs for now. - // The order of the registers is reg[1], reg[3], reg[2]. We just adjust the - // string so that we can compare in one go. - if (_strnicmp(cpuInfo.cpuString, "GenuntelineI", sizeof(cpuInfo.cpuString)) && - _strnicmp(cpuInfo.cpuString, "AuthcAMDenti", sizeof(cpuInfo.cpuString))) { - return false; - } - - int regs[4]; - - // detect if the Advanced Power Management feature is supported - __cpuid(regs, 0x80000000); - if ((unsigned int)regs[0] < 0x80000007) { - // XXX should we return true here? If there is no APM there may be - // no way how TSC can run out of sync among cores. - return false; - } - - __cpuid(regs, 0x80000007); - // if bit 8 is set than TSC will run at a constant rate - // in all ACPI P-states, C-states and T-states - return regs[3] & (1 << 8); -#endif + return static_cast<int64_t>(kResolution); } +// Note that we init early enough during startup such that we are supposed to +// not yet have started other threads which could try to use us. static bool gInitialized = false; MFBT_API void TimeStamp::Startup() { if (gInitialized) { return; } - + InitConstants(); gInitialized = true; - - // Decide which implementation to use for the high-performance timer. - - InitializeCriticalSectionAndSpinCount(&sTimeStampLock, kLockSpinCount); - - bool forceGTC = false; - bool forceQPC = false; - - char* modevar = getenv("MOZ_TIMESTAMP_MODE"); - if (modevar) { - if (!strcmp(modevar, "QPC")) { - forceQPC = true; - } else if (!strcmp(modevar, "GTC")) { - forceGTC = true; - } - } - - LARGE_INTEGER freq; - sUseQPC = !forceGTC && ::QueryPerformanceFrequency(&freq); - if (!sUseQPC) { - // No Performance Counter. Fall back to use GetTickCount64. - InitResolution(); - - LOG(("TimeStamp: using GetTickCount64")); - return; - } - - sHasStableTSC = forceQPC || HasStableTSC(); - LOG(("TimeStamp: HasStableTSC=%d", sHasStableTSC)); - - sFrequencyPerSec = freq.QuadPart; - LOG(("TimeStamp: QPC frequency=%llu", sFrequencyPerSec)); - - InitThresholds(); - InitResolution(); - - return; } -MFBT_API void TimeStamp::Shutdown() { DeleteCriticalSection(&sTimeStampLock); } - -TimeStampValue NowInternal(bool aHighResolution) { - // sUseQPC is volatile - bool useQPC = (aHighResolution && sUseQPC); - - // Both values are in [mt] units. - ULONGLONG QPC = useQPC ? PerformanceCounter() : uint64_t(0); - ULONGLONG GTC = ms2mt(GetTickCount64()); - return TimeStampValue(GTC, QPC, useQPC); -} +MFBT_API void TimeStamp::Shutdown() {} MFBT_API TimeStamp TimeStamp::Now(bool aHighResolution) { - return TimeStamp(NowInternal(aHighResolution)); + MOZ_ASSERT(gInitialized); + return TimeStamp((TimeStampValue)PerformanceCounter()); } // Computes and returns the process uptime in microseconds. diff --git a/mozglue/misc/TimeStamp_windows.h b/mozglue/misc/TimeStamp_windows.h @@ -1,113 +0,0 @@ -/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ -/* vim: set ts=8 sts=2 et sw=2 tw=80: */ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -#ifndef mozilla_TimeStamp_windows_h -#define mozilla_TimeStamp_windows_h - -#include "mozilla/Types.h" - -namespace mozilla { - -/** - * The [mt] unit: - * - * Many values are kept in ticks of the Performance Counter x 1000, - * further just referred as [mt], meaning milli-ticks. - * - * This is needed to preserve maximum precision of the performance frequency - * representation. GetTickCount64 values in milliseconds are multiplied with - * frequency per second. Therefore we need to multiply QPC value by 1000 to - * have the same units to allow simple arithmentic with both QPC and GTC. - */ -#define ms2mt(x) ((x) * mozilla::GetQueryPerformanceFrequencyPerSec()) -#define mt2ms(x) ((x) / mozilla::GetQueryPerformanceFrequencyPerSec()) -#define mt2ms_f(x) (double(x) / mozilla::GetQueryPerformanceFrequencyPerSec()) - -MFBT_API uint64_t GetQueryPerformanceFrequencyPerSec(); - -class TimeStamp; -class TimeStampValue; -class TimeStampValueTests; -class TimeStampTests; - -TimeStampValue NowInternal(bool aHighResolution); - -class TimeStampValue { - friend TimeStampValue NowInternal(bool); - friend bool IsCanonicalTimeStamp(TimeStampValue); - friend struct IPC::ParamTraits<mozilla::TimeStampValue>; - friend class TimeStamp; - friend class TimeStampValueTests; - friend class TimeStampTests; - - // Both QPC and GTC are kept in [mt] units. - uint64_t mGTC; - uint64_t mQPC; - - bool mIsNull; - bool mHasQPC; - - constexpr MFBT_API TimeStampValue(uint64_t aGTC, uint64_t aQPC, bool aHasQPC) - : mGTC(aGTC), - mQPC(aQPC), - mIsNull(aGTC == 0 && aQPC == 0), - mHasQPC(aHasQPC) {} - - // This constructor should be explicit but it is replacing a constructor that - // was MOZ_IMPLICIT and there are many locations that are using the automatic - // conversion. - constexpr MOZ_IMPLICIT MFBT_API TimeStampValue(uint64_t aGTCAndQPC) - : TimeStampValue(aGTCAndQPC, aGTCAndQPC, true) {} - - MFBT_API uint64_t CheckQPC(const TimeStampValue& aOther) const; - - public: - MFBT_API uint64_t operator-(const TimeStampValue& aOther) const; - - TimeStampValue operator+(const int64_t aOther) const { - return TimeStampValue(mGTC + aOther, mQPC + aOther, mHasQPC); - } - TimeStampValue operator-(const int64_t aOther) const { - return TimeStampValue(mGTC - aOther, mQPC - aOther, mHasQPC); - } - MFBT_API TimeStampValue& operator+=(const int64_t aOther); - MFBT_API TimeStampValue& operator-=(const int64_t aOther); - - constexpr bool operator<(const TimeStampValue& aOther) const { - return mHasQPC && aOther.mHasQPC ? mQPC < aOther.mQPC : mGTC < aOther.mGTC; - } - constexpr bool operator>(const TimeStampValue& aOther) const { - return mHasQPC && aOther.mHasQPC ? mQPC > aOther.mQPC : mGTC > aOther.mGTC; - } - constexpr bool operator<=(const TimeStampValue& aOther) const { - return mHasQPC && aOther.mHasQPC ? mQPC <= aOther.mQPC - : mGTC <= aOther.mGTC; - } - constexpr bool operator>=(const TimeStampValue& aOther) const { - return mHasQPC && aOther.mHasQPC ? mQPC >= aOther.mQPC - : mGTC >= aOther.mGTC; - } - constexpr bool operator==(const TimeStampValue& aOther) const { - return mHasQPC && aOther.mHasQPC ? mQPC == aOther.mQPC - : mGTC == aOther.mGTC; - } - constexpr bool operator!=(const TimeStampValue& aOther) const { - return mHasQPC && aOther.mHasQPC ? mQPC != aOther.mQPC - : mGTC != aOther.mGTC; - } - constexpr bool IsNull() const { return mIsNull; } - -#if defined(DEBUG) - uint64_t GTC() const { return mGTC; } - uint64_t QPC() const { return mQPC; } - - bool HasQPC() const { return mHasQPC; } -#endif -}; - -} // namespace mozilla - -#endif /* mozilla_TimeStamp_h */ diff --git a/mozglue/misc/moz.build b/mozglue/misc/moz.build @@ -38,7 +38,6 @@ if CONFIG["OS_ARCH"] == "WINNT": "PreXULSkeletonUI.h", "StackWalk_windows.h", "StackWalkThread.h", - "TimeStamp_windows.h", "WindowsDpiAwareness.h", ] diff --git a/netwerk/base/Predictor.cpp b/netwerk/base/Predictor.cpp @@ -540,15 +540,6 @@ Predictor::PredictNative(nsIURI* targetURI, nsIURI* sourceURI, nsCOMPtr<nsIURI> uriKey = targetURI; nsCOMPtr<nsIURI> originKey; switch (reason) { - case nsINetworkPredictor::PREDICT_LINK: - if (!targetURI || !sourceURI) { - PREDICTOR_LOG((" link invalid URI state")); - return NS_ERROR_INVALID_ARG; - } - // Link hover is a special case where we can predict without hitting the - // db, so let's go ahead and fire off that prediction here. - PredictForLink(targetURI, sourceURI, originAttributes, verifier); - return NS_OK; case nsINetworkPredictor::PREDICT_LOAD: if (!targetURI || sourceURI) { PREDICTOR_LOG((" load invalid URI state")); @@ -661,35 +652,6 @@ bool Predictor::PredictInternal(PredictorPredictReason reason, return rv; } -void Predictor::PredictForLink(nsIURI* targetURI, nsIURI* sourceURI, - const OriginAttributes& originAttributes, - nsINetworkPredictorVerifier* verifier) { - MOZ_ASSERT(NS_IsMainThread()); - - PREDICTOR_LOG(("Predictor::PredictForLink")); - if (!mSpeculativeService) { - PREDICTOR_LOG((" missing speculative service")); - return; - } - - if (!StaticPrefs::network_predictor_enable_hover_on_ssl()) { - if (sourceURI->SchemeIs("https")) { - // We don't want to predict from an HTTPS page, to avoid info leakage - PREDICTOR_LOG((" Not predicting for link hover - on an SSL page")); - return; - } - } - - nsCOMPtr<nsIPrincipal> principal = - BasePrincipal::CreateContentPrincipal(targetURI, originAttributes); - - mSpeculativeService->SpeculativeConnect(targetURI, principal, nullptr, false); - if (verifier) { - PREDICTOR_LOG((" sending verification")); - verifier->OnPredictPreconnect(targetURI); - } -} - // This is the driver for prediction based on a new pageload. static const uint8_t MAX_PAGELOAD_DEPTH = 10; bool Predictor::PredictForPageload(nsICacheEntry* entry, nsIURI* targetURI, diff --git a/netwerk/base/Predictor.h b/netwerk/base/Predictor.h @@ -243,15 +243,6 @@ class Predictor final : public nsINetworkPredictor, nsINetworkPredictorVerifier* verifier, uint8_t stackCount); - // Used when predicting because the user's mouse hovered over a link - // * targetURI - the URI target of the link - // * sourceURI - the URI of the page on which the link appears - // * originAttributes - the originAttributes for this prediction - // * verifier - used for testing to verify the expected predictions happen - void PredictForLink(nsIURI* targetURI, nsIURI* sourceURI, - const OriginAttributes& originAttributes, - nsINetworkPredictorVerifier* verifier); - // Used when predicting because a page is being loaded (which may include // being the target of a redirect). All arguments are the same as for // PredictInternal. Returns true if any predictions were queued up. diff --git a/netwerk/base/nsINetworkPredictor.idl b/netwerk/base/nsINetworkPredictor.idl @@ -35,16 +35,12 @@ interface nsINetworkPredictor : nsISupports /** * Prediction reasons * - * PREDICT_LINK - we are being asked to take predictive action because - * the user is hovering over a link. - * * PREDICT_LOAD - we are being asked to take predictive action because * the user has initiated a pageload. * * PREDICT_STARTUP - we are being asked to take predictive action * because the browser is starting up. */ - const PredictorPredictReason PREDICT_LINK = 0; const PredictorPredictReason PREDICT_LOAD = 1; const PredictorPredictReason PREDICT_STARTUP = 2; @@ -62,9 +58,6 @@ interface nsINetworkPredictor : nsISupports * example). * @param reason - The reason we are being asked to take actions. Can be * any of the PREDICT_* values above. - * In the case of PREDICT_LINK, targetURI should be the URI of the link - * that is being hovered over, and sourceURI should be the URI of the page - * on which the link appears. * In the case of PREDICT_LOAD, targetURI should be the URI of the page that * is being loaded and sourceURI should be null. * In the case of PREDICT_STARTUP, both targetURI and sourceURI should be diff --git a/netwerk/protocol/http/HttpBaseChannel.cpp b/netwerk/protocol/http/HttpBaseChannel.cpp @@ -394,10 +394,18 @@ nsresult HttpBaseChannel::Init(nsIURI* aURI, uint32_t aCaps, } } + RefPtr<mozilla::dom::BrowsingContext> browsingContext; + mLoadInfo->GetBrowsingContext(getter_AddRefs(browsingContext)); + + const nsCString& languageOverride = + browsingContext ? browsingContext->Top()->GetLanguageOverride() + : EmptyCString(); + rv = gHttpHandler->AddStandardRequestHeaders( &mRequestHead, aURI, isHTTPS, contentPolicyType, nsContentUtils::ShouldResistFingerprinting(this, - RFPTarget::HttpUserAgent)); + RFPTarget::HttpUserAgent), + languageOverride); if (NS_FAILED(rv)) return rv; nsAutoCString type; diff --git a/netwerk/protocol/http/nsHttpHandler.cpp b/netwerk/protocol/http/nsHttpHandler.cpp @@ -733,7 +733,8 @@ nsresult nsHttpHandler::AddAcceptAndDictionaryHeaders( nsresult nsHttpHandler::AddStandardRequestHeaders( nsHttpRequestHead* request, nsIURI* aURI, bool aIsHTTPS, - ExtContentPolicyType aContentPolicyType, bool aShouldResistFingerprinting) { + ExtContentPolicyType aContentPolicyType, bool aShouldResistFingerprinting, + const nsCString& aLanguageOverride) { nsresult rv; // Add the "User-Agent" header @@ -765,18 +766,26 @@ nsresult nsHttpHandler::AddStandardRequestHeaders( nsHttpHeaderArray::eVarietyRequestOverride); if (NS_FAILED(rv)) return rv; - // Add the "Accept-Language" header. This header is also exposed to the - // service worker. - if (mAcceptLanguagesIsDirty) { - rv = SetAcceptLanguages(); - MOZ_ASSERT(NS_SUCCEEDED(rv)); - } - - // Add the "Accept-Language" header - if (!mAcceptLanguages.IsEmpty()) { - rv = request->SetHeader(nsHttp::Accept_Language, mAcceptLanguages, false, + if (!aLanguageOverride.IsEmpty()) { + nsAutoCString acceptLanguage; + acceptLanguage.Assign(aLanguageOverride.get()); + rv = request->SetHeader(nsHttp::Accept_Language, acceptLanguage, false, nsHttpHeaderArray::eVarietyRequestOverride); if (NS_FAILED(rv)) return rv; + } else { + // Add the "Accept-Language" header. This header is also exposed to the + // service worker. + if (mAcceptLanguagesIsDirty) { + rv = SetAcceptLanguages(); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + } + + // Add the "Accept-Language" header + if (!mAcceptLanguages.IsEmpty()) { + rv = request->SetHeader(nsHttp::Accept_Language, mAcceptLanguages, false, + nsHttpHeaderArray::eVarietyRequestOverride); + if (NS_FAILED(rv)) return rv; + } } // add the "Send Hint" header diff --git a/netwerk/protocol/http/nsHttpHandler.h b/netwerk/protocol/http/nsHttpHandler.h @@ -123,8 +123,8 @@ class nsHttpHandler final : public nsIHttpProtocolHandler, const std::function<bool(bool, DictionaryCacheEntry*)>& aCallback); [[nodiscard]] nsresult AddStandardRequestHeaders( nsHttpRequestHead*, nsIURI* aURI, bool aIsHTTPS, - ExtContentPolicyType aContentPolicyType, - bool aShouldResistFingerprinting); + ExtContentPolicyType aContentPolicyType, bool aShouldResistFingerprinting, + const nsCString& aLanguageOverride); [[nodiscard]] nsresult AddConnectionHeader(nsHttpRequestHead*, uint32_t caps); bool IsAcceptableEncoding(const char* encoding, bool isSecure); diff --git a/netwerk/test/browser/browser.toml b/netwerk/test/browser/browser.toml @@ -13,6 +13,7 @@ support-files = [ "early_hint_asset_html.sjs", "early_hint_csp_options_html.sjs", "early_hint_preconnect_html.sjs", + "request_accept_language.sjs", "file_channel.html", "post.html", "res.css", @@ -150,6 +151,8 @@ support-files = ["early_hint_preload_test_helper.sys.mjs",] ["browser_about_cache.js"] skip-if = ["os == 'linux' && os_version == '24.04' && processor == 'x86_64' && display == 'x11'"] # Bug 1970244 +["browser_accept_language_override.js"] + ["browser_backgroundtask_purgeHTTPCache.js"] ["browser_bug968273.js"] @@ -191,6 +194,9 @@ support-files = ["file_lnk.lnk",] ["browser_ipAddressSpace_mainpage_unaffected.js"] +["browser_link_hover_speculative_connection.js"] +support-files = ["file_link_hover.sjs"] + ["browser_mock_https_rr.js"] skip-if = [ "http3", diff --git a/netwerk/test/browser/browser_accept_language_override.js b/netwerk/test/browser/browser_accept_language_override.js @@ -0,0 +1,152 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +"use strict"; + +const PAGE_URL = "https://example.com/browser/netwerk/test/browser/dummy.html"; +const SCRIPT_URL = + "https://example.com/browser/netwerk/test/browser/request_accept_language.sjs"; +const languages = ["en-US", "es-ES", "fr-FR"]; + +add_task(async function test_set_language_override() { + const tab = BrowserTestUtils.addTab(gBrowser, PAGE_URL); + const browser = gBrowser.getBrowserForTab(tab); + + await BrowserTestUtils.browserLoaded(browser); + + info("Get default language"); + const defaultLanguage = await getAcceptLanguageHeader(browser); + const browsingContext = browser.browsingContext; + const languageOverride = getLanguageToOverride(defaultLanguage); + + info("Set language override"); + browser.browsingContext.languageOverride = languageOverride; + is( + await getAcceptLanguageHeader(browser), + languageOverride, + '"Accept-Language" header is overridden' + ); + + const secondLanguageOverride = getSecondLanguageToOverride( + defaultLanguage, + languageOverride + ); + + info("Set language override again"); + browsingContext.languageOverride = secondLanguageOverride; + is( + await getAcceptLanguageHeader(browser), + secondLanguageOverride, + '"Accept-Language" header is overridden' + ); + + info("Reset language override"); + browsingContext.languageOverride = ""; + is( + await getAcceptLanguageHeader(browser), + defaultLanguage, + '"Accept-Language" header is not overridden' + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_set_language_override_and_navigate() { + const tab = BrowserTestUtils.addTab(gBrowser, PAGE_URL); + const browser = gBrowser.getBrowserForTab(tab); + + await BrowserTestUtils.browserLoaded(browser); + + info("Get default language"); + const defaultLanguage = await getAcceptLanguageHeader(browser); + const browsingContext = browser.browsingContext; + const languageOverride = getLanguageToOverride(defaultLanguage); + + info("Set language override"); + browsingContext.languageOverride = languageOverride; + is( + await getAcceptLanguageHeader(browser), + languageOverride, + '"Accept-Language" header is overridden' + ); + + info("Navigate browsing context"); + const url = "https://example.com/chrome/dom/base/test/dummy.html"; + const loaded = BrowserTestUtils.browserLoaded(browser, false, url, false); + BrowserTestUtils.startLoadingURIString(browser, url); + await loaded; + is( + await getAcceptLanguageHeader(browser), + languageOverride, + '"Accept-Language" header is overridden' + ); + + BrowserTestUtils.removeTab(tab); +}); + +add_task(async function test_set_language_override_in_different_contexts() { + const tab1 = BrowserTestUtils.addTab(gBrowser, PAGE_URL); + const browser1 = gBrowser.getBrowserForTab(tab1); + + await BrowserTestUtils.browserLoaded(browser1); + + const tab2 = BrowserTestUtils.addTab(gBrowser, PAGE_URL); + const browser2 = gBrowser.getBrowserForTab(tab2); + + await BrowserTestUtils.browserLoaded(browser2); + + info("Get default language in the first tab"); + const defaultLanguage1 = await getAcceptLanguageHeader(browser1); + + info("Get default language in the second tab"); + const defaultLanguage2 = await getAcceptLanguageHeader(browser2); + + const browsingContext1 = browser1.browsingContext; + const languageOverride = getLanguageToOverride(defaultLanguage1); + + info("Set language override to the first tab"); + browsingContext1.languageOverride = languageOverride; + is( + await getAcceptLanguageHeader(browser1), + languageOverride, + '"Accept-Language" header is overridden' + ); + + info("Make sure that in the second tab language is not overridden"); + browsingContext1.languageOverride = languageOverride; + is( + await getAcceptLanguageHeader(browser2), + defaultLanguage2, + '"Accept-Language" header is not overridden' + ); + + info("Reset language override"); + browsingContext1.languageOverride = ""; + is( + await getAcceptLanguageHeader(browser1), + defaultLanguage1, + '"Accept-Language" header is not overridden' + ); + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +function getLanguageToOverride(defaultLanguage) { + return languages.find(lang => lang !== defaultLanguage); +} + +function getSecondLanguageToOverride(defaultLanguage, secondLanguage) { + return languages.find( + lang => lang !== defaultLanguage && lang !== secondLanguage + ); +} + +async function getAcceptLanguageHeader(browser) { + return SpecialPowers.spawn(browser, [SCRIPT_URL], async url => { + const response = await content.fetch(url); + return response.json(); + }); +} diff --git a/netwerk/test/browser/browser_link_hover_speculative_connection.js b/netwerk/test/browser/browser_link_hover_speculative_connection.js @@ -0,0 +1,362 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { HttpServer } = ChromeUtils.importESModule( + "resource://testing-common/httpd.sys.mjs" +); + +const TEST_PATH = "/browser/netwerk/test/browser/"; + +let gServer; +let gServerURL; +let gConnectionNumberWhenRequested = 0; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + // Enable speculative connections for hover on HTTPS + ["network.predictor.enable-hover-on-ssl", true], + // Enable network debugging observations + ["network.http.debug-observations", true], + ], + }); + + // Set up local HTTP server for the target page + gServer = new HttpServer(); + gServer.start(-1); + gServerURL = `http://localhost:${gServer.identity.primaryPort}`; + + // Register handler for the target page + gServer.registerPathHandler("/target.html", handleTarget); + + registerCleanupFunction(async () => { + await gServer.stop(); + }); +}); + +function handleTarget(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + // Record which connection number handled this request + gConnectionNumberWhenRequested = response._connection.number; + + let body = ` + <!DOCTYPE html> + <html> + <head> + <meta charset="UTF-8"> + <title>Target Page</title> + </head> + <body> + <h1>Target Page</h1> + <p>Connection: ${gConnectionNumberWhenRequested}</p> + </body> + </html> + `; + + response.write(body); +} + +/** + * Helper function to simulate hovering over a link element + */ +function hoverOverLink(browser, linkId) { + return SpecialPowers.spawn(browser, [linkId], async id => { + let link = content.document.getElementById(id); + // Dispatch mouseover event which should trigger the speculative connection + let event = new content.MouseEvent("mouseover", { + bubbles: true, + cancelable: true, + view: content, + }); + link.dispatchEvent(event); + }); +} + +/** + * Helper function to click a link and wait for navigation + */ +async function clickLink(browser, linkId) { + let loadedPromise = BrowserTestUtils.browserLoaded(browser); + + await SpecialPowers.spawn(browser, [linkId], async id => { + let link = content.document.getElementById(id); + link.click(); + }); + + await loadedPromise; +} + +add_task(async function test_link_hover_https_page() { + let speculativeConnectObserved = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if ( + aTopic == "speculative-connect-request" && + aData.includes("localhost") + ) { + info("Observed speculative connection request for: " + aData); + speculativeConnectObserved = true; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + // Load the test page from HTTPS example.com with the target URL pointing to our local server + const targetURL = encodeURIComponent(gServerURL + "/target.html"); + const pageURL = + "https://example.com" + + TEST_PATH + + "file_link_hover.sjs?target=" + + targetURL; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageURL, + waitForLoad: true, + }, + async function (browser) { + // Record the current connection count before hovering + let connectionCountBeforeHover = gServer.connectionNumber; + info("Connection count before hover: " + connectionCountBeforeHover); + + // Wait for the link element to be available in the page + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("testLink"), + "Waiting for link element to be available" + ); + }); + + // Hover over the link to trigger speculative connection + await hoverOverLink(browser, "testLink"); + + // Wait for the speculative connection to be fully established + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check connection count after hover + let connectionCountAfterHover = gServer.connectionNumber; + info("Connection count after hover: " + connectionCountAfterHover); + + // Verify that a speculative connection request was observed + Assert.ok( + speculativeConnectObserved, + "Speculative connection should be triggered on link hover" + ); + + // Now click the link - it should use the speculative connection + await clickLink(browser, "testLink"); + + // Check connection count after click + let connectionCountAfterClick = gServer.connectionNumber; + info("Connection count after click: " + connectionCountAfterClick); + info( + "Connection number that handled the request: " + + gConnectionNumberWhenRequested + ); + + // Verify that exactly one NEW connection was established + Assert.equal( + connectionCountAfterClick, + connectionCountBeforeHover + 1, + "Exactly one connection should be established for the HTTP request" + ); + + // Verify that the request was handled by the new connection + Assert.equal( + gConnectionNumberWhenRequested, + connectionCountBeforeHover + 1, + "The HTTP request should be handled by the new connection" + ); + } + ); + + Services.obs.removeObserver(observer, "speculative-connect-request"); +}); + +add_task(async function test_link_hover_http_page() { + let speculativeConnectObserved = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if ( + aTopic == "speculative-connect-request" && + aData.includes("localhost") + ) { + info("Observed speculative connection request for: " + aData); + speculativeConnectObserved = true; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + // Load the test page from HTTP example.com with the target URL pointing to our local server + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + const targetURL = encodeURIComponent(gServerURL + "/target.html"); + const pageURL = + // eslint-disable-next-line @microsoft/sdl/no-insecure-url + "http://example.com" + + TEST_PATH + + "file_link_hover.sjs?target=" + + targetURL; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageURL, + waitForLoad: true, + }, + async function (browser) { + // Record the current connection count before hovering + // This should be 0 since we haven't connected to our server yet + let connectionCountBeforeHover = gServer.connectionNumber; + info("Connection count before hover: " + connectionCountBeforeHover); + + // Wait for the link element to be available in the page + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("testLink"), + "Waiting for link element to be available" + ); + }); + + // Hover over the link to trigger speculative connection + await hoverOverLink(browser, "testLink"); + + // Wait for the speculative connection to be fully established + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check connection count after hover + let connectionCountAfterHover = gServer.connectionNumber; + info("Connection count after hover: " + connectionCountAfterHover); + + // Verify that a speculative connection request was observed + Assert.ok( + speculativeConnectObserved, + "Speculative connection should be triggered on link hover from HTTP page" + ); + + // Now click the link - it should use the speculative connection + await clickLink(browser, "testLink"); + + // Check connection count after click + let connectionCountAfterClick = gServer.connectionNumber; + info("Connection count after click: " + connectionCountAfterClick); + info( + "Connection number that handled the request: " + + gConnectionNumberWhenRequested + ); + + // Verify that exactly one NEW connection was established + Assert.equal( + connectionCountAfterClick, + connectionCountBeforeHover + 1, + "Exactly one connection should be established for the HTTP request" + ); + + // Verify that the request was handled by the new connection + Assert.equal( + gConnectionNumberWhenRequested, + connectionCountBeforeHover + 1, + "The HTTP request should be handled by the new connection" + ); + } + ); + + Services.obs.removeObserver(observer, "speculative-connect-request"); +}); + +add_task(async function test_link_hover_https_page_pref_disabled() { + // Disable the pref for speculative connections on HTTPS + await SpecialPowers.pushPrefEnv({ + set: [["network.predictor.enable-hover-on-ssl", false]], + }); + + let speculativeConnectObserved = false; + let observer = { + QueryInterface: ChromeUtils.generateQI(["nsIObserver"]), + observe(aSubject, aTopic, aData) { + if ( + aTopic == "speculative-connect-request" && + aData.includes("localhost") + ) { + info("Observed speculative connection request for: " + aData); + speculativeConnectObserved = true; + } + }, + }; + Services.obs.addObserver(observer, "speculative-connect-request"); + + // Load the test page from HTTPS example.com with the target URL pointing to our local server + const targetURL = encodeURIComponent(gServerURL + "/target.html"); + const pageURL = + "https://example.com" + + TEST_PATH + + "file_link_hover.sjs?target=" + + targetURL; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: pageURL, + waitForLoad: true, + }, + async function (browser) { + // Record the current connection count before hovering + let connectionCountBeforeHover = gServer.connectionNumber; + info("Connection count before hover: " + connectionCountBeforeHover); + + // Wait for the link element to be available in the page + await SpecialPowers.spawn(browser, [], async () => { + await ContentTaskUtils.waitForCondition( + () => content.document.getElementById("testLink"), + "Waiting for link element to be available" + ); + }); + + // Hover over the link + await hoverOverLink(browser, "testLink"); + + // Wait a bit to see if any speculative connection would be triggered + // eslint-disable-next-line mozilla/no-arbitrary-setTimeout + await new Promise(resolve => setTimeout(resolve, 1000)); + + // Check connection count after hover + let connectionCountAfterHover = gServer.connectionNumber; + info("Connection count after hover: " + connectionCountAfterHover); + + // Verify that NO speculative connection request was observed + Assert.ok( + !speculativeConnectObserved, + "Speculative connection should NOT be triggered on link hover from HTTPS page when pref is disabled" + ); + + // Now click the link - it will create a new connection + await clickLink(browser, "testLink"); + + // Check connection count after click + let connectionCountAfterClick = gServer.connectionNumber; + info("Connection count after click: " + connectionCountAfterClick); + + // Verify that exactly one NEW connection was created (by the click, not hover) + Assert.equal( + connectionCountAfterClick, + connectionCountBeforeHover + 1, + "Exactly one connection should be established (from the click, not hover)" + ); + } + ); + + Services.obs.removeObserver(observer, "speculative-connect-request"); + + // Restore the pref + await SpecialPowers.popPrefEnv(); +}); diff --git a/netwerk/test/browser/file_link_hover.sjs b/netwerk/test/browser/file_link_hover.sjs @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function handleRequest(request, response) { + response.setHeader("Content-Type", "text/html", false); + response.setHeader("Cache-Control", "no-cache", false); + + // Get the target URL from the query string + const params = new URLSearchParams(request.queryString); + const targetURL = params.get("target") || "about:blank"; + + const html = `<!DOCTYPE html> +<html> +<head> + <meta charset="UTF-8"> + <title>Link Hover Test</title> +</head> +<body> + <h1>Link Hover Speculative Connection Test</h1> + <a id="testLink" href="${targetURL}">Test Link</a> +</body> +</html>`; + + response.write(html); +} diff --git a/netwerk/test/browser/request_accept_language.sjs b/netwerk/test/browser/request_accept_language.sjs @@ -0,0 +1,7 @@ +"use strict"; + +function handleRequest(request, response) { + const headers = request.getHeader("Accept-Language"); + response.setHeader("Content-Type", "text/plain", false); + response.write(JSON.stringify(headers)); +} diff --git a/netwerk/test/unit/test_predictor.js b/netwerk/test/unit/test_predictor.js @@ -184,28 +184,6 @@ function open_and_continue(uris, continueCallback) { } } -function test_link_hover() { - if (!running_single_process && !is_child_process()) { - // This one we can just proxy to the child and be done with, no extra setup - // is necessary. - sendCommand("test_link_hover();"); - return; - } - - var uri = newURI("http://localhost:4444/foo/bar"); - var referrer = newURI("http://localhost:4444/foo"); - var preconns = ["http://localhost:4444"]; - - var verifier = new Verifier("hover", [], preconns, []); - predictor.predict( - uri, - referrer, - predictor.PREDICT_LINK, - origin_attributes, - verifier - ); -} - const pageload_toplevel = newURI("http://localhost:4444/index.html"); function continue_test_pageload() { @@ -765,7 +743,6 @@ function cleanup() { var tests = [ // This must ALWAYS come first, to ensure a clean slate reset_predictor, - test_link_hover, test_pageload, // TODO: These are disabled until the features are re-written //test_redirect, diff --git a/python/mozbuild/mozbuild/repackaging/rpm.py b/python/mozbuild/mozbuild/repackaging/rpm.py @@ -93,7 +93,7 @@ def repackage_rpm( ) as f: f.write("This is a packaged app.\n") - inject_distribution_folder(source_dir, "rpm", app_name) + inject_distribution_folder(rpm_dir, "", app_name) inject_desktop_entry_file( log, rpm_dir, diff --git a/remote/marionette/actors/MarionetteCommandsChild.sys.mjs b/remote/marionette/actors/MarionetteCommandsChild.sys.mjs @@ -87,7 +87,7 @@ export class MarionetteCommandsChild extends JSWindowActorChild { lazy.event.sendKeyUp(details.eventData, win); break; case "synthesizeMouseAtPoint": - lazy.event.synthesizeMouseAtPoint( + await lazy.event.synthesizeMouseAtPoint( details.x, details.y, details.eventData, diff --git a/remote/marionette/driver.sys.mjs b/remote/marionette/driver.sys.mjs @@ -158,10 +158,12 @@ class ActionsHelper { */ dispatchEvent(eventName, browsingContext, details) { if ( - eventName === "synthesizeWheelAtPoint" && - lazy.actions.useAsyncWheelEvents + (eventName === "synthesizeWheelAtPoint" && + lazy.actions.useAsyncWheelEvents) || + (eventName == "synthesizeMouseAtPoint" && + lazy.actions.useAsyncMouseEvents) ) { - browsingContext = browsingContext.topChromeWindow.browsingContext; + browsingContext = browsingContext.topChromeWindow?.browsingContext; details.eventData.asyncEnabled = true; } @@ -173,12 +175,16 @@ class ActionsHelper { * * @param {BrowsingContext} browsingContext * The browsing context to dispatch the event to. - * - * @returns {Promise} - * Promise that resolves when the finalization is done. */ - finalizeAction(browsingContext) { - return this.#getActor(browsingContext).finalizeAction(); + async finalizeAction(browsingContext) { + try { + await this.#getActor(browsingContext).finalizeAction(); + } catch (e) { + // Ignore the error if the underlying browsing context is already gone. + if (e.name !== lazy.error.NoSuchWindowError.name) { + throw e; + } + } } /** diff --git a/remote/marionette/interaction.sys.mjs b/remote/marionette/interaction.sys.mjs @@ -190,7 +190,7 @@ async function webdriverClickElement(el, a11y) { interaction.selectOption(el); } else { // Synthesize a pointerMove action. - lazy.event.synthesizeMouseAtPoint( + await lazy.event.synthesizeMouseAtPoint( clickPoint.x, clickPoint.y, { @@ -204,7 +204,7 @@ async function webdriverClickElement(el, a11y) { // Special handling is required if the mousemove started a drag session. // In this case, mousedown event shouldn't be fired, and the mouseup should // end the session. Therefore, we should synthesize only mouseup. - lazy.event.synthesizeMouseAtPoint( + await lazy.event.synthesizeMouseAtPoint( clickPoint.x, clickPoint.y, { @@ -218,7 +218,7 @@ async function webdriverClickElement(el, a11y) { let clicked = interaction.flushEventLoop(containerEl); // Synthesize a pointerDown + pointerUp action. - lazy.event.synthesizeMouseAtPoint( + await lazy.event.synthesizeMouseAtPoint( clickPoint.x, clickPoint.y, { allowToHandleDragDrop: true }, @@ -278,7 +278,7 @@ async function seleniumClickElement(el, a11y) { let rects = el.getClientRects(); let centre = lazy.dom.getInViewCentrePoint(rects[0], win); let opts = {}; - lazy.event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win); + await lazy.event.synthesizeMouseAtPoint(centre.x, centre.y, opts, win); } } diff --git a/remote/shared/webdriver/Actions.sys.mjs b/remote/shared/webdriver/Actions.sys.mjs @@ -49,6 +49,14 @@ const MODIFIER_NAME_LOOKUP = { Meta: "meta", }; +// Flag, that indicates if an async widget event should be used when dispatching a mouse event. +XPCOMUtils.defineLazyPreferenceGetter( + actions, + "useAsyncMouseEvents", + "remote.events.async.mouse.enabled", + false +); + // Flag, that indicates if an async widget event should be used when dispatching a wheel scroll event. XPCOMUtils.defineLazyPreferenceGetter( actions, @@ -507,6 +515,10 @@ class KeyInputSource extends InputSource { * Input state associated with a pointer-type device. */ class PointerInputSource extends InputSource { + #initialized; + #x; + #y; + static type = "pointer"; /** @@ -521,9 +533,23 @@ class PointerInputSource extends InputSource { super(id); this.pointer = pointer; - this.x = 0; - this.y = 0; this.pressed = new Set(); + + this.#initialized = false; + this.#x = 0; + this.#y = 0; + } + + get initialized() { + return this.#initialized; + } + + get x() { + return this.#x; + } + + get y() { + return this.#y; } /** @@ -544,6 +570,12 @@ class PointerInputSource extends InputSource { return this.pressed.has(button); } + moveTo(x, y) { + this.#initialized = true; + this.#x = x; + this.#y = y; + } + /** * Add |button| to the set of pressed keys. * @@ -1368,14 +1400,15 @@ class PointerDownAction extends PointerAction { * Promise that is resolved once the action is complete. */ async dispatch(state, inputSource, tickDuration, options) { - lazy.logger.trace( - `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} button: ${this.button}` - ); - if (inputSource.isPressed(this.button)) { return; } + lazy.logger.trace( + `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} ` + + `button: ${this.button} async: ${actions.useAsyncMouseEvents}` + ); + inputSource.press(this.button); await inputSource.pointer.pointerDown(state, inputSource, this, options); @@ -1472,14 +1505,15 @@ class PointerUpAction extends PointerAction { * Promise that is resolved once the action is complete. */ async dispatch(state, inputSource, tickDuration, options) { - lazy.logger.trace( - `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} button: ${this.button}` - ); - if (!inputSource.isPressed(this.button)) { return; } + lazy.logger.trace( + `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} ` + + `button: ${this.button} async: ${actions.useAsyncMouseEvents}` + ); + inputSource.release(this.button); await inputSource.pointer.pointerUp(state, inputSource, this, options); @@ -1581,23 +1615,38 @@ class PointerMoveAction extends PointerAction { * Promise that is resolved once the action is complete. */ async dispatch(state, inputSource, tickDuration, options) { - const { assertInViewPort, context } = options; - - lazy.logger.trace( - `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} x: ${this.x} y: ${this.y}` - ); + const { assertInViewPort, context, toBrowserWindowCoordinates } = options; - const target = await this.origin.getTargetCoordinates( + let moveCoordinates = await this.origin.getTargetCoordinates( inputSource, [this.x, this.y], options ); - await assertInViewPort(target, context); + await assertInViewPort(moveCoordinates, context); + + lazy.logger.trace( + `Dispatch ${this.constructor.name} ${inputSource.pointer.type} with id: ${this.id} ` + + `x: ${moveCoordinates[0]} y: ${moveCoordinates[1]} ` + + `async: ${actions.useAsyncMouseEvents}` + ); + + // Only convert coordinates if these are for a content process, and are not + // relative to an already initialized pointer source. + if ( + !(this.origin instanceof PointerOrigin && inputSource.initialized) && + context.isContent && + actions.useAsyncMouseEvents + ) { + moveCoordinates = await toBrowserWindowCoordinates( + moveCoordinates, + context + ); + } return moveOverTime( [[inputSource.x, inputSource.y]], - [target], + [moveCoordinates], this.duration ?? tickDuration, async _target => await this.performPointerMoveStep(state, inputSource, _target, options) @@ -1626,13 +1675,14 @@ class PointerMoveAction extends PointerAction { } const target = targets[0]; - lazy.logger.trace( - `PointerMoveAction.performPointerMoveStep ${JSON.stringify(target)}` - ); if (target[0] == inputSource.x && target[1] == inputSource.y) { return; } + lazy.logger.trace( + `PointerMoveAction.performPointerMoveStep ${JSON.stringify(target)}` + ); + await inputSource.pointer.pointerMove( state, inputSource, @@ -1642,8 +1692,7 @@ class PointerMoveAction extends PointerAction { options ); - inputSource.x = target[0]; - inputSource.y = target[1]; + inputSource.moveTo(target[0], target[1]); } /** @@ -2238,8 +2287,7 @@ class PointerMoveTouchActionGroup extends TouchActionGroup { const eventData = new MultiTouchEventData("touchmove"); for (const [inputSource, action, target] of perPointerData) { - inputSource.x = target[0]; - inputSource.y = target[1]; + inputSource.moveTo(target[0], target[1]); eventData.addPointerEventData(inputSource, action); eventData.update(state, inputSource); } diff --git a/remote/shared/webdriver/Event.sys.mjs b/remote/shared/webdriver/Event.sys.mjs @@ -59,26 +59,40 @@ event.MouseButton = { }; /** - * Synthesise a mouse event at a point. + * Synthesize a mouse event in `win` at a point. * - * If the type is specified in opts, an mouse event of that type is + * If the type is specified in `event`, a mouse event of that type is * fired. Otherwise, a mousedown followed by a mouseup is performed. * - * @param {number} left - * Offset from viewport left, in CSS pixels - * @param {number} top - * Offset from viewport top, in CSS pixels - * @param {object} opts - * Object which may contain the properties "shiftKey", "ctrlKey", - * "altKey", "metaKey", "accessKey", "clickCount", "button", and - * "type". - * @param {Window} win - * Window object. + * @param {number} left - Value for the X offset in CSS pixels. + * @param {number} top - Value for the Y offset in CSS pixels. + * @param {module:EventUtils~MouseEventData} event - Details of the mouse event + * to dispatch. + * @param {DOMWindow} win - DOM window used to dispatch the event. * - * @returns {boolean} defaultPrevented + * @returns {Promise<boolean>} Promise that resolves to a boolean, + * indicating whether the event had preventDefault() called on it. */ -event.synthesizeMouseAtPoint = function (left, top, opts, win) { - return _getEventUtils(win).synthesizeMouseAtPoint(left, top, opts, win); +event.synthesizeMouseAtPoint = function (left, top, event, win) { + if (!event.asyncEnabled) { + return Promise.resolve( + _getEventUtils(win).synthesizeMouseAtPoint(left, top, event, win) + ); + } + + // A callback must be used when handling events with the `asyncEnabled` + // flag set to `true`, as these events are synthesized in the parent process. + // We need to wait for them to be fully dispatched to the content process + // before continuing. + return new Promise(resolve => { + const preventDefault = _getEventUtils(win).synthesizeMouseAtPoint( + left, + top, + event, + win, + () => resolve(preventDefault) + ); + }); }; /** diff --git a/remote/shared/webdriver/test/xpcshell/test_Actions.js b/remote/shared/webdriver/test/xpcshell/test_Actions.js @@ -314,8 +314,7 @@ add_task(async function test_computePointerDestinationViewport() { const actionItem = chain[0][0]; const inputSource = state.getInputSource(actionItem.id); // these values should not affect the outcome - inputSource.x = "99"; - inputSource.y = "10"; + inputSource.moveTo(99, 10); const target = await actionItem.origin.getTargetCoordinates( inputSource, [actionItem.x, actionItem.y], @@ -343,8 +342,7 @@ add_task(async function test_computePointerDestinationPointer() { ); const actionItem = chain[0][0]; const inputSource = state.getInputSource(actionItem.id); - inputSource.x = 10; - inputSource.y = 99; + inputSource.moveTo(10, 99); const target = await actionItem.origin.getTargetCoordinates( inputSource, [actionItem.x, actionItem.y], diff --git a/remote/webdriver-bidi/modules/root/input.sys.mjs b/remote/webdriver-bidi/modules/root/input.sys.mjs @@ -9,7 +9,9 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { actions: "chrome://remote/content/shared/webdriver/Actions.sys.mjs", assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs", + error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs", event: "chrome://remote/content/shared/webdriver/Event.sys.mjs", + NavigableManager: "chrome://remote/content/shared/NavigableManager.sys.mjs", pprint: "chrome://remote/content/shared/Format.sys.mjs", }); @@ -76,17 +78,31 @@ class InputModule extends RootBiDiModule { * Promise that resolves once the event is dispatched. */ async #dispatchEvent(eventName, context, details) { - if ( - eventName === "synthesizeWheelAtPoint" && - lazy.actions.useAsyncWheelEvents - ) { - details.eventData.asyncEnabled = true; - } + details.eventData.asyncEnabled = + (eventName === "synthesizeWheelAtPoint" && + lazy.actions.useAsyncWheelEvents) || + (eventName == "synthesizeMouseAtPoint" && + lazy.actions.useAsyncMouseEvents); // TODO: Call the _dispatchEvent method of the windowglobal module once // chrome support was added for the message handler. if (details.eventData.asyncEnabled) { + if (!context || context.isDiscarded) { + const id = lazy.NavigableManager.getIdForBrowsingContext(context); + throw new lazy.error.NoSuchFrameError( + `Browsing Context with id ${id} not found` + ); + } + switch (eventName) { + case "synthesizeMouseAtPoint": + await lazy.event.synthesizeMouseAtPoint( + details.x, + details.y, + details.eventData, + context.topChromeWindow + ); + break; case "synthesizeWheelAtPoint": await lazy.event.synthesizeWheelAtPoint( details.x, @@ -113,12 +129,16 @@ class InputModule extends RootBiDiModule { * * @param {BrowsingContext} context * The browsing context to forward the command to. - * - * @returns {Promise} - * Promise that resolves when the finalization is done. */ - #finalizeAction(context) { - return this._forwardToWindowGlobal("_finalizeAction", context.id); + async #finalizeAction(context) { + try { + await this._forwardToWindowGlobal("_finalizeAction", context.id); + } catch (e) { + // Ignore the error if the underlying browsing context is already gone. + if (e.name !== "DiscardedBrowsingContextError") { + throw e; + } + } } /** diff --git a/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs b/remote/webdriver-bidi/modules/windowglobal/input.sys.mjs @@ -69,7 +69,7 @@ class InputModule extends WindowGlobalBiDiModule { lazy.event.sendKeyUp(details.eventData, this.messageHandler.window); break; case "synthesizeMouseAtPoint": - lazy.event.synthesizeMouseAtPoint( + await lazy.event.synthesizeMouseAtPoint( details.x, details.y, details.eventData, diff --git a/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp b/security/sandbox/win/src/sandboxbroker/sandboxBroker.cpp @@ -1083,6 +1083,12 @@ void SandboxBroker::SetSecurityLevelForContentProcess(int32_t aSandboxLevel, config->SetDesktop(sandbox::Desktop::kAlternateWinstation); } + if (StaticPrefs::security_sandbox_content_close_ksecdd_handle()) { + result = config->AddKernelObjectToClose(L"File", L"\\Device\\KsecDD"); + MOZ_RELEASE_ASSERT(sandbox::SBOX_ALL_OK == result, + "AddKernelObjectToClose should never fail."); + } + sandbox::MitigationFlags mitigations = sandbox::MITIGATION_BOTTOM_UP_ASLR | sandbox::MITIGATION_HEAP_TERMINATE | sandbox::MITIGATION_SEHOP | sandbox::MITIGATION_DEP_NO_ATL_THUNK | diff --git a/taskcluster/test_configs/variants.yml b/taskcluster/test_configs/variants.yml @@ -523,6 +523,7 @@ async-event-dispatching: merge: mozharness: extra-options: + - "--setpref=remote.events.async.mouse.enabled=true" - "--setpref=remote.events.async.wheel.enabled=true" vertical-tabs: diff --git a/testing/gtest/mozilla/WaitFor.h b/testing/gtest/mozilla/WaitFor.h @@ -192,6 +192,30 @@ inline auto TakeN(MediaEventSourceImpl<Lp, Args...>& aEvent, size_t aN) return holder->Ensure(__func__); } +using TakeNVoidPromise = MozPromise<size_t, bool, true>; + +template <ListenerPolicy Lp> +inline auto TakeN(MediaEventSourceImpl<Lp, void>& aEvent, size_t aN) + -> RefPtr<TakeNVoidPromise> { + using Storage = Maybe<size_t>; + using Promise = TakeNVoidPromise; + using Holder = media::Refcountable<MozPromiseHolder<Promise>>; + using Values = media::Refcountable<Storage>; + using Listener = media::Refcountable<MediaEventListener>; + auto values = MakeRefPtr<Values>(); + *values = Some(0); + auto listener = MakeRefPtr<Listener>(); + auto holder = MakeRefPtr<Holder>(); + *listener = aEvent.Connect( + AbstractThread::GetCurrent(), [values, listener, aN, holder]() { + if (++(values->ref()) == aN) { + listener->Disconnect(); + holder->Resolve(**values, "TakeN (void) listener callback"); + } + }); + return holder->Ensure(__func__); +} + /** * Helper that, given that canonicals have just been updated on the current * thread, will block its execution until mirrors and their watchers have diff --git a/testing/web-platform/meta/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py.ini b/testing/web-platform/meta/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py.ini @@ -1,24 +1,4 @@ [pointer_mouse.py] - [test_drag_and_drop[10-15-0\]] - disabled: - if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1762119 - - [test_drag_and_drop[10-15-300\]] - disabled: - if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1762119 - - [test_drag_and_drop[10-15-800\]] - disabled: - if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1762119 - - [test_drag_and_drop[10--15-0\]] - disabled: - if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1762119 - - [test_drag_and_drop[10--15-300\]] - disabled: - if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1762119 - - [test_drag_and_drop[10--15-800\]] - disabled: - if os == "android": bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1762119 + [test_move_to_position_in_viewport[default value\]] + expected: + if not remoteAsyncEvents: FAIL # Cannot be fixed when dispatching the event in the content process diff --git a/testing/web-platform/meta/webdriver/tests/classic/perform_actions/pointer_dblclick.py.ini b/testing/web-platform/meta/webdriver/tests/classic/perform_actions/pointer_dblclick.py.ini @@ -0,0 +1,12 @@ +[pointer_dblclick.py] + [test_dblclick_at_coordinates[0\]] + bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1999380 + expected: + if os == "android": FAIL + PASS + + [test_dblclick_at_coordinates[200\]] + bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1999380 + expected: + if os == "android": FAIL + PASS diff --git a/testing/web-platform/meta/webdriver/tests/classic/perform_actions/pointer_mouse.py.ini b/testing/web-platform/meta/webdriver/tests/classic/perform_actions/pointer_mouse.py.ini @@ -0,0 +1,12 @@ +[pointer_mouse.py] + [test_move_to_position_in_viewport[default value\]] + expected: + if not remoteAsyncEvents: FAIL # Cannot be fixed when dispatching the event in the content process + + [test_down_closes_browsing_context[without up\]] + bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1998260 + expected: + if remoteAsyncEvents: PASS + if (os == "android") and not debug: [PASS, FAIL] + if (os == "linux"): [PASS, FAIL] + FAIL diff --git a/testing/web-platform/mozilla/meta/mathml/mathml-console-messages.html.ini b/testing/web-platform/mozilla/meta/mathml/mathml-console-messages.html.ini @@ -1,2 +1,2 @@ [mathml-console-messages.html] - prefs: [dom.use_components_shim:false, mathml.legacy_mathvariant_attribute.disabled: false, mathml.mathspace_names.disabled: false] + prefs: [dom.use_components_shim:false, mathml.legacy_mathvariant_attribute.disabled: false, mathml.mathspace_names.disabled: false, mathml.operator_dictionary_accent.disabled: false] diff --git a/testing/web-platform/mozilla/meta/mediacapture-streams/MediaStreamTrack-independent-clones.https.html.ini b/testing/web-platform/mozilla/meta/mediacapture-streams/MediaStreamTrack-independent-clones.https.html.ini @@ -0,0 +1,32 @@ +[MediaStreamTrack-independent-clones.https.html] + [gDM gets expected modes by {"resizeMode":"none","width":100} + (cloned) {"resizeMode":"none","height":500}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"none","frameRate":50} + (cloned) {"resizeMode":"none","frameRate":1}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"none"} + (cloned) {"resizeMode":"crop-and-scale"}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"crop-and-scale"} + (cloned) {"resizeMode":"crop-and-scale","height":100}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"crop-and-scale","height":100} + (cloned) {"resizeMode":"crop-and-scale"}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"crop-and-scale","frameRate":5} + (cloned) {"resizeMode":"crop-and-scale","frameRate":50}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"crop-and-scale","frameRate":50} + (cloned) {"resizeMode":"crop-and-scale","frameRate":5}] + expected: + if os == "android": FAIL + [gDM gets expected modes by {"resizeMode":"none"} + (cloned) {"resizeMode":"crop-and-scale","frameRate":5000}] + expected: + if os == "android": FAIL + [applyConstraints on gDM clone doesn't crop with only ideal dimensions] + expected: + if os == "android": FAIL + [applyConstraints on gDM clone doesn't crop with ideal and max dimensions] + expected: + if os == "android": FAIL + prefs: [media.getusermedia.camera.fake.force:true, media.navigator.streams.fake:false, media.cubeb.force_mock_context:true] diff --git a/testing/web-platform/mozilla/meta/screen-capture/MediaStreamTrack-independent-clones.https.html.ini b/testing/web-platform/mozilla/meta/screen-capture/MediaStreamTrack-independent-clones.https.html.ini @@ -0,0 +1,3 @@ +[MediaStreamTrack-independent-clones.https.html] + expected: + if os == "android": ERROR diff --git a/testing/web-platform/mozilla/tests/mathml/mathml-console-messages.html b/testing/web-platform/mozilla/tests/mathml/mathml-console-messages.html @@ -39,16 +39,14 @@ function testMessageForMarkup(markup, regexp, level) { promise_test(async function() { let messages = await retrieveConsoleMessagesFor(markup); - - // Sometimes MathML messages are logged several times, so just - // ensure there is at least one. assert_greater_than_equal(messages.length, 1); - // Compare against the regexp. - assert_regexp_match(messages[0].errorMessage, regexp); + // Try to match any element in messages against the provided regexp. + let found = messages.find(m => regexp.test(m.errorMessage)); + assert_true(!!found, `regexp ${regexp} not found in array ${messages.map(m => m.errorMessage)}`); // Check whether this is a warning or an error. - assert_equals(messages[0].isWarning, level == MessageLevel.WARNING); + assert_equals(found.isWarning, level == MessageLevel.WARNING); }, `Message for ${markup}`); } @@ -241,5 +239,49 @@ new RegExp(`mathvariant='${value}'” .* deprecated`), MessageLevel.WARNING); }); + + + // MathML_DeprecatedMoExplicitAccent + testNoMessageForMarkup(`<math><mrow><mo accent="true">x</mo></mrow></math>`); + [ + ["mover", "accent", `<mover><mi>A</mi><mo accent="true">x</mo></mover>`], + ["munder", "accentunder", `<munder><mi>A</mi><mo accent="true">x</mo></munder>`], + ["munderover", "accent/accentunder", `<munderover><mi>A</mi><mo accent="true">x</mo><mo>y</mo></munderover>`], + ["munderover", "accent/accentunder", `<munderover><mi>A</mi><mo>x</mo><mo accent="true">y</mo></munderover>`], + ["munderover", "accent/accentunder", `<munderover><mi>A</mi><mo accent="true">x</mo><mo accent="true">y</mo></munderover>`], + ].forEach(([name, accent, markup]) => { + testMessageForMarkup( + `<math><${markup}></math>`, + new RegExp(`Setting the accent attribute on mo elements is deprecated. Use the “${accent}” attribute on “${name}” instead.`), + MessageLevel.WARNING); + }); + + + // MathML_DeprecatedMoverNonExplicitAccent + // MathML_DeprecatedMunderNonExplicitAccentunder + [ "mover", "munder" ].forEach((name) => { + testNoMessageForMarkup(`<math><${name}><mi>A</mi><mo>x</mo></${name}></math>`); + }); + testNoMessageForMarkup(`<math><munderover><mi>A</mi><mo>x</mo><mo>y</mo></munderover></math>`); + [ + // Inferring the accent from the operator dictionary. + ["mover", "accent", `<mover><mi>B</mi><mo>^</mo></mover>`], + ["munder", "accentunder", `<munder><mi>B</mi><mo>^</mo></munder>`], + ["munderover", "accent", `<munderover><mi>B</mi><mo>x</mo><mo>^</mo></munderover>`], + ["munderover", "accentunder", `<munderover><mi>B</mi><mo>^</mo><mo>x</mo></munderover>`], + // Inferring the accent from `accent="true"`. + // The last test triggers both accent and accentunder warnings, so we check it twice. + ["mover", "accent", `<mover><mi>C</mi><mo accent="true">x</mo></mover>`], + ["munder", "accentunder", `<munder><mi>C</mi><mo accent="true">x</mo></munder>`], + ["munderover", "accent", `<munderover><mi>C</mi><mo>x</mo><mo accent="true">y</mo></munderover>`], + ["munderover", "accentunder", `<munderover><mi>C</mi><mo accent="true">x</mo><mo>y</mo></munderover>`], + ["munderover", "accent", `<munderover><mi>C</mi><mo accent="true">x</mo><mo accent="true">y</mo></munderover>`], + ["munderover", "accentunder", `<munderover><mi>D</mi><mo accent="true">x</mo><mo accent="true">y</mo></munderover>`], + ].forEach(([name, accent, markup]) => { + testMessageForMarkup( + `<math><${markup}></math>`, + new RegExp(`Inferring the ${accent} property from the core operator is deprecated. Consider adding an explicit ${accent} attribute to “${name}”.`), + MessageLevel.WARNING); + }); </script> </body> diff --git a/testing/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-independent-clones.https.html b/testing/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-independent-clones.https.html @@ -0,0 +1,209 @@ +<!doctype html> +<title>MediaStreamTrack clones have independent settings. Assumes Mozilla's fake camera source with 480p and 720p capabilities.</title> +<meta name="timeout" content="long"> +<p class="instructions">When prompted, accept to share your video stream.</p> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/resources/testdriver.js></script> +<script src=/resources/testdriver-vendor.js></script> +<script src=video-test-helper.js></script> +<script> + "use strict" + + // Native capabilities supported by the fake camera. + const nativeLow = {width: 640, height: 480, frameRate: 30, resizeMode: "none"}; + const nativeHigh = {width: 1280, height: 720, frameRate: 10, resizeMode: "none"}; + + [ + [ + [{resizeMode: "none", width: 1000}, {resizeMode: "none", width: 500}], + [nativeHigh, nativeLow], + ], + [ + [{resizeMode: "none", height: 500}, {resizeMode: "none", width: 1000}], + [nativeLow, nativeHigh], + ], + [ + [{resizeMode: "none", width: 500, height: 500}, {resizeMode: "crop-and-scale", width: 1000, height: 750}], + [nativeLow, {resizeMode: "crop-and-scale", width: 1000, height: 720, frameRate: 10}], + ], + [ + [{resizeMode: "crop-and-scale", width: 800, height: 600}, {resizeMode: "none"}], + [{resizeMode: "crop-and-scale", width: 800, height: 600, frameRate: 10}, nativeLow], + ], + [ + [{resizeMode: "crop-and-scale", height: 500}, {resizeMode: "crop-and-scale", height: 300}], + [ + {resizeMode: "crop-and-scale", width: 889, height: 500, frameRate: 10}, + {resizeMode: "crop-and-scale", width: 400, height: 300, frameRate: 30} + ], + ], + [ + [ + {resizeMode: "crop-and-scale", frameRate: {exact: 5}}, + {resizeMode: "crop-and-scale", frameRate: {exact: 1}}, + ], + [ + {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 5}, + {resizeMode: "crop-and-scale", width: 640, height: 480, frameRate: 1}, + ], + [[2, 7], [0.5, 3]], + ], + ].forEach( + ([ + [video, cloneVideo], + [expected, cloneExpected], + [testFramerate, cloneTestFramerate] = [null, null] + ]) => promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + let settings = track.getSettings(); + let cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original: ${key}`); + assert_equals(cloneSettings[key], expected[key], `clone: ${key}`); + } + await clone.applyConstraints(cloneVideo); + settings = track.getSettings(); + cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original-post: ${key}`); + } + for (const key of Object.keys(cloneExpected)) { + assert_equals(cloneSettings[key], cloneExpected[key], `clone-post: ${key}`); + } + await test_resolution_equals(t, track, settings.width, settings.height); + await test_resolution_equals(t, clone, cloneSettings.width, cloneSettings.height); + if (testFramerate) { + const [low, high] = testFramerate; + await test_framerate_between_exclusive(t, track, low, high); + } + if (cloneTestFramerate) { + const [low, high] = cloneTestFramerate; + await test_framerate_between_exclusive(t, clone, low, high); + } + }, `gUM gets ${JSON.stringify(expected)} + clone ` + + `${JSON.stringify(cloneExpected)} by ${JSON.stringify(video)} ` + + `and ${JSON.stringify(cloneVideo)}`)); + + [ + [ + [{echoCancellation: true}, {echoCancellation: false, noiseSuppression: true, autoGainControl: true}], + [ + {echoCancellation: true, noiseSuppression: true, autoGainControl: true}, + {echoCancellation: false, noiseSuppression: true, autoGainControl: true}, + ], + ], + [ + [{echoCancellation: false, noiseSuppression: false, autoGainControl: false}, {}], + [ + {echoCancellation: false, noiseSuppression: false, autoGainControl: false}, + {echoCancellation: true, noiseSuppression: true, autoGainControl: true}, + ], + ], + ].forEach( + ([ + [audio, cloneAudio], + [expected, cloneExpected], + ]) => promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({audio}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + let settings = track.getSettings(); + let cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original: ${key}`); + assert_equals(cloneSettings[key], expected[key], `clone: ${key}`); + } + await clone.applyConstraints(cloneAudio); + settings = track.getSettings(); + cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original-post: ${key}`); + } + for (const key of Object.keys(cloneExpected)) { + assert_equals(cloneSettings[key], cloneExpected[key], `clone-post: ${key}`); + } + }, `gUM gets ${JSON.stringify(expected)} + clone ` + + `${JSON.stringify(cloneExpected)} by ${JSON.stringify(audio)} ` + + `and ${JSON.stringify(cloneAudio)}`)); + + + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "none", width: {min: 2000}}) + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible width"); + }, "applyConstraints on gUM clone is rejected by resizeMode none and impossible min-width"); + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "none", width: {max: 200}}) + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible width"); + }, "applyConstraints on gUM clone is rejected by resizeMode none and impossible max-width"); + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "crop-and-scale", width: {min: 2000}}) + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible width"); + }, "applyConstraints on gUM clone is rejected by resizeMode crop-and-scale and impossible width"); + + promise_test(async t => { + const stream = await navigator.mediaDevices.getUserMedia({video: true}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + try { + await clone.applyConstraints({resizeMode: "crop-and-scale", frameRate: {min: 50}}); + } catch(e) { + assert_equals(e.name, "OverconstrainedError", `got error ${e.message}`); + return; + } + assert_unreached("applyConstraints is rejected with impossible fps"); + }, "applyConstraints on gUM clone is rejected by resizeMode crop-and-scale impossible fps"); +</script> diff --git a/testing/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-resizeMode.https.html b/testing/web-platform/mozilla/tests/mediacapture-streams/MediaStreamTrack-resizeMode.https.html @@ -6,77 +6,11 @@ <script src=/resources/testharnessreport.js></script> <script src=/resources/testdriver.js></script> <script src=/resources/testdriver-vendor.js></script> +<script src=settings-helper.js></script> +<script src=video-test-helper.js></script> <script> "use strict" - async function test_framerate_between_exclusive(t, track, lower, upper) { - const video = document.createElement("video"); - document.body.appendChild(video); - t.add_cleanup(async () => document.body.removeChild(video)); - - video.srcObject = new MediaStream([track]); - await video.play(); - - const numSeconds = 2; - await new Promise(r => setTimeout(r, numSeconds * 1000)); - const totalVideoFrames = video.mozPaintedFrames; - assert_between_exclusive(totalVideoFrames / numSeconds, lower, upper, "totalVideoFrames"); - } - - function createSettingsDicts(width, height, step = 1) { - const settingsDicts = [], aspect = width / height; - do { - settingsDicts.push({ width, height }); - if (width > height) { - height = Math.round((width - step) / aspect); - width -= step; - } else { - width = Math.round((height - step) * aspect); - height -= step; - } - } while (width > 2 && height > 2); - return settingsDicts; - } - - function integerFitness(actual, ideal) { - if (actual == ideal) { - return 0; - } - return Math.abs(actual - ideal) / Math.max(Math.abs(actual), Math.abs(ideal)); - } - - function findFittestResolutionSetting(width, height, constraints) { - const widthIsNumber = typeof constraints.width == "number"; - const heightIsNumber = typeof constraints.height == "number"; - const c = { - width: { - ideal: widthIsNumber ? constraints.width : constraints?.width?.ideal, - max: constraints?.width?.max ?? 1000000, - }, - height: { - ideal: heightIsNumber ? constraints.height : constraints?.height?.ideal, - max: constraints?.height?.max ?? 1000000, - }, - }; - const dicts = createSettingsDicts(width, height) - .filter(s => s.width <= c.width.max && s.height <= c.height.max); - for (const dict of dicts) { - dict.distance = - integerFitness(dict.width, c.width.ideal) + - integerFitness(dict.height, c.height.ideal); - } - - const filteredDicts = dicts.filter(s => { - return (!c.width.ideal || s.width <= c.width.ideal) && - (!c.height.ideal || s.height <= c.height.ideal); - }); - - return filteredDicts.reduce( - (a, b) => (a.distance < b.distance ? a : b), - filteredDicts[0], - ); - } - // Native capabilities supported by the fake camera. const nativeLow = {width: 640, height: 480, frameRate: 30, resizeMode: "none"}; const nativeHigh = {width: 1280, height: 720, frameRate: 10, resizeMode: "none"}; diff --git a/testing/web-platform/mozilla/tests/mediacapture-streams/settings-helper.js b/testing/web-platform/mozilla/tests/mediacapture-streams/settings-helper.js @@ -0,0 +1,54 @@ + +function createSettingsDicts(width, height, step = 1) { + const settingsDicts = [], aspect = width / height; + do { + settingsDicts.push({ width, height }); + if (width > height) { + height = Math.round((width - step) / aspect); + width -= step; + } else { + width = Math.round((height - step) * aspect); + height -= step; + } + } while (width > 2 && height > 2); + return settingsDicts; +} + +function integerFitness(actual, ideal) { + if (actual == ideal) { + return 0; + } + return Math.abs(actual - ideal) / Math.max(Math.abs(actual), Math.abs(ideal)); +} + +function findFittestResolutionSetting(width, height, constraints) { + const widthIsNumber = typeof constraints.width == "number"; + const heightIsNumber = typeof constraints.height == "number"; + const c = { + width: { + ideal: widthIsNumber ? constraints.width : constraints?.width?.ideal, + max: constraints?.width?.max ?? 1000000, + }, + height: { + ideal: heightIsNumber ? constraints.height : constraints?.height?.ideal, + max: constraints?.height?.max ?? 1000000, + }, + }; + const dicts = createSettingsDicts(width, height) + .filter(s => s.width <= c.width.max && s.height <= c.height.max); + for (const dict of dicts) { + dict.distance = + integerFitness(dict.width, c.width.ideal) + + integerFitness(dict.height, c.height.ideal); + } + + const filteredDicts = dicts.filter(s => { + return (!c.width.ideal || s.width <= c.width.ideal) && + (!c.height.ideal || s.height <= c.height.ideal); + }); + + return filteredDicts.reduce( + (a, b) => (a.distance < b.distance ? a : b), + filteredDicts[0], + ); +} diff --git a/testing/web-platform/mozilla/tests/mediacapture-streams/video-test-helper.js b/testing/web-platform/mozilla/tests/mediacapture-streams/video-test-helper.js @@ -0,0 +1,38 @@ +// Helper functions checking video flow using HTMLVideoElement. + +async function test_resolution_equals(t, track, width, height) { + const video = document.createElement("video"); + const timeout = new Promise(r => t.step_timeout(r, 1000)); + const waitForResize = () => Promise.race([ + new Promise(r => video.onresize = r), + timeout.then(() => { throw new Error("Timeout waiting for resize"); }), + ]); + video.srcObject = new MediaStream([track]); + video.play(); + // Wait for the first frame. + await waitForResize(); + // There's a potential race with applyConstraints, where a frame of the old + // resolution is in flight and renders after applyConstraints has resolved. + // In that case, wait for another frame. + if (video.videoWidth != width || video.videoHeight != height) { + await waitForResize(); + } + + assert_equals(video.videoWidth, width, "videoWidth"); + assert_equals(video.videoHeight, height, "videoHeight"); +} + +async function test_framerate_between_exclusive(t, track, lower, upper) { + const video = document.createElement("video"); + document.body.appendChild(video); + video.style = "width:320px;height:240px;" + t.add_cleanup(async () => document.body.removeChild(video)); + + video.srcObject = new MediaStream([track]); + await video.play(); + + const numSeconds = 2; + await new Promise(r => setTimeout(r, numSeconds * 1000)); + const totalVideoFrames = video.mozPaintedFrames; + assert_between_exclusive(totalVideoFrames / numSeconds, lower, upper, "totalVideoFrames"); +} diff --git a/testing/web-platform/mozilla/tests/screen-capture/MediaStreamTrack-independent-clones.https.html b/testing/web-platform/mozilla/tests/screen-capture/MediaStreamTrack-independent-clones.https.html @@ -0,0 +1,232 @@ +<!doctype html> +<title>MediaStreamTrack clones have independent settings. Assumes Mozilla's fake camera source with 480p and 720p capabilities.</title> +<meta name="timeout" content="long"> +<p class="instructions">When prompted, accept to share your video stream.</p> +<script src=/resources/testharness.js></script> +<script src=/resources/testharnessreport.js></script> +<script src=/resources/testdriver.js></script> +<script src=/resources/testdriver-vendor.js></script> +<script src=../mediacapture-streams/settings-helper.js></script> +<script src=../mediacapture-streams/video-test-helper.js></script> +<script> + "use strict" + + setup(() => assert_implements("getDisplayMedia" in navigator.mediaDevices, "getDisplayMedia() not supported")); + + // Note these gDM tests will fail if our own window is on a screen different + // than the system's first screen. They're functions in case the browser + // window needs to be moved to the first screen during the test in order to + // pass. + function screenPixelRatio() { return SpecialPowers.wrap(window).desktopToDeviceScale; } + function screenWidth() { return window.screen.width * window.devicePixelRatio; } + function screenHeight() { return window.screen.height * window.devicePixelRatio; } + function desktopWidth() { + // TODO: Bug 1965499 - scale down by screenPixelRatio by default in resizeMode: crop-and-scale. + // return screenWidth() / screenPixelRatio(); + return screenWidth(); + } + function desktopHeight() { + // TODO: Bug 1965499 - scale down by screenPixelRatio by default in resizeMode: crop-and-scale. + // return screenHeight() / screenPixelRatio(); + return screenHeight(); + } + + // TODO: By default we shouldn't be multiplying with window.devicePixelRatio (bug 1703991). + function defaultScreen() { + return { + resizeMode: "crop-and-scale", + width: screenWidth(), + height: screenHeight(), + frameRate: 30, + }; + } + // TODO: Should get the source's real refresh rate for frameRate (bug 1984363). + function nativeScreen() { + return { + resizeMode: "none", + width: screenWidth(), + height: screenHeight(), + frameRate: 60 + }; + } + + [ + [ + [{resizeMode: "none", width: 100}, {resizeMode: "none", height: 500}], + [nativeScreen, nativeScreen], + ], + [ + [{resizeMode: "none", frameRate: 50}, {resizeMode: "none", frameRate: 1}], + [nativeScreen, nativeScreen], + ], + [ + [{resizeMode: "none"}, {resizeMode: "crop-and-scale"}], + [nativeScreen, defaultScreen], + ], + [ + [{resizeMode: "crop-and-scale"}, {resizeMode: "crop-and-scale", height: 100}], + [ + defaultScreen, + () => ({ + resizeMode: "crop-and-scale", + width: Math.round(screenWidth() / screenHeight() * 100), + height: 100, + frameRate: 30 + }), + ], + ], + [ + [{resizeMode: "crop-and-scale", height: 100}, {resizeMode: "crop-and-scale"}], + [ + () => ({ + resizeMode: "crop-and-scale", + width: Math.round(screenWidth() / screenHeight() * 100), + height: 100, + frameRate: 30 + }), + defaultScreen, + ], + ], + [ + [{resizeMode: "crop-and-scale", frameRate: 5}, {resizeMode: "crop-and-scale", frameRate: 50}], + [ + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 5}; + }, + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 50}; + }, + ], + [[2, 7], [2, 50]], + ], + [ + [{resizeMode: "crop-and-scale", frameRate: 50}, {resizeMode: "crop-and-scale", frameRate: 5}], + [ + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 50}; + }, + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 5}; + }, + [[2, 50], [2, 7]] + ], + ], + [ + [{resizeMode: "none"}, {resizeMode: "crop-and-scale", frameRate: 5000}, {}], + [ + nativeScreen, + () => { + const { width, height } = defaultScreen(); + return { width, height, frameRate: 120}; + }, + ] + ], + ].forEach( + ([ + [video, cloneVideo], + [expectedFunc, cloneExpectedFunc], + [testFramerate, cloneTestFramerate] = [null, null] + ]) => { + let expected, cloneExpected; + promise_test(async t => { + expected = expectedFunc(); + cloneExpected = cloneExpectedFunc(); + await test_driver.bless('getDisplayMedia()'); + const stream = await navigator.mediaDevices.getDisplayMedia({video}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + let settings = track.getSettings(); + let cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original: ${key}`); + assert_equals(cloneSettings[key], expected[key], `clone: ${key}`); + } + await clone.applyConstraints(cloneVideo); + settings = track.getSettings(); + cloneSettings = clone.getSettings(); + for (const key of Object.keys(expected)) { + assert_equals(settings[key], expected[key], `original-post: ${key}`); + } + for (const key of Object.keys(cloneExpected)) { + assert_equals(cloneSettings[key], cloneExpected[key], `clone-post: ${key}`); + } + await test_resolution_equals(t, track, settings.width, settings.height); + await test_resolution_equals(t, clone, cloneSettings.width, cloneSettings.height); + if (testFramerate) { + const [low, high] = testFramerate; + await test_framerate_between_exclusive(t, track, low, high); + } + if (cloneTestFramerate) { + const [low, high] = cloneTestFramerate; + await test_framerate_between_exclusive(t, clone, low, high); + } + }, `gDM gets expected modes by ${JSON.stringify(video)} + (cloned) ${JSON.stringify(cloneVideo)}`); + }); + + promise_test(async t => { + await test_driver.bless('getDisplayMedia()'); + const stream = await navigator.mediaDevices.getDisplayMedia({video: {resizeMode: "none"}}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + await clone.applyConstraints( + { + resizeMode: "crop-and-scale", + width: 400, + height: 400 + } + ); + const expected = findFittestResolutionSetting( + screenWidth(), + screenHeight(), + clone.getConstraints() + ); + assert_equals(clone.getSettings().width, expected.width, "width"); + assert_equals(clone.getSettings().height, expected.height, "height"); + await test_resolution_equals(t, clone, clone.getSettings().width, clone.getSettings().height); + assert_approx_equals( + clone.getSettings().width / clone.getSettings().height, + desktopWidth() / desktopHeight(), + 0.01, + "aspect ratio" + ); + assert_equals(clone.getSettings().resizeMode, "crop-and-scale", "resizeMode"); + }, "applyConstraints on gDM clone doesn't crop with only ideal dimensions"); + + promise_test(async t => { + await test_driver.bless('getDisplayMedia()'); + const stream = await navigator.mediaDevices.getDisplayMedia({video: {resizeMode: "none"}}); + const [track] = stream.getTracks(); + const clone = track.clone(); + t.add_cleanup(() => { + track.stop(); + clone.stop(); + }); + await clone.applyConstraints( + { + resizeMode: "crop-and-scale", + width: {max: 400}, + height: {ideal: 400} + } + ); + assert_equals(clone.getSettings().width, 400, "width"); + assert_equals( + clone.getSettings().height, + Math.round(screenHeight() / screenWidth() * 400), + "height" + ); + await test_resolution_equals(t, clone, clone.getSettings().width, clone.getSettings().height); + assert_equals(clone.getSettings().resizeMode, "crop-and-scale", "resizeMode"); + }, "applyConstraints on gDM clone doesn't crop with ideal and max dimensions"); +</script> diff --git a/testing/web-platform/mozilla/tests/webdriver/bidi/emulation/set_locale_override/contexts.py b/testing/web-platform/mozilla/tests/webdriver/bidi/emulation/set_locale_override/contexts.py @@ -15,7 +15,7 @@ pytest_plugins = "tests.bidi.emulation.conftest" } ) async def test_locale_override_isolated_in_browsing_context( - bidi_session, get_current_locale, some_locale, another_locale + bidi_session, another_locale, assert_locale_against_value, some_locale ): context_in_process_1 = await bidi_session.browsing_context.create(type_hint="tab") @@ -31,5 +31,5 @@ async def test_locale_override_isolated_in_browsing_context( ) # Make sure that the locale override didn't override inappropriate context. - assert await get_current_locale(context_in_process_1) == some_locale - assert await get_current_locale(context_in_process_2) == another_locale + await assert_locale_against_value(some_locale, context_in_process_1) + await assert_locale_against_value(another_locale, context_in_process_2) diff --git a/testing/web-platform/tests/svg/text/reftests/text-path-transformed-002-ref.html b/testing/web-platform/tests/svg/text/reftests/text-path-transformed-002-ref.html @@ -0,0 +1,7 @@ +<!doctype html> +<svg width="400" height="200" viewBox="-150 0 600 200"> + <path id="rotated-anchor" d="M 20 200 A 125 125 0 0 1 289 175" transform="rotate(36 150 190)" fill="none" stroke="black"/> + <text> + <textPath href="#rotated-anchor">XXXX</textPath> + </text> +</svg> diff --git a/testing/web-platform/tests/svg/text/reftests/text-path-transformed-002.html b/testing/web-platform/tests/svg/text/reftests/text-path-transformed-002.html @@ -0,0 +1,18 @@ +<!doctype html> +<meta charset="utf-8"> +<style> +#rotated-anchor { + transform: rotate(36deg); + transform-origin: 150px 190px; +} +</style> +<link rel="author" title="Emilio Cobos Álvarez" href="mailto:emilio@crisal.io"> +<link rel="author" title="Mozilla" href="https://mozilla.org"> +<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1998071"> +<link rel="match" href="text-path-transformed-002-ref.html"> +<svg width="400" height="200" viewBox="-150 0 600 200"> + <path id="rotated-anchor" d="M 20 200 A 125 125 0 0 1 289 175" fill="none" stroke="black"/> + <text> + <textPath href="#rotated-anchor">XXXX</textPath> + </text> +</svg> diff --git a/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py b/testing/web-platform/tests/tools/wptrunner/wptrunner/browsers/firefox.py @@ -240,7 +240,8 @@ def run_info_extras(logger, default_prefs=None, **kwargs): "swgl": bool_pref("gfx.webrender.software"), "useDrawSnapshot": bool_pref("reftest.use-draw-snapshot"), "privateBrowsing": bool_pref("browser.privatebrowsing.autostart"), - "remoteAsyncEvents": bool_pref("remote.events.async.wheel.enabled"), + "remoteAsyncEvents": (bool_pref("remote.events.async.mouse.enabled") or + bool_pref("remote.events.async.wheel.enabled")), "incOriginInit": os.environ.get("MOZ_ENABLE_INC_ORIGIN_INIT") == "1", "navigationApi": bool_pref("dom.navigation.webidl.enabled"), } diff --git a/testing/web-platform/tests/webdriver/tests/bidi/emulation/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/emulation/conftest.py @@ -99,14 +99,46 @@ async def default_navigator_languages(get_current_navigator_languages, top_conte @pytest_asyncio.fixture +async def default_accept_language(bidi_session, get_fetch_headers, top_context, url): + """ + Returns default value of `Accept-Language` header. + """ + await bidi_session.browsing_context.navigate( + context=top_context["context"], + url=url("/webdriver/tests/bidi/browsing_context/support/empty.html"), + wait="complete" + ) + headers = await get_fetch_headers(top_context) + return headers["accept-language"][0] if "accept-language" in headers else None + + +@pytest_asyncio.fixture +async def assert_accept_language(bidi_session, assert_header_present, url): + """ + Assert value of `Accept-Language` header. + """ + async def assert_accept_language(context, value, sandbox_name=None): + await bidi_session.browsing_context.navigate( + context=context["context"], + url=url("/webdriver/tests/bidi/browsing_context/support/empty.html"), + wait="complete" + ) + await assert_header_present(context, "accept-language", value, sandbox_name) + + return assert_accept_language + + +@pytest_asyncio.fixture async def assert_locale_against_default( top_context, - get_current_locale, - get_current_navigator_language, - get_current_navigator_languages, + assert_accept_language, default_locale, default_navigator_language, default_navigator_languages, + default_accept_language, + get_current_locale, + get_current_navigator_language, + get_current_navigator_languages, ): """ Assert JS locale and navigator.language/s against default values. @@ -130,6 +162,7 @@ async def assert_locale_against_default( await get_current_navigator_languages(context, sandbox_name) == default_navigator_languages ) + await assert_accept_language(context, default_accept_language, sandbox_name) return assert_locale_against_default @@ -137,6 +170,7 @@ async def assert_locale_against_default( @pytest_asyncio.fixture async def assert_locale_against_value( top_context, + assert_accept_language, get_current_locale, get_current_navigator_language, get_current_navigator_languages, @@ -153,6 +187,7 @@ async def assert_locale_against_value( assert await get_current_locale(context, sandbox_name) == value assert await get_current_navigator_language(context, sandbox_name) == value assert await get_current_navigator_languages(context, sandbox_name) == [value] + await assert_accept_language(context, value) return assert_locale_against_value diff --git a/testing/web-platform/tests/webdriver/tests/bidi/emulation/set_locale_override/contexts.py b/testing/web-platform/tests/webdriver/tests/bidi/emulation/set_locale_override/contexts.py @@ -6,9 +6,9 @@ pytestmark = pytest.mark.asyncio async def test_contexts( bidi_session, new_tab, - some_locale, assert_locale_against_default, assert_locale_against_value, + some_locale, ): new_context = await bidi_session.browsing_context.create(type_hint="tab") @@ -34,8 +34,8 @@ async def test_contexts( async def test_multiple_contexts( bidi_session, new_tab, - assert_locale_against_value, assert_locale_against_default, + assert_locale_against_value, some_locale, ): new_context = await bidi_session.browsing_context.create(type_hint="tab") @@ -63,11 +63,11 @@ async def test_multiple_contexts( async def test_iframe( bidi_session, new_tab, - some_locale, - domain, inline, another_locale, assert_locale_against_value, + some_locale, + domain, ): # Set locale override. await bidi_session.emulation.set_locale_override( @@ -123,8 +123,8 @@ async def test_locale_override_applies_to_existing_sandbox( bidi_session, new_tab, another_locale, - assert_locale_against_value, assert_locale_against_default, + assert_locale_against_value, ): sandbox_name = "test" diff --git a/testing/web-platform/tests/webdriver/tests/bidi/emulation/set_locale_override/locale.py b/testing/web-platform/tests/webdriver/tests/bidi/emulation/set_locale_override/locale.py @@ -6,10 +6,10 @@ pytestmark = pytest.mark.asyncio async def test_locale_set_override_and_reset( bidi_session, new_tab, - some_locale, another_locale, assert_locale_against_default, assert_locale_against_value, + some_locale, ): await assert_locale_against_default(new_tab) diff --git a/testing/web-platform/tests/webdriver/tests/bidi/emulation/set_locale_override/user_contexts.py b/testing/web-platform/tests/webdriver/tests/bidi/emulation/set_locale_override/user_contexts.py @@ -7,9 +7,9 @@ async def test_user_contexts( bidi_session, create_user_context, new_tab, - some_locale, assert_locale_against_default, assert_locale_against_value, + some_locale, ): user_context = await create_user_context() context_in_user_context = await bidi_session.browsing_context.create( @@ -41,9 +41,9 @@ async def test_set_to_default_user_context( bidi_session, new_tab, create_user_context, - some_locale, assert_locale_against_default, assert_locale_against_value, + some_locale, ): user_context = await create_user_context() context_in_user_context = await bidi_session.browsing_context.create( @@ -76,8 +76,8 @@ async def test_set_to_default_user_context( async def test_set_to_multiple_user_contexts( bidi_session, create_user_context, - some_locale, assert_locale_against_value, + some_locale, ): user_context_1 = await create_user_context() context_in_user_context_1 = await bidi_session.browsing_context.create( @@ -98,10 +98,10 @@ async def test_set_to_multiple_user_contexts( async def test_set_to_user_context_and_then_to_context( bidi_session, create_user_context, - some_locale, another_locale, assert_locale_against_default, assert_locale_against_value, + some_locale, ): user_context = await create_user_context() context_in_user_context_1 = await bidi_session.browsing_context.create( @@ -155,10 +155,10 @@ async def test_set_to_user_context_and_then_to_context( async def test_set_to_context_and_then_to_user_context( bidi_session, create_user_context, - some_locale, another_locale, assert_locale_against_default, assert_locale_against_value, + some_locale, ): user_context = await create_user_context() context_in_user_context_1 = await bidi_session.browsing_context.create( diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse.py @@ -20,20 +20,27 @@ pytestmark = pytest.mark.asyncio CONTEXT_LOAD_EVENT = "browsingContext.load" -async def test_pointer_down_closes_browsing_context( - bidi_session, configuration, get_element, new_tab, inline, subscribe_events, - wait_for_event +@pytest.mark.parametrize("mouse_up", [False, True], ids=["without up", "with up"]) +async def test_down_closes_browsing_context( + bidi_session, + configuration, + get_element, + top_context, + inline, + subscribe_events, + wait_for_event, + mouse_up ): - url = inline("""<input onpointerdown="window.close()">close</input>""") + url = inline("""<input onmousedown="window.close()">close</input>""") - # Opening a new context via `window.open` is required for script to be able - # to close it. + # Opening a new context via `window.open` is required + # for the script to be able to close it. await subscribe_events(events=[CONTEXT_LOAD_EVENT]) on_load = wait_for_event(CONTEXT_LOAD_EVENT) await bidi_session.script.evaluate( expression=f"window.open('{url}')", - target=ContextTarget(new_tab["context"]), + target=ContextTarget(top_context["context"]), await_promise=True ) # Wait for the new context to be created and get it. @@ -42,20 +49,35 @@ async def test_pointer_down_closes_browsing_context( element = await get_element("input", context=new_context) origin = get_element_origin(element) - actions = Actions() - ( - actions.add_pointer() - .pointer_move(0, 0, origin=origin) - .pointer_down(button=0) - .pause(250 * configuration["timeout_multiplier"]) - .pointer_up(button=0) - ) + if mouse_up: + actions = Actions() + ( + actions.add_pointer() + .pointer_move(0, 0, origin=origin) + .pointer_down(button=0) + .pause(250 * configuration["timeout_multiplier"]) + .pointer_up(button=0) + ) + + with pytest.raises(NoSuchFrameException): + await bidi_session.input.perform_actions( + actions=actions, context=new_context["context"] + ) + else: + actions = Actions() + ( + actions.add_pointer() + .pointer_move(0, 0, origin=origin) + .pointer_down(button=0) + ) - with pytest.raises(NoSuchFrameException): await bidi_session.input.perform_actions( actions=actions, context=new_context["context"] ) + with pytest.raises(NoSuchFrameException): + await bidi_session.browsing_context.get_tree(root=new_context["context"], max_depth=0) + async def test_click_at_coordinates(bidi_session, top_context, load_static_test_page): await load_static_test_page(page="test_actions.html") @@ -361,13 +383,13 @@ async def test_click_navigation( assert event["url"] == destination -@pytest.mark.parametrize("x, y, event_count", [ - (0, 0, 0), - (1, 0, 1), - (0, 1, 1), +@pytest.mark.parametrize("x, y", [ + (0, 0), + (1, 0), + (0, 1), ], ids=["default value", "x", "y"]) async def test_move_to_position_in_viewport( - bidi_session, load_static_test_page, top_context, x, y, event_count + bidi_session, load_static_test_page, top_context, x, y ): await load_static_test_page(page="test_actions.html") @@ -379,7 +401,7 @@ async def test_move_to_position_in_viewport( ) events = await get_events(bidi_session, top_context["context"]) - assert len(events) == event_count + assert len(events) == 1 # Move again to check that no further mouse move event is emitted. actions = Actions() @@ -390,7 +412,7 @@ async def test_move_to_position_in_viewport( ) events = await get_events(bidi_session, top_context["context"]) - assert len(events) == event_count + assert len(events) == 1 @pytest.mark.parametrize("origin", ["viewport", "pointer", "element"]) diff --git a/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_modifier.py b/testing/web-platform/tests/webdriver/tests/bidi/input/perform_actions/pointer_mouse_modifier.py @@ -126,7 +126,8 @@ async def test_control_click_release( ] all_events = await get_events(bidi_session, top_context["context"]) events = [filter_dict(e, expected[0]) for e in all_events] - assert events == expected + for expected_event in expected: + assert expected_event in events async def test_many_modifiers_click( diff --git a/testing/web-platform/tests/webdriver/tests/bidi/network/set_extra_headers/conftest.py b/testing/web-platform/tests/webdriver/tests/bidi/network/set_extra_headers/conftest.py @@ -62,21 +62,6 @@ async def get_navigation_headers(bidi_session, url): return get_navigation_headers -@pytest_asyncio.fixture -async def get_fetch_headers(bidi_session, url): - async def get_fetch_headers(context): - echo_link = url("webdriver/tests/support/http_handlers/headers_echo.py") - result = await bidi_session.script.evaluate( - expression=f"fetch('{echo_link}').then(r => r.text())", - target=ContextTarget(context["context"]), - await_promise=True, - ) - - return (json.JSONDecoder().decode(result["value"]))["headers"] - - return get_fetch_headers - - @pytest_asyncio.fixture(params=["fetch", "navigation"]) def get_headers_methods_invariant(request, get_fetch_headers, get_navigation_headers): @@ -88,18 +73,6 @@ def get_headers_methods_invariant(request, get_fetch_headers, @pytest_asyncio.fixture -def assert_header_present(get_fetch_headers): - async def assert_header_present(context, header_name, header_value): - actual_headers = await get_fetch_headers(context) - assert header_name in actual_headers, \ - f"header '{header_name}' should be present" - assert [header_value] == actual_headers[header_name], \ - f"header '{header_name}' should have value '{header_value}'" - - return assert_header_present - - -@pytest_asyncio.fixture def assert_header_not_present(get_fetch_headers): async def assert_header_not_present(context, header_name): actual_headers = await get_fetch_headers(context) diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_contextmenu.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_contextmenu.py @@ -75,4 +75,5 @@ def test_release_control_click(session, key_reporter, key_chain, mouse_chain): {"type": "keyup"}, ] events = [filter_dict(e, expected[0]) for e in get_events(session)] - assert events == expected + for expected_event in expected: + assert expected_event in events diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse.py @@ -33,20 +33,33 @@ def test_no_browsing_context(session, closed_frame, mouse_chain): mouse_chain.click().perform() -def test_pointer_down_closes_browsing_context( - session, configuration, http_new_tab, inline, mouse_chain +@pytest.mark.parametrize("mouse_up", [False, True], ids=["without up", "with up"]) +def test_down_closes_browsing_context( + session, configuration, inline, mouse_chain, mouse_up ): - session.url = inline( - """<input onpointerdown="window.close()">close</input>""") + # Opening a new tab/window via `window.open` is required + # for the script to be able to close it. + new_window = session.execute_script(f"return window.open()") + session.window_handle = new_window.id + session.url = inline("""<input onmousedown="window.close()">close</input>""") + origin = session.find.css("input", all=False) - with pytest.raises(NoSuchWindowException): + if mouse_up: + with pytest.raises(NoSuchWindowException): + mouse_chain.pointer_move(0, 0, origin=origin) \ + .pointer_down(button=0) \ + .pause(100 * configuration["timeout_multiplier"]) \ + .pointer_up(button=0) \ + .perform() + else: mouse_chain.pointer_move(0, 0, origin=origin) \ .pointer_down(button=0) \ - .pause(100 * configuration["timeout_multiplier"]) \ - .pointer_up(button=0) \ .perform() + with pytest.raises(NoSuchWindowException): + session.url + @pytest.mark.parametrize("as_frame", [False, True], ids=["top_context", "child_context"]) def test_stale_element_reference(session, stale_element, mouse_chain, as_frame): @@ -74,13 +87,17 @@ def test_click_at_coordinates(session, test_actions_page, mouse_chain): .pointer_move(div_point["x"], div_point["y"], duration=1000) \ .click() \ .perform() + events = get_events(session) assert len(events) == 4 + assert_move_to_coordinates(div_point, "outer", events) + for e in events: if e["type"] != "mousedown": assert e["buttons"] == 0 assert e["button"] == 0 + expected = [ {"type": "mousedown", "buttons": 1}, {"type": "mouseup", "buttons": 0}, @@ -153,8 +170,10 @@ def test_click_element_center(session, test_actions_page, mouse_chain): outer = session.find.css("#outer", all=False) center = get_inview_center(outer.rect, get_viewport_rect(session)) mouse_chain.click(element=outer).perform() + events = get_events(session) assert len(events) == 4 + event_types = [e["type"] for e in events] assert ["mousemove", "mousedown", "mouseup", "click"] == event_types for e in events: @@ -221,29 +240,27 @@ def test_click_navigation(session, url, inline): wait.until(assert_page_loaded) -@pytest.mark.parametrize("x, y, event_count", [ - (0, 0, 0), - (1, 0, 1), - (0, 1, 1), +@pytest.mark.parametrize("x, y", [ + (0, 0), + (1, 0), + (0, 1), ], ids=["default value", "x", "y"]) -def test_move_to_position_in_viewport( - session, test_actions_page, mouse_chain, x, y, event_count -): +def test_move_to_position_in_viewport(session, test_actions_page, mouse_chain, x, y): mouse_chain.pointer_move(x, y).perform() events = get_events(session) - assert len(events) == event_count + assert len(events) == 1 # Move again to check that no further mouse move event is emitted. mouse_chain.pointer_move(x, y).perform() events = get_events(session) - assert len(events) == event_count + assert len(events) == 1 def test_move_to_fractional_position(session, inline, mouse_chain): session.url = inline(""" <script> var allEvents = { events: [] }; - window.addEventListener("pointermove", ev => { + window.addEventListener("mousemove", ev => { allEvents.events.push({ "type": event.type, "pageX": event.pageX, @@ -267,7 +284,6 @@ def test_move_to_fractional_position(session, inline, mouse_chain): # For now we are allowing any of floor, ceil, or precise values, because # it's unclear what the actual spec requirements really are - assert events[0]["type"] == "pointermove" assert events[0]["pageX"] == pytest.approx(target_point["x"], abs=1.0) assert events[0]["pageY"] == pytest.approx(target_point["y"], abs=1.0) diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse_drag.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_mouse_drag.py @@ -33,11 +33,11 @@ def test_drag_and_drop(session, .pointer_move(80, 50, duration=100, origin="pointer") \ .perform() - # mouseup that ends the drag is at the expected destination - e = get_events(session)[1] - assert e["type"] == "mouseup" - assert e["pageX"] == pytest.approx(initial_center["x"] + dx, abs=1.0) - assert e["pageY"] == pytest.approx(initial_center["y"] + dy, abs=1.0) + # `mouseup` that ends the drag is at the expected destination + events = get_events(session) + assert events[1]["type"] == "mouseup" + assert events[1]["pageX"] == pytest.approx(initial_center["x"] + dx, abs=1.0) + assert events[1]["pageY"] == pytest.approx(initial_center["y"] + dy, abs=1.0) def check_final_position(_): assert drag_target.rect["x"] == pytest.approx( @@ -70,12 +70,12 @@ def test_drag_and_drop_with_draggable_element(session_new_window, .pointer_move(80, 50, duration=100, origin="pointer") \ .perform() # mouseup that ends the drag is at the expected destination - e = get_events(new_session) - assert len(e) >= 5 - assert e[1]["type"] == "dragstart", "Events captured were {}".format(e) - assert e[2]["type"] == "dragover", "Events captured were {}".format(e) + events = get_events(session_new_window) + assert len(events) >= 5 + assert events[1]["type"] == "dragstart", "Events captured were {}".format(events) + assert events[2]["type"] == "dragover", "Events captured were {}".format(events) drag_events_captured = [ - ev["type"] for ev in e if ev["type"].startswith("drag") or ev["type"].startswith("drop") + ev["type"] for ev in events if ev["type"].startswith("drag") or ev["type"].startswith("drop") ] assert "dragend" in drag_events_captured assert "dragenter" in drag_events_captured diff --git a/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_origin.py b/testing/web-platform/tests/webdriver/tests/classic/perform_actions/pointer_origin.py @@ -37,9 +37,11 @@ def test_pointer_inside(session, mouse_chain, get_actions_origin_page): session.url = get_actions_origin_page( "width: 100px; height: 50px; background: green;" ) - mouse_chain.pointer_move(start_point["x"], start_point["y"]).pointer_move( - offset["x"], offset["y"], origin="pointer" - ).perform() + (mouse_chain + .pointer_move(start_point["x"], start_point["y"]) + .pointer_move(offset["x"], offset["y"], origin="pointer") + .perform() + ) click_coords = session.execute_script("return window.coords;") assert click_coords["x"] == pytest.approx(start_point["x"] + offset["x"], abs=1.0) diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures.py b/testing/web-platform/tests/webdriver/tests/support/fixtures.py @@ -315,7 +315,7 @@ def iframe(inline): @pytest.fixture def get_actions_origin_page(inline): - """Create a test pagefor action origin tests, recording mouse coordinates + """Create a test page for action origin tests, recording mouse coordinates automatically on window.coords.""" def get_actions_origin_page(inner_style, outer_style=""): diff --git a/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py b/testing/web-platform/tests/webdriver/tests/support/fixtures_bidi.py @@ -644,6 +644,7 @@ def fetch(bidi_session, top_context, configuration): post_data=None, context=top_context, timeout_in_seconds=3, + sandbox_name=None ): if method is None: method = "GET" if post_data is None else "POST" @@ -683,7 +684,7 @@ def fetch(bidi_session, top_context, configuration): # Wait for fetch() to resolve a response and for response.text() to # resolve as well to make sure the request/response is completed when # the helper returns. - await bidi_session.script.evaluate( + return await bidi_session.script.evaluate( expression=f""" {{ const controller = new AbortController(); @@ -695,7 +696,7 @@ def fetch(bidi_session, top_context, configuration): signal: controller.signal, }}).then(response => response.text()); }}""", - target=ContextTarget(context["context"]), + target=ContextTarget(context["context"], sandbox=sandbox_name), await_promise=True, ) @@ -1152,3 +1153,29 @@ async def assert_file_dialog_not_canceled(bidi_session, inline, top_context, cancel_event_future.cancel() yield assert_file_dialog_not_canceled + + +@pytest_asyncio.fixture +async def get_fetch_headers(url, fetch): + async def get_fetch_headers(context, sandbox_name=None): + result = await fetch( + url=url("webdriver/tests/support/http_handlers/headers_echo.py"), + context=context, + sandbox_name=sandbox_name + ) + + return (json.JSONDecoder().decode(result["value"]))["headers"] + + return get_fetch_headers + + +@pytest_asyncio.fixture +async def assert_header_present(get_fetch_headers): + async def assert_header_present(context, header_name, header_value, sandbox_name=None): + actual_headers = await get_fetch_headers(context, sandbox_name) + assert header_name in actual_headers, \ + f"header '{header_name}' should be present" + assert [header_value] == actual_headers[header_name], \ + f"header '{header_name}' should have value '{header_value}'" + + return assert_header_present diff --git a/toolkit/components/formautofill/shared/HeuristicsRegExp.sys.mjs b/toolkit/components/formautofill/shared/HeuristicsRegExp.sys.mjs @@ -555,7 +555,7 @@ export const HeuristicsRegExp = { "|vorname" + // de-DE "|nombre" + // es "|forename|prénom|prenom" + // fr-FR - "|名" + // ja-JP + "|(^|[^\\p{L}\\p{N}])名([^\\p{L}\\p{N}]|$)" + // ja-JP "|nome" + // pt-BR, pt-PT "|Имя" + // ru "|نام" + // fa @@ -692,7 +692,14 @@ export const HeuristicsRegExp = { // lower-cased field name and get a rough equivalent of a case-insensitive // match. This avoids a performance cliff with the "iu" flag on regular // expressions. - regexps.push(`(${set[name].toLowerCase()})`.normalize("NFKC")); + let pattern = `(${set[name].toLowerCase()})`.normalize("NFKC"); + + // We should not lower case the \p{L} & \p{N} parts of the pattern, + // revert them back. + pattern = pattern.replaceAll("\\p{l}", "\\p{L}"); + pattern = pattern.replaceAll("\\p{n}", "\\p{N}"); + + regexps.push(pattern); } }); diff --git a/tools/profiler/core/platform.cpp b/tools/profiler/core/platform.cpp @@ -3363,10 +3363,9 @@ static void MaybeWriteRawStartTimeValue(SpliceableJSONWriter& aWriter, #endif #ifdef XP_WIN - Maybe<uint64_t> startTimeQPC = aStartTime.RawQueryPerformanceCounterValue(); - if (startTimeQPC) - aWriter.DoubleProperty("startTimeAsQueryPerformanceCounterValue", - static_cast<double>(*startTimeQPC)); + uint64_t startTimeQPC = aStartTime.RawQueryPerformanceCounterValue(); + aWriter.DoubleProperty("startTimeAsQueryPerformanceCounterValue", + static_cast<double>(startTimeQPC)); #endif } diff --git a/tools/profiler/public/ETWTools.h b/tools/profiler/public/ETWTools.h @@ -247,14 +247,8 @@ static inline void CreateDataDescForPayloadNonPOD( static inline void CreateDataDescForPayloadNonPOD( PayloadBuffer& aBuffer, EVENT_DATA_DESCRIPTOR& aDescriptor, const mozilla::TimeStamp& aPayload) { - if (aPayload.RawQueryPerformanceCounterValue().isNothing()) { - // This should never happen? - EventDataDescCreate(&aDescriptor, nullptr, 0); - return; - } - - CreateDataDescForPayloadPOD( - aBuffer, aDescriptor, aPayload.RawQueryPerformanceCounterValue().value()); + CreateDataDescForPayloadPOD(aBuffer, aDescriptor, + aPayload.RawQueryPerformanceCounterValue()); } static inline void CreateDataDescForPayloadNonPOD( @@ -308,13 +302,13 @@ static inline void StoreBaseEventDataDesc( const mozilla::MarkerOptions& aOptions) { if (aOptions.IsTimingUnspecified()) { aStorage.mStartTime = - mozilla::TimeStamp::Now().RawQueryPerformanceCounterValue().value(); + mozilla::TimeStamp::Now().RawQueryPerformanceCounterValue(); aStorage.mPhase = 0; } else { aStorage.mStartTime = - aOptions.Timing().StartTime().RawQueryPerformanceCounterValue().value(); + aOptions.Timing().StartTime().RawQueryPerformanceCounterValue(); aStorage.mEndTime = - aOptions.Timing().EndTime().RawQueryPerformanceCounterValue().value(); + aOptions.Timing().EndTime().RawQueryPerformanceCounterValue(); aStorage.mPhase = uint8_t(aOptions.Timing().MarkerPhase()); } if (!aOptions.InnerWindowId().IsUnspecified()) { diff --git a/view/nsView.cpp b/view/nsView.cpp @@ -187,14 +187,16 @@ struct WidgetViewBounds { int32_t mRoundTo = 1; }; -static WidgetViewBounds CalcWidgetViewBounds( - const nsRect& aBounds, int32_t aAppUnitsPerDevPixel, nsView* aParentView, - nsIWidget* aThisWidget, WindowType aType, TransparencyMode aTransparency) { +static WidgetViewBounds CalcWidgetViewBounds(const nsRect& aBounds, + int32_t aAppUnitsPerDevPixel, + nsIFrame* aParentFrame, + nsIWidget* aThisWidget, + WindowType aType) { nsRect viewBounds(aBounds); nsIWidget* parentWidget = nullptr; - if (aParentView) { + if (aParentFrame) { nsPoint offset; - parentWidget = aParentView->GetNearestWidget(&offset, aAppUnitsPerDevPixel); + parentWidget = aParentFrame->GetNearestWidget(offset); // make viewBounds be relative to the parent widget, in appunits viewBounds += offset; @@ -237,8 +239,9 @@ static LayoutDeviceIntRect WidgetViewBoundsToDevicePixels( LayoutDeviceIntRect nsView::CalcWidgetBounds(WindowType aType, TransparencyMode aTransparency) { int32_t p2a = mViewManager->AppUnitsPerDevPixel(); - auto viewBounds = CalcWidgetViewBounds(mDimBounds, p2a, GetParent(), - mWindow.get(), aType, aTransparency); + auto viewBounds = CalcWidgetViewBounds( + mDimBounds, p2a, GetParent() ? GetParent()->GetFrame() : nullptr, + mWindow.get(), aType); auto newBounds = WidgetViewBoundsToDevicePixels(viewBounds, p2a, aType, aTransparency); @@ -258,11 +261,10 @@ LayoutDeviceIntRect nsView::CalcWidgetBounds(WindowType aType, } LayoutDeviceIntRect nsView::CalcWidgetBounds( - const nsRect& aBounds, int32_t aAppUnitsPerDevPixel, nsView* aParentView, + const nsRect& aBounds, int32_t aAppUnitsPerDevPixel, nsIFrame* aParentFrame, nsIWidget* aThisWidget, WindowType aType, TransparencyMode aTransparency) { - auto viewBounds = - CalcWidgetViewBounds(aBounds, aAppUnitsPerDevPixel, aParentView, - aThisWidget, aType, aTransparency); + auto viewBounds = CalcWidgetViewBounds(aBounds, aAppUnitsPerDevPixel, + aParentFrame, aThisWidget, aType); return WidgetViewBoundsToDevicePixels(viewBounds, aAppUnitsPerDevPixel, aType, aTransparency); } @@ -310,16 +312,6 @@ void nsView::SetVisibility(ViewVisibility aVisibility) { NotifyEffectiveVisibilityChanged(IsEffectivelyVisible()); } -void nsView::InvalidateHierarchy() { - if (mViewManager->GetRootView() == this) { - mViewManager->InvalidateHierarchy(); - } - - for (nsView* child = mFirstChild; child; child = child->GetNextSibling()) { - child->InvalidateHierarchy(); - } -} - void nsView::InsertChild(nsView* aChild, nsView* aSibling) { MOZ_ASSERT(nullptr != aChild, "null ptr"); @@ -337,14 +329,6 @@ void nsView::InsertChild(nsView* aChild, nsView* aSibling) { mFirstChild = aChild; } aChild->SetParent(this); - - // If we just inserted a root view, then update the RootViewManager - // on all view managers in the new subtree. - - nsViewManager* vm = aChild->GetViewManager(); - if (vm->GetRootView() == aChild) { - aChild->InvalidateHierarchy(); - } } } @@ -370,14 +354,6 @@ void nsView::RemoveChild(nsView* child) { kid = kid->GetNextSibling(); } NS_ASSERTION(found, "tried to remove non child"); - - // If we just removed a root view, then update the RootViewManager - // on all view managers in the removed subtree. - - nsViewManager* vm = child->GetViewManager(); - if (vm->GetRootView() == child) { - child->InvalidateHierarchy(); - } } } @@ -546,32 +522,6 @@ nsPoint nsView::GetOffsetTo(const nsView* aOther, const int32_t aAPD) const { return offset; } -nsPoint nsView::GetOffsetToWidget(nsIWidget* aWidget) const { - nsPoint pt; - // Get the view for widget - nsView* widgetView = GetViewFor(aWidget); - if (!widgetView) { - return pt; - } - - // Get the offset to the widget view in the widget view's APD - // We get the offset in the widget view's APD first and then convert to our - // APD afterwards so that we can include the widget view's ViewToWidgetOffset - // in the sum in its native APD, and then convert the whole thing to our APD - // so that we don't have to convert the APD of the relatively small - // ViewToWidgetOffset by itself with a potentially large relative rounding - // error. - pt = -widgetView->GetOffsetTo(this); - // Add in the offset to the widget. - pt += widgetView->ViewToWidgetOffset(); - - // Convert to our appunits. - int32_t widgetAPD = widgetView->GetViewManager()->AppUnitsPerDevPixel(); - int32_t ourAPD = GetViewManager()->AppUnitsPerDevPixel(); - pt = pt.ScaleToOtherAppUnits(widgetAPD, ourAPD); - return pt; -} - nsIWidget* nsView::GetNearestWidget(nsPoint* aOffset) const { return GetNearestWidget(aOffset, GetViewManager()->AppUnitsPerDevPixel()); } diff --git a/view/nsView.h b/view/nsView.h @@ -205,15 +205,6 @@ class nsView final : public nsIWidgetListener { nsPoint GetOffsetTo(const nsView* aOther) const; /** - * Get the offset between the origin of |this| and the origin of aWidget. - * Adding the return value to a point in the coordinate system of |this| - * will transform the point to the coordinate system of aWidget. - * - * The offset is expressed in appunits of |this|. - */ - nsPoint GetOffsetToWidget(nsIWidget* aWidget) const; - - /** * Called to query the visibility state of a view. * @result current visibility state */ @@ -340,9 +331,9 @@ class nsView final : public nsIWidgetListener { bool IsRoot() const; static LayoutDeviceIntRect CalcWidgetBounds( - const nsRect& aBounds, int32_t aAppUnitsPerDevPixel, nsView* aParentView, - nsIWidget* aThisWidget, mozilla::widget::WindowType, - mozilla::widget::TransparencyMode); + const nsRect& aBounds, int32_t aAppUnitsPerDevPixel, + nsIFrame* aParentFrame, nsIWidget* aThisWidget, + mozilla::widget::WindowType, mozilla::widget::TransparencyMode); LayoutDeviceIntRect CalcWidgetBounds(mozilla::widget::WindowType, mozilla::widget::TransparencyMode); @@ -440,9 +431,6 @@ class nsView final : public nsIWidgetListener { void NotifyEffectiveVisibilityChanged(bool aEffectivelyVisible); - // Update the cached RootViewManager for all view manager descendents. - void InvalidateHierarchy(); - void CallOnAllRemoteChildren( const std::function<mozilla::CallState(mozilla::dom::BrowserParent*)>& aCallback); diff --git a/view/nsViewManager.cpp b/view/nsViewManager.cpp @@ -69,13 +69,10 @@ nsViewManager::~nsViewManager() { mRootView = nullptr; } - mRootViewManager = nullptr; - MOZ_RELEASE_ASSERT(!mPresShell, "Releasing nsViewManager without having called Destroy on " "the PresShell!"); } - nsView* nsViewManager::CreateView(const nsRect& aBounds, nsView* aParent, ViewVisibility aVisibilityFlag) { auto* v = new nsView(this, aVisibilityFlag); @@ -93,18 +90,6 @@ void nsViewManager::SetRootView(nsView* aView) { // Do NOT destroy the current root view. It's the caller's responsibility // to destroy it mRootView = aView; - - if (mRootView) { - nsView* parent = mRootView->GetParent(); - if (parent) { - // Calling InsertChild on |parent| will InvalidateHierarchy() on us, so - // no need to set mRootViewManager ourselves here. - parent->InsertChild(mRootView, nullptr); - } else { - InvalidateHierarchy(); - } - } - // Else don't touch mRootViewManager } void nsViewManager::GetWindowDimensions(nscoord* aWidth, nscoord* aHeight) { @@ -214,6 +199,25 @@ nsView* nsViewManager::GetDisplayRootFor(nsView* aView) { } } +nsViewManager* nsViewManager::RootViewManager() const { + const auto* cur = this; + while (auto* parent = cur->GetParentViewManager()) { + cur = parent; + } + return const_cast<nsViewManager*>(cur); +} + +nsViewManager* nsViewManager::GetParentViewManager() const { + if (!mPresShell) { + return nullptr; + } + auto* pc = mPresShell->GetPresContext(); + if (auto* parent = pc->GetParentPresContext()) { + return parent->PresShell()->GetViewManager(); + } + return nullptr; +} + /** aRegion is given in device coordinates!! aContext may be null, in which case layers should be used for @@ -529,7 +533,6 @@ bool nsViewManager::PaintWindow(nsIWidget* aWidget, if (!aWidget) { return false; } - // Get the view pointer here since NS_WILL_PAINT might have // destroyed it during CallWillPaintOnObservers (bug 378273). nsView* view = nsView::GetViewFor(aWidget); @@ -706,19 +709,6 @@ bool nsViewManager::IsViewInserted(nsView* aView) { return false; } -nsIWidget* nsViewManager::GetRootWidget() const { - if (!mRootView) { - return nullptr; - } - if (mRootView->HasWidget()) { - return mRootView->GetWidget(); - } - if (mRootView->GetParent()) { - return mRootView->GetParent()->GetViewManager()->GetRootWidget(); - } - return nullptr; -} - LayoutDeviceIntRect nsViewManager::ViewToWidget(nsView* aView, const nsRect& aRect) const { NS_ASSERTION(aView->GetViewManager() == this, "wrong view manager"); @@ -795,16 +785,3 @@ void nsViewManager::CallWillPaintOnObservers() { } } } - -void nsViewManager::InvalidateHierarchy() { - if (mRootView) { - mRootViewManager = nullptr; - nsView* parent = mRootView->GetParent(); - if (parent) { - mRootViewManager = parent->GetViewManager()->RootViewManager(); - NS_ASSERTION(mRootViewManager != this, - "Root view had a parent, but it has the same view manager"); - } - // else, we are implicitly our own root view manager. - } -} diff --git a/view/nsViewManager.h b/view/nsViewManager.h @@ -204,12 +204,6 @@ class nsViewManager final { public: /** - * Retrieve the widget at the root of the nearest enclosing - * view manager whose root view has a widget. - */ - nsIWidget* GetRootWidget() const; - - /** * Indicate whether the viewmanager is currently painting * * @param aPainting true if the viewmanager is painting @@ -248,8 +242,6 @@ class nsViewManager final { private: static uint32_t gLastUserEventTime; - /* Update the cached RootViewManager pointer on this view manager. */ - void InvalidateHierarchy(); void FlushPendingInvalidates(); MOZ_CAN_RUN_SCRIPT @@ -296,11 +288,9 @@ class nsViewManager final { void SetPainting(bool aPainting) { RootViewManager()->mPainting = aPainting; } - nsViewManager* RootViewManager() const { - return mRootViewManager ? mRootViewManager.get() - : const_cast<nsViewManager*>(this); - } - bool IsRootVM() const { return !mRootViewManager; } + nsViewManager* RootViewManager() const; + nsViewManager* GetParentViewManager() const; + bool IsRootVM() const { return !GetParentViewManager(); } MOZ_CAN_RUN_SCRIPT void WillPaintWindow(nsIWidget* aWidget); MOZ_CAN_RUN_SCRIPT @@ -320,12 +310,6 @@ class nsViewManager final { nsView* mRootView; - // mRootViewManager is a strong reference to the root view manager, unless - // |this| is the root, in which case mRootViewManager is null. Callers - // should use RootViewManager() (which handles that case) rather than using - // mRootViewManager directly. - RefPtr<nsViewManager> mRootViewManager; - // The following members should not be accessed directly except by // the root view manager. Some have accessor functions to enforce // this, as noted. diff --git a/widget/PuppetWidget.cpp b/widget/PuppetWidget.cpp @@ -26,9 +26,9 @@ #include "mozilla/TextEvents.h" #include "PuppetWidget.h" #include "nsContentUtils.h" +#include "nsView.h" #include "nsIWidgetListener.h" #include "imgIContainer.h" -#include "nsView.h" #include "nsXPLookAndFeel.h" #include "nsPrintfCString.h" diff --git a/widget/gtk/nsWindow.cpp b/widget/gtk/nsWindow.cpp @@ -4330,7 +4330,11 @@ gboolean nsWindow::OnShellConfigureEvent(GdkEventConfigure* aEvent) { return FALSE; } - SchedulePendingBounds(MayChangeCsdMargin::No); + // X11 calc bounds from outer window while Wayland uses + // container size after container allocation event. + if (GdkIsX11Display()) { + SchedulePendingBounds(MayChangeCsdMargin::No); + } return FALSE; } diff --git a/xpcom/ds/nsTArray.h b/xpcom/ds/nsTArray.h @@ -767,17 +767,17 @@ namespace detail { // called as a function with two instances of our element type, returns an int, // we treat it as a tri-state comparator. // -// T is the type of the comparator object we want to check. U is the array -// element type that we'll be comparing. +// T is the type of the comparator object we want to check. L and R are the +// types that we'll be comparing. // // V is never passed, and is only used to allow us to specialize on the return // value of the comparator function. -template <typename T, typename U, typename V = int> +template <typename T, typename L, typename R, typename V = int> struct IsCompareMethod : std::false_type {}; -template <typename T, typename U> +template <typename T, typename L, typename R> struct IsCompareMethod< - T, U, decltype(std::declval<T>()(std::declval<U>(), std::declval<U>()))> + T, L, R, decltype(std::declval<T>()(std::declval<L>(), std::declval<R>()))> : std::true_type {}; // These two wrappers allow us to use either a tri-state comparator, or an @@ -792,7 +792,8 @@ struct IsCompareMethod< // purpose. // Comparator wrapper for a tri-state comparator function -template <typename T, typename U, bool IsCompare = IsCompareMethod<T, U>::value> +template <typename T, typename L, typename R, + bool IsCompare = IsCompareMethod<T, L, R>::value> struct CompareWrapper { #ifdef _MSC_VER # pragma warning(push) @@ -824,8 +825,8 @@ struct CompareWrapper { }; // Comparator wrapper for a class with Equals() and LessThan() methods. -template <typename T, typename U> -struct CompareWrapper<T, U, false> { +template <typename T, typename L, typename R> +struct CompareWrapper<T, L, R, false> { MOZ_IMPLICIT CompareWrapper(const T& aComparator) : mComparator(aComparator) {} @@ -1221,7 +1222,7 @@ class nsTArray_Impl template <class Item, class Comparator> [[nodiscard]] index_type IndexOf(const Item& aItem, index_type aStart, const Comparator& aComp) const { - ::detail::CompareWrapper<Comparator, Item> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, Item> comp(aComp); const value_type* iter = Elements() + aStart; const value_type* iend = Elements() + Length(); @@ -1255,7 +1256,7 @@ class nsTArray_Impl template <class Item, class Comparator> [[nodiscard]] index_type LastIndexOf(const Item& aItem, index_type aStart, const Comparator& aComp) const { - ::detail::CompareWrapper<Comparator, Item> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, Item> comp(aComp); size_type endOffset = aStart >= Length() ? Length() : aStart + 1; const value_type* iend = Elements() - 1; @@ -1292,7 +1293,7 @@ class nsTArray_Impl [[nodiscard]] index_type BinaryIndexOf(const Item& aItem, const Comparator& aComp) const { using mozilla::BinarySearchIf; - ::detail::CompareWrapper<Comparator, Item> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, Item> comp(aComp); size_t index; bool found = BinarySearchIf( @@ -1537,7 +1538,7 @@ class nsTArray_Impl [[nodiscard]] index_type IndexOfFirstElementGt( const Item& aItem, const Comparator& aComp) const { using mozilla::BinarySearchIf; - ::detail::CompareWrapper<Comparator, Item> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, Item> comp(aComp); size_t index; BinarySearchIf( @@ -2014,7 +2015,7 @@ class nsTArray_Impl typename mozilla::FunctionTypeTraits<FunctionElse>::ReturnType>, "ApplyIf's `Function` and `FunctionElse` must return the same type."); - ::detail::CompareWrapper<Comparator, Item> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, Item> comp(aComp); const value_type* const elements = Elements(); const value_type* const iend = elements + Length(); @@ -2035,7 +2036,7 @@ class nsTArray_Impl typename mozilla::FunctionTypeTraits<FunctionElse>::ReturnType>, "ApplyIf's `Function` and `FunctionElse` must return the same type."); - ::detail::CompareWrapper<Comparator, Item> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, Item> comp(aComp); value_type* const elements = Elements(); value_type* const iend = elements + Length(); @@ -2245,7 +2246,7 @@ class nsTArray_Impl static_assert(std::is_move_assignable_v<value_type>); static_assert(std::is_move_constructible_v<value_type>); - ::detail::CompareWrapper<Comparator, value_type> comp(aComp); + ::detail::CompareWrapper<Comparator, value_type, value_type> comp(aComp); auto compFn = [&comp](const auto& left, const auto& right) { return comp.LessThan(left, right); }; @@ -2271,7 +2272,8 @@ class nsTArray_Impl static_assert(std::is_move_assignable_v<value_type>); static_assert(std::is_move_constructible_v<value_type>); - const ::detail::CompareWrapper<Comparator, value_type> comp(aComp); + const ::detail::CompareWrapper<Comparator, value_type, value_type> comp( + aComp); auto compFn = [&comp](const auto& lhs, const auto& rhs) { return comp.LessThan(lhs, rhs); }; diff --git a/xpcom/tests/gtest/TestTArray2.cpp b/xpcom/tests/gtest/TestTArray2.cpp @@ -1448,8 +1448,9 @@ TEST(TArray, test_SetLengthAndRetainStorage_no_ctor) #undef RPAREN } -template <typename Comparator> -bool TestCompareMethods(const Comparator& aComp) { +template <typename F, typename Comparator, typename ItemComparator> +bool TestCompareMethodsImpl(F aItemCreator, const Comparator& aComp, + const ItemComparator& aItemComp) { nsTArray<int> ary({57, 4, 16, 17, 3, 5, 96, 12}); ary.Sort(aComp); @@ -1461,31 +1462,43 @@ bool TestCompareMethods(const Comparator& aComp) { } } - if (!ary.ContainsSorted(5, aComp)) { + if (!ary.ContainsSorted(aItemCreator(5), aItemComp)) { return false; } - if (ary.ContainsSorted(42, aComp)) { + if (ary.ContainsSorted(aItemCreator(42), aItemComp)) { return false; } - if (ary.BinaryIndexOf(16, aComp) != 4) { + if (ary.BinaryIndexOf(aItemCreator(16), aItemComp) != 4) { return false; } return true; } +template <typename Comparator> +bool TestCompareMethods(const Comparator& aComp) { + return TestCompareMethodsImpl([](int aI) { return aI; }, aComp, aComp); +} + struct IntComparator { bool Equals(int aLeft, int aRight) const { return aLeft == aRight; } bool LessThan(int aLeft, int aRight) const { return aLeft < aRight; } }; +struct IntWrapper { + int mI; +}; + TEST(TArray, test_comparator_objects) { ASSERT_TRUE(TestCompareMethods(IntComparator())); ASSERT_TRUE( TestCompareMethods([](int aLeft, int aRight) { return aLeft - aRight; })); + ASSERT_TRUE(TestCompareMethodsImpl( + [](int aI) { return IntWrapper{.mI = aI}; }, IntComparator(), + [](int aElem, const IntWrapper& aItem) { return aElem - aItem.mI; })); } struct Big { diff --git a/xpcom/threads/TimerThread.cpp b/xpcom/threads/TimerThread.cpp @@ -1230,25 +1230,25 @@ void TimerThread::PostTimerEvent(Entry& aPostMe) { // event, so we can avoid firing a timer that was re-initialized after being // canceled. - nsCOMPtr<nsIEventTarget> target = timer->mEventTarget; - void* p = nsTimerEvent::operator new(sizeof(nsTimerEvent)); if (!p) { return; } - RefPtr<nsTimerEvent> event = ::new (KnownNotNull, p) - nsTimerEvent(timer.forget(), aPostMe.mTimerSeq, mProfilerThreadId); + // We need to release mMonitor around the Dispatch because if the Dispatch + // or any Release of our objects interacts with the timer API we'll deadlock. + + nsCOMPtr<nsIEventTarget> lockedTargetPtr = timer->mEventTarget; + RefPtr<nsTimerEvent> lockedEventPtr = ::new (KnownNotNull, p) + nsTimerEvent(timer.forget(), aPostMe.mTimerSeq, mProfilerThreadId); { - // We release mMonitor around the Dispatch because if the Dispatch interacts - // with the timer API we'll deadlock. MonitorAutoUnlock unlock(mMonitor); - if (NS_WARN_IF(NS_FAILED(target->Dispatch(event, NS_DISPATCH_NORMAL)))) { - // Dispatch may fail for an already shut down target. In that case - // we can't do much about it but drop the timer. We already removed - // its reference from our book-keeping, anyways. - RefPtr<nsTimerImpl> dropMe = event->ForgetTimer(); - } + // Ensure references are released while we're unlocked. + nsCOMPtr<nsIEventTarget> target = lockedTargetPtr.forget(); + RefPtr<nsTimerEvent> event = lockedEventPtr.forget(); + // If we fail we have no way to report an error, but fallible dispatch + // will take care of releasing our event and timer. + target->Dispatch(event.forget(), NS_DISPATCH_FALLIBLE); } }