browser/components/sessionstore/src/SessionFile.jsm

changeset 0
6474c204b198
equal deleted inserted replaced
-1:000000000000 0:60cdfc60e7b3
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/. */
4
5 "use strict";
6
7 this.EXPORTED_SYMBOLS = ["SessionFile"];
8
9 /**
10 * Implementation of all the disk I/O required by the session store.
11 * This is a private API, meant to be used only by the session store.
12 * It will change. Do not use it for any other purpose.
13 *
14 * Note that this module implicitly depends on one of two things:
15 * 1. either the asynchronous file I/O system enqueues its requests
16 * and never attempts to simultaneously execute two I/O requests on
17 * the files used by this module from two distinct threads; or
18 * 2. the clients of this API are well-behaved and do not place
19 * concurrent requests to the files used by this module.
20 *
21 * Otherwise, we could encounter bugs, especially under Windows,
22 * e.g. if a request attempts to write sessionstore.js while
23 * another attempts to copy that file.
24 *
25 * This implementation uses OS.File, which guarantees property 1.
26 */
27
28 const Cu = Components.utils;
29 const Cc = Components.classes;
30 const Ci = Components.interfaces;
31 const Cr = Components.results;
32
33 Cu.import("resource://gre/modules/Services.jsm");
34 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
35 Cu.import("resource://gre/modules/osfile.jsm");
36 Cu.import("resource://gre/modules/osfile/_PromiseWorker.jsm", this);
37 Cu.import("resource://gre/modules/Promise.jsm");
38 Cu.import("resource://gre/modules/AsyncShutdown.jsm");
39
40 XPCOMUtils.defineLazyModuleGetter(this, "console",
41 "resource://gre/modules/devtools/Console.jsm");
42 XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
43 "resource://gre/modules/TelemetryStopwatch.jsm");
44 XPCOMUtils.defineLazyModuleGetter(this, "Task",
45 "resource://gre/modules/Task.jsm");
46 XPCOMUtils.defineLazyServiceGetter(this, "Telemetry",
47 "@mozilla.org/base/telemetry;1", "nsITelemetry");
48
49 this.SessionFile = {
50 /**
51 * Read the contents of the session file, asynchronously.
52 */
53 read: function () {
54 return SessionFileInternal.read();
55 },
56 /**
57 * Write the contents of the session file, asynchronously.
58 */
59 write: function (aData) {
60 return SessionFileInternal.write(aData);
61 },
62 /**
63 * Gather telemetry statistics.
64 *
65 *
66 * Most of the work is done off the main thread but there is a main
67 * thread cost involved to send data to the worker thread. This method
68 * should therefore be called only when we know that it will not disrupt
69 * the user's experience, e.g. on idle-daily.
70 *
71 * @return {Promise}
72 * @promise {object} An object holding all the information to be submitted
73 * to Telemetry.
74 */
75 gatherTelemetry: function(aData) {
76 return SessionFileInternal.gatherTelemetry(aData);
77 },
78 /**
79 * Create a backup copy, asynchronously.
80 * This is designed to perform backup on upgrade.
81 */
82 createBackupCopy: function (ext) {
83 return SessionFileInternal.createBackupCopy(ext);
84 },
85 /**
86 * Remove a backup copy, asynchronously.
87 * This is designed to clean up a backup on upgrade.
88 */
89 removeBackupCopy: function (ext) {
90 return SessionFileInternal.removeBackupCopy(ext);
91 },
92 /**
93 * Wipe the contents of the session file, asynchronously.
94 */
95 wipe: function () {
96 SessionFileInternal.wipe();
97 }
98 };
99
100 Object.freeze(SessionFile);
101
102 /**
103 * Utilities for dealing with promises and Task.jsm
104 */
105 let SessionFileInternal = {
106 /**
107 * The path to sessionstore.js
108 */
109 path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"),
110
111 /**
112 * The path to sessionstore.bak
113 */
114 backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"),
115
116 /**
117 * The promise returned by the latest call to |write|.
118 * We use it to ensure that AsyncShutdown.profileBeforeChange cannot
119 * interrupt a call to |write|.
120 */
121 _latestWrite: null,
122
123 /**
124 * |true| once we have decided to stop receiving write instructiosn
125 */
126 _isClosed: false,
127
128 read: function () {
129 // We must initialize the worker during startup so it will be ready to
130 // perform the final write. If shutdown happens soon after startup and
131 // the worker has not started yet we may not write.
132 // See Bug 964531.
133 SessionWorker.post("init");
134
135 return Task.spawn(function*() {
136 for (let filename of [this.path, this.backupPath]) {
137 try {
138 let startMs = Date.now();
139
140 let data = yield OS.File.read(filename, { encoding: "utf-8" });
141
142 Telemetry.getHistogramById("FX_SESSION_RESTORE_READ_FILE_MS")
143 .add(Date.now() - startMs);
144
145 return data;
146 } catch (ex if ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
147 // Ignore exceptions about non-existent files.
148 }
149 }
150
151 return "";
152 }.bind(this));
153 },
154
155 gatherTelemetry: function(aStateString) {
156 return Task.spawn(function() {
157 let msg = yield SessionWorker.post("gatherTelemetry", [aStateString]);
158 this._recordTelemetry(msg.telemetry);
159 throw new Task.Result(msg.telemetry);
160 }.bind(this));
161 },
162
163 write: function (aData) {
164 if (this._isClosed) {
165 return Promise.reject(new Error("SessionFile is closed"));
166 }
167 let refObj = {};
168
169 let isFinalWrite = false;
170 if (Services.startup.shuttingDown) {
171 // If shutdown has started, we will want to stop receiving
172 // write instructions.
173 isFinalWrite = this._isClosed = true;
174 }
175
176 return this._latestWrite = Task.spawn(function task() {
177 TelemetryStopwatch.start("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
178
179 try {
180 let promise = SessionWorker.post("write", [aData]);
181 // At this point, we measure how long we stop the main thread
182 TelemetryStopwatch.finish("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
183
184 // Now wait for the result and record how long the write took
185 let msg = yield promise;
186 this._recordTelemetry(msg.telemetry);
187 } catch (ex) {
188 TelemetryStopwatch.cancel("FX_SESSION_RESTORE_WRITE_FILE_LONGEST_OP_MS", refObj);
189 console.error("Could not write session state file ", this.path, ex);
190 }
191
192 if (isFinalWrite) {
193 Services.obs.notifyObservers(null, "sessionstore-final-state-write-complete", "");
194 }
195 }.bind(this));
196 },
197
198 createBackupCopy: function (ext) {
199 return SessionWorker.post("createBackupCopy", [ext]);
200 },
201
202 removeBackupCopy: function (ext) {
203 return SessionWorker.post("removeBackupCopy", [ext]);
204 },
205
206 wipe: function () {
207 SessionWorker.post("wipe");
208 },
209
210 _recordTelemetry: function(telemetry) {
211 for (let id of Object.keys(telemetry)){
212 let value = telemetry[id];
213 let samples = [];
214 if (Array.isArray(value)) {
215 samples.push(...value);
216 } else {
217 samples.push(value);
218 }
219 let histogram = Telemetry.getHistogramById(id);
220 for (let sample of samples) {
221 histogram.add(sample);
222 }
223 }
224 }
225 };
226
227 // Interface to a dedicated thread handling I/O
228 let SessionWorker = (function () {
229 let worker = new PromiseWorker("resource:///modules/sessionstore/SessionWorker.js",
230 OS.Shared.LOG.bind("SessionWorker"));
231 return {
232 post: function post(...args) {
233 let promise = worker.post.apply(worker, args);
234 return promise.then(
235 null,
236 function onError(error) {
237 // Decode any serialized error
238 if (error instanceof PromiseWorker.WorkerError) {
239 throw OS.File.Error.fromMsg(error.data);
240 }
241 // Extract something meaningful from ErrorEvent
242 if (error instanceof ErrorEvent) {
243 throw new Error(error.message, error.filename, error.lineno);
244 }
245 throw error;
246 }
247 );
248 }
249 };
250 })();
251
252 // Ensure that we can write sessionstore.js cleanly before the profile
253 // becomes unaccessible.
254 AsyncShutdown.profileBeforeChange.addBlocker(
255 "SessionFile: Finish writing the latest sessionstore.js",
256 function() {
257 return SessionFileInternal._latestWrite;
258 });

mercurial