tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit 4903e6275b2f9d65ad17d1dbb9b367ac2415955e
parent 0974467706f2067587546c466e799d484eed8dd9
Author: Greg Tatum <tatum.creative@gmail.com>
Date:   Thu, 30 Oct 2025 19:45:48 +0000

Bug 1996291 - Rework getStatus to getStatusByEngineId and fix status reporting in about:inference r=firefox-ai-ml-reviewers,tarek

When going through the tests I could not figure out what this code did.
After some sleuthing I realized that it was gathering information for
the about:inference. However, the code was badly broken, and incorrect
in what it was doing. I simplified the implementation a bit, and fixed
the breakage in about:inference.

There are no tests for this behavior in about:inference, and there as
far as I can tell there are no live models in about:inference that could
be used to test this behavior. Since it's a developer-only facing
feature, I'm not planning on adding a test for it.

Also please note that I'm adding an ml.d.ts file here to centralize some
shared types. See the in-flight TypeScript documentation here:
https://phabricator.services.mozilla.com/D268571

Differential Revision: https://phabricator.services.mozilla.com/D269982

Diffstat:
Mtoolkit/components/aboutinference/content/aboutInference.js | 40+++++++++++++++++++++++++---------------
Mtoolkit/components/ml/actors/MLEngineChild.sys.mjs | 52+++++++++++++++++++++++++++-------------------------
Mtoolkit/components/ml/actors/MLEngineParent.sys.mjs | 23+++++++++++++++--------
Atoolkit/components/ml/ml.d.ts | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/components/ml/tests/browser/browser_ml_engine_lifetime.js | 19+++++++++++--------
5 files changed, 138 insertions(+), 56 deletions(-)

diff --git a/toolkit/components/aboutinference/content/aboutInference.js b/toolkit/components/aboutinference/content/aboutInference.js @@ -6,6 +6,11 @@ "use strict"; /** + * @import { MLEngineParent } from "resource://gre/actors/MLEngineParent.sys.mjs" + * @import { StatusByEngineId } from "../../ml/ml.d.ts" + */ + +/** * Imports necessary modules from ChromeUtils. */ const lazy = {}; @@ -75,6 +80,7 @@ function getNumThreadsArray() { ); } +/** @type {MLEngineParent | null} */ let engineParent = null; const TINY_ARTICLE = @@ -460,25 +466,26 @@ function ts2str(ts) { */ async function updateStatus() { - if (!engineParent) { - return; - } + const engineParent = await getEngineParent(); - let info; + /** + * @type {StatusByEngineId} + */ + let statusByEngineId; // Fetch the engine status info try { - info = await engineParent.getStatus(); - } catch (e) { - engineParent = null; // let's re-create it on errors. - info = new Map(); + statusByEngineId = await engineParent.getStatusByEngineId(); + } catch (error) { + console.error("Failed to get the engine status", error); + statusByEngineId = new Map(); } // Get the container where the table will be displayed let tableContainer = document.getElementById("statusTableContainer"); // Clear the container if the map is empty - if (info.size === 0) { + if (statusByEngineId.size === 0) { tableContainer.innerHTML = ""; // Clear any existing table if (updateStatusInterval) { clearInterval(updateStatusInterval); // Clear the interval if it exists @@ -520,7 +527,7 @@ async function updateStatus() { let tbody = document.createElement("tbody"); // Iterate over the info map - for (let [engineId, engineInfo] of info.entries()) { + for (let [engineId, { status, options }] of statusByEngineId.entries()) { let row = document.createElement("tr"); // Create a cell for each piece of data @@ -529,23 +536,23 @@ async function updateStatus() { row.appendChild(engineIdCell); let statusCell = document.createElement("td"); - statusCell.textContent = engineInfo.status; + statusCell.textContent = status; row.appendChild(statusCell); let modelIdCell = document.createElement("td"); - modelIdCell.textContent = engineInfo.options?.modelId || "N/A"; + modelIdCell.textContent = options?.modelId || "N/A"; row.appendChild(modelIdCell); let dtypeCell = document.createElement("td"); - dtypeCell.textContent = engineInfo.options?.dtype || "N/A"; + dtypeCell.textContent = options?.dtype || "N/A"; row.appendChild(dtypeCell); let deviceCell = document.createElement("td"); - deviceCell.textContent = engineInfo.options?.device || "N/A"; + deviceCell.textContent = options?.device || "N/A"; row.appendChild(deviceCell); let timeoutCell = document.createElement("td"); - timeoutCell.textContent = engineInfo.options?.timeoutMS || "N/A"; + timeoutCell.textContent = options?.timeoutMS || "N/A"; row.appendChild(timeoutCell); // Append the row to the table body @@ -1133,6 +1140,9 @@ function showTab(button) { button.setAttribute("selected", "true"); } +/** + * @returns {Promise<MLEngineParent>} + */ async function getEngineParent() { if (!engineParent) { engineParent = await EngineProcess.getMLEngineParent(); diff --git a/toolkit/components/ml/actors/MLEngineChild.sys.mjs b/toolkit/components/ml/actors/MLEngineChild.sys.mjs @@ -9,6 +9,7 @@ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; /** * @import { BasePromiseWorker } from "resource://gre/modules/PromiseWorker.sys.mjs" * @import { PipelineOptions } from "chrome://global/content/ml/EngineProcess.sys.mjs" + * @import { EngineStatus, EngineId, StatusByEngineId } from "../ml.d.ts" * @import { ProgressAndStatusCallbackParams } from "chrome://global/content/ml/Utils.sys.mjs" */ @@ -62,11 +63,11 @@ export class MLEngineChild extends JSProcessActorChild { #engineDispatchers = new Map(); /** - * Engine statuses + * Tracks that an engine is present, even if the dispatcher is not present yet. * - * @type {Map<string, string>} + * @type {Map<EngineId, PipelineOptions>} */ - #engineStatuses = new Map(); + #enginesPresent = new Map(); // eslint-disable-next-line consistent-return async receiveMessage({ name, data }) { @@ -75,8 +76,8 @@ export class MLEngineChild extends JSProcessActorChild { await this.#onNewPortCreated(data); break; } - case "MLEngine:GetStatus": { - return this.getStatus(); + case "MLEngine:GetStatusByEngineId": { + return this.getStatusByEngineId(); } case "MLEngine:ForceShutdown": { for (const engineDispatcher of this.#engineDispatchers.values()) { @@ -114,7 +115,7 @@ export class MLEngineChild extends JSProcessActorChild { this.getUpdatedPipelineOptions(pipelineOptions); options.updateOptions(updatedPipelineOptions); const engineId = options.engineId; - this.#engineStatuses.set(engineId, "INITIALIZING"); + this.#enginesPresent.set(engineId, options); // Check if we already have an engine under this id. if (this.#engineDispatchers.has(engineId)) { @@ -126,8 +127,6 @@ export class MLEngineChild extends JSProcessActorChild { type: "EnginePort:EngineReady", error: null, }); - this.#engineStatuses.set(engineId, "READY"); - return; } @@ -139,8 +138,6 @@ export class MLEngineChild extends JSProcessActorChild { this.#engineDispatchers.delete(engineId); } - this.#engineStatuses.set(engineId, "CREATING"); - const dispatcher = new EngineDispatcher(this, port, options); this.#engineDispatchers.set(engineId, dispatcher); @@ -154,7 +151,6 @@ export class MLEngineChild extends JSProcessActorChild { await dispatcher.isReady(); } - this.#engineStatuses.set(engineId, "READY"); port.postMessage({ type: "EnginePort:EngineReady", error: null, @@ -238,7 +234,7 @@ export class MLEngineChild extends JSProcessActorChild { */ removeEngine(engineId, shutDownIfEmpty, replacement) { this.#engineDispatchers.delete(engineId); - this.#engineStatuses.delete(engineId); + this.#enginesPresent.delete(engineId); try { this.sendAsyncMessage("MLEngine:Removed", { @@ -262,19 +258,26 @@ export class MLEngineChild extends JSProcessActorChild { } } - /* + /** * Collects information about the current status. + * + * @returns {StatusByEngineId} */ - async getStatus() { + getStatusByEngineId() { + /** @type {StatusByEngineId} */ const statusMap = new Map(); - for (const [key, value] of this.#engineStatuses) { - if (this.#engineDispatchers.has(key)) { - statusMap.set(key, this.#engineDispatchers.get(key).getStatus()); - } else { - // The engine is probably being created - statusMap.set(key, { status: value }); + for (let [engineId, options] of this.#enginesPresent) { + const dispatcher = this.#engineDispatchers.get(engineId); + let status = dispatcher.getStatus(); + if (!status) { + // This engine doesn't have a dispatcher yet. + status = { + status: "SHUTTING_DOWN_PREVIOUS_ENGINE", + options, + }; } + statusMap.set(engineId, status); } return statusMap; } @@ -329,7 +332,7 @@ class EngineDispatcher { /** @type {PipelineOptions | null} */ pipelineOptions = null; - /** @type {string} */ + /** @type {EngineStatus} */ #status; /** @@ -414,7 +417,7 @@ class EngineDispatcher { * @param {PipelineOptions} pipelineOptions */ constructor(mlEngineChild, port, pipelineOptions) { - this.#status = "CREATED"; + this.#status = "INITIALIZING"; /** @type {MLEngineChild} */ this.mlEngineChild = mlEngineChild; this.#featureId = pipelineOptions.featureId; @@ -431,7 +434,7 @@ class EngineDispatcher { this.#engine .then(() => { - this.#status = "READY"; + this.#status = "IDLE"; // Trigger the keep alive timer. void this.keepAlive(); }) @@ -454,7 +457,6 @@ class EngineDispatcher { return { status: this.#status, options: this.pipelineOptions, - engineId: this.#engineId, }; } @@ -583,7 +585,7 @@ class EngineDispatcher { error, }); } - this.#status = "IDLING"; + this.#status = "IDLE"; break; } default: diff --git a/toolkit/components/ml/actors/MLEngineParent.sys.mjs b/toolkit/components/ml/actors/MLEngineParent.sys.mjs @@ -3,6 +3,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; +/** + * @import { MLEngineChild } from "./MLEngineChild.sys.mjs" + */ + const lazy = XPCOMUtils.declareLazy({ RemoteSettings: "resource://services-settings/remote-settings.sys.mjs", Utils: "resource://services-settings/Utils.sys.mjs", @@ -227,10 +231,8 @@ export class MLEngineParent extends JSProcessActorParent { // Wait for the existing lock to resolve await MLEngineParent.engineLocks.get(engineId); } - let resolveLock; - const lockPromise = new Promise(resolve => { - resolveLock = resolve; - }); + const { promise: lockPromise, resolve: resolveLock } = + Promise.withResolvers(); MLEngineParent.engineLocks.set(engineId, lockPromise); MLEngineParent.engineCreationAbortSignal.set(engineId, abortSignal); try { @@ -783,10 +785,15 @@ export class MLEngineParent extends JSProcessActorParent { } /** - * Gets a status + * Goes through the engines and determines their status. This is used by about:inference + * to display debug information about the engines. + * + * @see MLEngineChild#getStatusByEngineId + * + * @returns {Promise<StatusByEngineId>} */ - getStatus() { - return this.sendQuery("MLEngine:GetStatus"); + getStatusByEngineId() { + return this.sendQuery("MLEngine:GetStatusByEngineId"); } /** @@ -941,7 +948,7 @@ export class MLEngine { #requests = new Map(); /** - * @type {"uninitialized" | "ready" | "error" | "closed"} + * @type {"uninitialized" | "ready" | "error" | "closed" | "crashed"} */ engineStatus = "uninitialized"; diff --git a/toolkit/components/ml/ml.d.ts b/toolkit/components/ml/ml.d.ts @@ -0,0 +1,60 @@ +/* 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/. */ + +/** + * This file contains the shared types for the machine learning component. The intended + * use is for defining types to be used in JSDoc. They are used in a form that the + * TypeScript language server can read them, and provide code hints. + * + * @see https://firefox-source-docs.mozilla.org/code-quality/typescript/ + */ + +import { type PipelineOptions } from "chrome://global/content/ml/EngineProcess.sys.mjs"; + +export type EngineStatus = + // The engine is waiting for a previous one to shut down. + | "SHUTTING_DOWN_PREVIOUS_ENGINE" + // The engine dispatcher has been created, and the engine is still initializing. + | "INITIALIZING" + // The engine is fully ready and idle. + | "IDLE" + // The engine is currently processing a run request. + | "RUNNING" + // The engine is in the process of terminating, but hasn't fully shut down. + | "TERMINATING" + // The engine has been fully terminated and removed. + | "TERMINATED"; + +/** + * The EngineId is used to identify a unique engine that can be shared across multiple + * consumers. This way a single model can be loaded into memory and used in different + * locations, assuming the other parameters match as well. + */ +export type EngineId = string; + +/** + * Utility type to extract the data fields from a class. It removes all of the + * functions. + */ +type DataFields<T> = { + [K in keyof T as T[K] extends Function ? never : K]: T[K]; +}; + +/** + * The PipelineOptions are a nominal class that validates the options. The + * PipelineOptionsRaw are the raw subset of those. + */ +type PipelineOptionsRaw = Partial<DataFields<PipelineOptions>>; + +/** + * Tracks the current status of the engines for about:inference. It's not used + * for deciding any business logic of the engines, only for debug info. + */ +export type StatusByEngineId = Map< + EngineId, + { + status: EngineStatus; + options: PipelineOptions | PipelineOptionsRaw; + } +>; diff --git a/toolkit/components/ml/tests/browser/browser_ml_engine_lifetime.js b/toolkit/components/ml/tests/browser/browser_ml_engine_lifetime.js @@ -435,7 +435,11 @@ add_task(async function test_ml_engine_infinite_worker() { await cleanup(); }); -add_task(async function test_ml_engine_get_status() { +/** + * These status are visualized in about:inference, but aren't used for business + * logic. + */ +add_task(async function test_ml_engine_get_status_by_engine_id() { const { cleanup, remoteClients } = await setup(); info("Get the engine"); @@ -459,7 +463,7 @@ add_task(async function test_ml_engine_get_status() { const expected = { "default-engine": { - status: "IDLING", + status: "IDLE", options: { useExternalDataFormat: false, engineId: "default-engine", @@ -496,15 +500,14 @@ add_task(async function test_ml_engine_get_status() { baseURL: null, apiKey: null, }, - engineId: "default-engine", }, }; - let status = await engineInstance.mlEngineParent.getStatus(); - status = JSON.parse(JSON.stringify(Object.fromEntries(status))); - - status["default-engine"].options.numThreads = "NOT_COMPARED"; - Assert.deepEqual(status, expected); + const statusByEngineId = Object.fromEntries( + await engineInstance.mlEngineParent.getStatusByEngineId() + ); + statusByEngineId["default-engine"].options.numThreads = "NOT_COMPARED"; + Assert.deepEqual(statusByEngineId, expected); await ok( !EngineProcess.areAllEnginesTerminated(),