Bug 1675375 Part 7: Update expectations in helper_hittest_clippath.html. r=botond
[gecko.git] / services / settings / RemoteSettingsWorker.jsm
blob147ebb6b13d67adbc8c96a8d82468d62c00707e8
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 /**
8  * Interface to a dedicated thread handling for Remote Settings heavy operations.
9  */
10 const { XPCOMUtils } = ChromeUtils.import(
11   "resource://gre/modules/XPCOMUtils.jsm"
13 const { setTimeout, clearTimeout } = ChromeUtils.import(
14   "resource://gre/modules/Timer.jsm"
17 var EXPORTED_SYMBOLS = ["RemoteSettingsWorker"];
19 XPCOMUtils.defineLazyPreferenceGetter(
20   this,
21   "gMaxIdleMilliseconds",
22   "services.settings.worker_idle_max_milliseconds",
23   30 * 1000 // Default of 30 seconds.
26 ChromeUtils.defineModuleGetter(
27   this,
28   "AsyncShutdown",
29   "resource://gre/modules/AsyncShutdown.jsm"
32 ChromeUtils.defineModuleGetter(
33   this,
34   "SharedUtils",
35   "resource://services-settings/SharedUtils.jsm"
38 // Note: we currently only ever construct one instance of Worker.
39 // If it stops being a singleton, the AsyncShutdown code at the bottom
40 // of this file, as well as these globals, will need adjusting.
41 let gShutdown = false;
42 let gShutdownResolver = null;
44 class RemoteSettingsWorkerError extends Error {
45   constructor(message) {
46     super(message);
47     this.name = "RemoteSettingsWorkerError";
48   }
51 class Worker {
52   constructor(source) {
53     if (gShutdown) {
54       Cu.reportError("Can't create worker once shutdown has started");
55     }
56     this.source = source;
57     this.worker = null;
59     this.callbacks = new Map();
60     this.lastCallbackId = 0;
61     this.idleTimeoutId = null;
62   }
64   async _execute(method, args = [], options = {}) {
65     // Check if we're shutting down.
66     if (gShutdown && method != "prepareShutdown") {
67       throw new RemoteSettingsWorkerError("Remote Settings has shut down.");
68     }
69     // Don't instantiate the worker to shut it down.
70     if (method == "prepareShutdown" && !this.worker) {
71       return null;
72     }
74     const { mustComplete = false } = options;
75     // (Re)instantiate the worker if it was terminated.
76     if (!this.worker) {
77       this.worker = new ChromeWorker(this.source);
78       this.worker.onmessage = this._onWorkerMessage.bind(this);
79       this.worker.onerror = error => {
80         // Worker crashed. Reject each pending callback.
81         for (const { reject } of this.callbacks.values()) {
82           reject(error);
83         }
84         this.callbacks.clear();
85         // And terminate it.
86         this.stop();
87       };
88     }
89     // New activity: reset the idle timer.
90     if (this.idleTimeoutId) {
91       clearTimeout(this.idleTimeoutId);
92     }
93     let identifier = method + "-";
94     // Include the collection details in the importJSONDump case.
95     if (identifier == "importJSONDump-") {
96       identifier += `${args[0]}-${args[1]}-`;
97     }
98     return new Promise((resolve, reject) => {
99       const callbackId = `${identifier}${++this.lastCallbackId}`;
100       this.callbacks.set(callbackId, { resolve, reject, mustComplete });
101       this.worker.postMessage({ callbackId, method, args });
102     });
103   }
105   _onWorkerMessage(event) {
106     const { callbackId, result, error } = event.data;
107     // If we're shutting down, we may have already rejected this operation
108     // and removed its callback from our map:
109     if (!this.callbacks.has(callbackId)) {
110       return;
111     }
112     const { resolve, reject } = this.callbacks.get(callbackId);
113     if (error) {
114       reject(new RemoteSettingsWorkerError(error));
115     } else {
116       resolve(result);
117     }
118     this.callbacks.delete(callbackId);
120     // Terminate the worker when it's unused for some time.
121     // But don't terminate it if an operation is pending.
122     if (!this.callbacks.size) {
123       if (gShutdown) {
124         this.stop();
125         if (gShutdownResolver) {
126           gShutdownResolver();
127         }
128       } else {
129         this.idleTimeoutId = setTimeout(() => {
130           this.stop();
131         }, gMaxIdleMilliseconds);
132       }
133     }
134   }
136   /**
137    * Called at shutdown to abort anything the worker is doing that isn't
138    * critical.
139    */
140   _abortCancelableRequests() {
141     // End all tasks that we can.
142     const callbackCopy = Array.from(this.callbacks.entries());
143     const error = new Error("Shutdown, aborting read-only worker requests.");
144     for (const [id, { reject, mustComplete }] of callbackCopy) {
145       if (!mustComplete) {
146         this.callbacks.delete(id);
147         reject(error);
148       }
149     }
150     // There might be nothing left now:
151     if (!this.callbacks.size) {
152       this.stop();
153       if (gShutdownResolver) {
154         gShutdownResolver();
155       }
156     }
157     // If there was something left, we'll stop as soon as we get messages from
158     // those tasks, too.
159     // Let's hurry them along a bit:
160     this._execute("prepareShutdown");
161   }
163   stop() {
164     this.worker.terminate();
165     this.worker = null;
166     this.idleTimeoutId = null;
167   }
169   async canonicalStringify(localRecords, remoteRecords, timestamp) {
170     return this._execute("canonicalStringify", [
171       localRecords,
172       remoteRecords,
173       timestamp,
174     ]);
175   }
177   async importJSONDump(bucket, collection) {
178     return this._execute("importJSONDump", [bucket, collection], {
179       mustComplete: true,
180     });
181   }
183   async checkFileHash(filepath, size, hash) {
184     return this._execute("checkFileHash", [filepath, size, hash]);
185   }
187   async checkContentHash(buffer, size, hash) {
188     // The implementation does little work on the current thread, so run the
189     // task on the current thread instead of the worker thread.
190     return SharedUtils.checkContentHash(buffer, size, hash);
191   }
194 // Now, first add a shutdown blocker. If that fails, we must have
195 // shut down already.
196 // We're doing this here rather than in the Worker constructor because in
197 // principle having just 1 shutdown blocker for the entire file should be
198 // fine. If we ever start creating more than one Worker instance, this
199 // code will need adjusting to deal with that.
200 try {
201   AsyncShutdown.profileBeforeChange.addBlocker(
202     "Remote Settings profile-before-change",
203     async () => {
204       // First, indicate we've shut down.
205       gShutdown = true;
206       // Then, if we have no worker or no callbacks, we're done.
207       if (
208         !RemoteSettingsWorker.worker ||
209         !RemoteSettingsWorker.callbacks.size
210       ) {
211         return null;
212       }
213       // Otherwise, there's something left to do. Set up a promise:
214       let finishedPromise = new Promise(resolve => {
215         gShutdownResolver = resolve;
216       });
218       // Try to cancel most of the work:
219       RemoteSettingsWorker._abortCancelableRequests();
221       // Return a promise that the worker will resolve.
222       return finishedPromise;
223     },
224     {
225       fetchState() {
226         const remainingCallbacks = RemoteSettingsWorker.callbacks;
227         const details = Array.from(remainingCallbacks.keys()).join(", ");
228         return `Remaining: ${remainingCallbacks.size} callbacks (${details}).`;
229       },
230     }
231   );
232 } catch (ex) {
233   Cu.reportError(
234     "Couldn't add shutdown blocker, assuming shutdown has started."
235   );
236   Cu.reportError(ex);
237   // If AsyncShutdown throws, `profileBeforeChange` has already fired. Ignore it
238   // and mark shutdown. Constructing the worker will report an error and do
239   // nothing.
240   gShutdown = true;
243 var RemoteSettingsWorker = new Worker(
244   "resource://services-settings/RemoteSettingsWorker.js"