Created
June 25, 2026 05:44
-
-
Save flodolo/b7dcdbc806be030d6de6ff4e1144fba7 to your computer and use it in GitHub Desktop.
Hang Firefox
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # Reproduce bug 2049845 (catastrophic AboutNewTabResourceMapping observer | |
| # re-entrancy -> ballooning memory + main-thread freeze) on a LOCAL build. | |
| # | |
| # The hang needs THREE ingredients together: | |
| # 1. newtab running as the train-hop XPI (not the built-in add-on), so the | |
| # AboutNewTabResourceMapping observer is actually installed. | |
| # 2. Many active langpacks (>=20) so there are enough locales for the | |
| # L10nRegistry/LocaleService source churn to matter. | |
| # 3. A RUNTIME locale negotiation change (switching language / live langpack | |
| # install). Startup alone does NOT trigger it: the langpacks register | |
| # before the observer is added, and the startup shadow registrations don't | |
| # re-fire intl:app-locales-changed. This script forces the change via | |
| # Marionette after launch. | |
| # | |
| # The profile is also set up with: | |
| # - intl.locale.requested empty (forces OS-locale negotiation) | |
| # - a batch of release langpacks pre-installed | |
| # - the nimbus-devtools extension installed | |
| set -euo pipefail | |
| # --- Config --------------------------------------------------------------- | |
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | |
| # Path to the Firefox source checkout (override with MOZ_REPO=...). | |
| MOZ_REPO="${MOZ_REPO:-/Users/flodolo/mozilla/git/mozilla-firefox}" | |
| LANGPACK_BASE="https://archive.mozilla.org/pub/firefox/releases/152.0.2/mac/xpi" | |
| NIMBUS_URL="https://github.com/mozilla-extensions/nimbus-devtools/releases/download/release%2Fv0.3.0/nimbus-devtools.xpi" | |
| NIMBUS_ID="nimbus-devtools@mozilla.com" | |
| NUM_LANGPACKS=40 | |
| PROFILE_DIR="${PROFILE_DIR:-/tmp/fx-langpack-profile}" | |
| # The newtab train-hop XPI. It is NOT on archive.mozilla.org (it ships via | |
| # Nimbus), so we keep a persistent copy next to this script. Override with | |
| # NEWTAB_XPI=/path/to/newtab@mozilla.org.xpi | |
| NEWTAB_XPI="${NEWTAB_XPI:-${SCRIPT_DIR}/newtab@mozilla.org.xpi}" | |
| NEWTAB_ID="newtab@mozilla.org" | |
| # Set TRIGGER_HANG=0 to just launch the profile without firing the locale switch. | |
| TRIGGER_HANG="${TRIGGER_HANG:-1}" | |
| MARIONETTE_PORT="${MARIONETTE_PORT:-2828}" | |
| # --- Preserve the newtab XPI before wiping the profile -------------------- | |
| # If we don't have a saved copy but a previous profile run does, rescue it so | |
| # the script stays self-contained across runs (the profile is recreated below). | |
| if [[ ! -f "${NEWTAB_XPI}" && -f "${PROFILE_DIR}/extensions/${NEWTAB_ID}.xpi" ]]; then | |
| echo "Preserving newtab XPI from existing profile -> ${NEWTAB_XPI}" | |
| cp "${PROFILE_DIR}/extensions/${NEWTAB_ID}.xpi" "${NEWTAB_XPI}" | |
| fi | |
| if [[ ! -f "${NEWTAB_XPI}" ]]; then | |
| echo "ERROR: newtab train-hop XPI not found at:" >&2 | |
| echo " ${NEWTAB_XPI}" >&2 | |
| echo "Drop a newtab@mozilla.org.xpi there (or set NEWTAB_XPI=...)." >&2 | |
| exit 1 | |
| fi | |
| # Read the XPI version from its manifest so the train-hop pref matches it. | |
| NEWTAB_VERSION="$(python3 -c 'import json,sys,zipfile; print(json.loads(zipfile.ZipFile(sys.argv[1]).read("manifest.json"))["version"])' "${NEWTAB_XPI}")" | |
| echo "Using newtab train-hop XPI version ${NEWTAB_VERSION}" | |
| # --- Clean profile -------------------------------------------------------- | |
| echo "Recreating clean profile at ${PROFILE_DIR}" | |
| # macOS Finder can recreate .DS_Store mid-delete, making rm -rf fail with | |
| # "Directory not empty". Retry a few times to win the race. | |
| for _ in 1 2 3 4 5; do | |
| rm -rf "${PROFILE_DIR}" 2>/dev/null && break | |
| sleep 0.3 | |
| done | |
| if [[ -d "${PROFILE_DIR}" ]]; then | |
| echo "ERROR: could not remove ${PROFILE_DIR} (something is recreating files in it)" >&2 | |
| exit 1 | |
| fi | |
| mkdir -p "${PROFILE_DIR}/extensions" | |
| # --- Pick langpacks ------------------------------------------------------- | |
| echo "Fetching langpack list..." | |
| LANGPACKS=() | |
| while IFS= read -r line; do | |
| LANGPACKS+=("$line") | |
| done < <( | |
| curl -sf "${LANGPACK_BASE}/" \ | |
| | grep -oE 'href="[^"]+/([A-Za-z-]+)\.xpi"' \ | |
| | sed -E 's#.*/([A-Za-z-]+)\.xpi"#\1#' \ | |
| | sort -u \ | |
| | head -n "${NUM_LANGPACKS}" | |
| ) | |
| if [[ "${#LANGPACKS[@]}" -lt 30 ]]; then | |
| echo "ERROR: only found ${#LANGPACKS[@]} langpacks at:" >&2 | |
| echo " ${LANGPACK_BASE}/" >&2 | |
| exit 1 | |
| fi | |
| echo "Downloading ${#LANGPACKS[@]} langpacks..." | |
| for loc in "${LANGPACKS[@]}"; do | |
| echo " ${loc}" | |
| addon_id="langpack-${loc}@firefox.mozilla.org" | |
| curl -sf -o "${PROFILE_DIR}/extensions/${addon_id}.xpi" "${LANGPACK_BASE}/${loc}.xpi" | |
| done | |
| # --- nimbus-devtools ------------------------------------------------------ | |
| echo "Downloading nimbus-devtools..." | |
| curl -sfL -o "${PROFILE_DIR}/extensions/${NIMBUS_ID}.xpi" "${NIMBUS_URL}" | |
| # --- newtab train-hop XPI ------------------------------------------------- | |
| echo "Installing newtab train-hop XPI..." | |
| cp "${NEWTAB_XPI}" "${PROFILE_DIR}/extensions/${NEWTAB_ID}.xpi" | |
| # --- Profile prefs -------------------------------------------------------- | |
| cat > "${PROFILE_DIR}/user.js" <<'EOF' | |
| // Empty requested locale -> Firefox negotiates against installed langpacks. | |
| user_pref("intl.locale.requested", ""); | |
| user_pref("intl.multilingual.enabled", true); | |
| user_pref("intl.multilingual.liveReload", true); | |
| // Auto-enable side-loaded add-ons dropped into the profile's extensions dir. | |
| user_pref("extensions.autoDisableScopes", 0); | |
| // nimbus-devtools is a privileged add-on that uses experiment APIs. | |
| user_pref("extensions.experiments.enabled", true); | |
| // Skip first-run noise. | |
| user_pref("browser.aboutwelcome.enabled", false); | |
| user_pref("datareporting.policy.dataSubmissionEnabled", false); | |
| EOF | |
| # Make newtab load from the train-hop XPI instead of the built-in add-on. | |
| # The version must be non-empty and higher than the built-in newtab version, | |
| # otherwise getPreferredMapping() falls back to the built-in resources. | |
| cat >> "${PROFILE_DIR}/user.js" <<EOF | |
| // Force newtab to run from the train-hop XPI (matches the XPI's version). | |
| user_pref("browser.newtabpage.trainhopAddon.version", "${NEWTAB_VERSION}"); | |
| EOF | |
| # --- Locate the built binary ---------------------------------------------- | |
| BIN="$(ls -td "${MOZ_REPO}"/obj-*/dist/*.app/Contents/MacOS/firefox 2>/dev/null | head -n 1 || true)" | |
| if [[ -z "${BIN}" || ! -x "${BIN}" ]]; then | |
| echo "ERROR: no built Firefox binary found under ${MOZ_REPO}/obj-*/dist/*.app" >&2 | |
| echo "Build first with: (cd ${MOZ_REPO} && ./mach build)" >&2 | |
| exit 1 | |
| fi | |
| echo "Using binary: ${BIN}" | |
| # --- Launch --------------------------------------------------------------- | |
| if [[ "${TRIGGER_HANG}" != "1" ]]; then | |
| echo "Launching local build (no auto-trigger; switch language manually to hang)..." | |
| exec env MOZ_DISABLE_CONTENT_SANDBOX=1 "${BIN}" \ | |
| -profile "${PROFILE_DIR}" -no-remote about:newtab | |
| fi | |
| echo "Launching local build with Marionette to auto-trigger the hang..." | |
| env MOZ_DISABLE_CONTENT_SANDBOX=1 "${BIN}" \ | |
| -profile "${PROFILE_DIR}" -no-remote \ | |
| -marionette -remote-allow-system-access about:newtab & | |
| FX_PID=$! | |
| # Wait for the Marionette socket to come up. | |
| echo "Waiting for Marionette on port ${MARIONETTE_PORT}..." | |
| for _ in $(seq 1 60); do | |
| if lsof -iTCP:"${MARIONETTE_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then | |
| break | |
| fi | |
| sleep 0.5 | |
| done | |
| # Give the browser a moment to finish startup (langpacks + shadow sources). | |
| sleep 5 | |
| # Drive a runtime locale negotiation change to ignite the observer cascade. | |
| TRIGGER_PY="$(mktemp -t fx-langpack-trigger.XXXXXX.py)" | |
| trap 'rm -f "${TRIGGER_PY}"' EXIT | |
| cat > "${TRIGGER_PY}" <<'PYEOF' | |
| import sys, time | |
| from marionette_driver.marionette import Marionette | |
| port = int(sys.argv[1]) | |
| m = Marionette(host="127.0.0.1", port=port, socket_timeout=20) | |
| m.start_session() | |
| m.set_context(m.CONTEXT_CHROME) | |
| available = set(m.execute_script("return Services.locale.availableLocales;")) | |
| current = m.execute_script("return Services.locale.appLocalesAsBCP47;") | |
| print("available locales:", len(available), "current app locale:", current) | |
| # Pick a few installed langpack locales to switch between. The first switch to | |
| # a different, available locale forces intl:app-locales-changed -> observe() -> | |
| # the synchronous re-entrant cascade. That execute_script will then block | |
| # (main thread pinned), so we treat a socket timeout as "hang triggered". | |
| candidates = [c for c in ["de", "fr", "it", "es-ES", "ja", "pl", "ru"] | |
| if c in available and c != (current[0] if current else None)] | |
| print("switching through:", candidates) | |
| for loc in candidates: | |
| try: | |
| print("setting requestedLocales =", [loc], flush=True) | |
| m.execute_script("Services.locale.requestedLocales = arguments[0];", | |
| script_args=[[loc]]) | |
| time.sleep(1.0) | |
| except Exception as e: | |
| print("no response after switch -> cascade triggered (browser is " | |
| "hanging):", type(e).__name__) | |
| sys.exit(0) | |
| print("fired all switches without an obvious hang; check memory/CPU") | |
| PYEOF | |
| echo "Firing runtime locale switch (this should make Firefox beachball)..." | |
| set +e | |
| "${MOZ_REPO}/mach" python "${TRIGGER_PY}" "${MARIONETTE_PORT}" | |
| set -e | |
| echo | |
| echo "Trigger fired. The browser (pid ${FX_PID}) should now be hung:" | |
| echo " - main process CPU pinned near 100%, RSS climbing past several GB" | |
| echo " - watch with: ps -o pid,rss,%cpu -p ${FX_PID}" | |
| echo "Press Ctrl-C to kill it, or run: kill -9 ${FX_PID}" | |
| wait "${FX_PID}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment