1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/components/sessionstore/src/SessionWorker.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,329 @@ 1.4 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.5 + * License, v. 2.0. If a copy of the MPL was not distributed with this file, 1.6 + * You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.7 + 1.8 +/** 1.9 + * A worker dedicated to handle I/O for Session Store. 1.10 + */ 1.11 + 1.12 +"use strict"; 1.13 + 1.14 +importScripts("resource://gre/modules/osfile.jsm"); 1.15 + 1.16 +let File = OS.File; 1.17 +let Encoder = new TextEncoder(); 1.18 +let Decoder = new TextDecoder(); 1.19 + 1.20 +/** 1.21 + * Communications with the controller. 1.22 + * 1.23 + * Accepts messages: 1.24 + * {fun:function_name, args:array_of_arguments_or_null, id: custom_id} 1.25 + * 1.26 + * Sends messages: 1.27 + * {ok: result, id: custom_id, telemetry: {}} / 1.28 + * {fail: serialized_form_of_OS.File.Error, id: custom_id} 1.29 + */ 1.30 +self.onmessage = function (msg) { 1.31 + let data = msg.data; 1.32 + if (!(data.fun in Agent)) { 1.33 + throw new Error("Cannot find method " + data.fun); 1.34 + } 1.35 + 1.36 + let result; 1.37 + let id = data.id; 1.38 + 1.39 + try { 1.40 + result = Agent[data.fun].apply(Agent, data.args) || {}; 1.41 + } catch (ex if ex instanceof OS.File.Error) { 1.42 + // Instances of OS.File.Error know how to serialize themselves 1.43 + // (deserialization ensures that we end up with OS-specific 1.44 + // instances of |OS.File.Error|) 1.45 + self.postMessage({fail: OS.File.Error.toMsg(ex), id: id}); 1.46 + return; 1.47 + } 1.48 + 1.49 + // Other exceptions do not, and should be propagated through DOM's 1.50 + // built-in mechanism for uncaught errors, although this mechanism 1.51 + // may lose interesting information. 1.52 + self.postMessage({ 1.53 + ok: result.result, 1.54 + id: id, 1.55 + telemetry: result.telemetry || {} 1.56 + }); 1.57 +}; 1.58 + 1.59 +let Agent = { 1.60 + // Boolean that tells whether we already made a 1.61 + // call to write(). We will only attempt to move 1.62 + // sessionstore.js to sessionstore.bak on the 1.63 + // first write. 1.64 + hasWrittenState: false, 1.65 + 1.66 + // The path to sessionstore.js 1.67 + path: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.js"), 1.68 + 1.69 + // The path to sessionstore.bak 1.70 + backupPath: OS.Path.join(OS.Constants.Path.profileDir, "sessionstore.bak"), 1.71 + 1.72 + /** 1.73 + * NO-OP to start the worker. 1.74 + */ 1.75 + init: function () { 1.76 + return {result: true}; 1.77 + }, 1.78 + 1.79 + /** 1.80 + * Write the session to disk. 1.81 + */ 1.82 + write: function (stateString) { 1.83 + let exn; 1.84 + let telemetry = {}; 1.85 + 1.86 + if (!this.hasWrittenState) { 1.87 + try { 1.88 + let startMs = Date.now(); 1.89 + File.move(this.path, this.backupPath); 1.90 + telemetry.FX_SESSION_RESTORE_BACKUP_FILE_MS = Date.now() - startMs; 1.91 + } catch (ex if isNoSuchFileEx(ex)) { 1.92 + // Ignore exceptions about non-existent files. 1.93 + } catch (ex) { 1.94 + // Throw the exception after we wrote the state to disk 1.95 + // so that the backup can't interfere with the actual write. 1.96 + exn = ex; 1.97 + } 1.98 + 1.99 + this.hasWrittenState = true; 1.100 + } 1.101 + 1.102 + let ret = this._write(stateString, telemetry); 1.103 + 1.104 + if (exn) { 1.105 + throw exn; 1.106 + } 1.107 + 1.108 + return ret; 1.109 + }, 1.110 + 1.111 + /** 1.112 + * Extract all sorts of useful statistics from a state string, 1.113 + * for use with Telemetry. 1.114 + * 1.115 + * @return {object} 1.116 + */ 1.117 + gatherTelemetry: function (stateString) { 1.118 + return Statistics.collect(stateString); 1.119 + }, 1.120 + 1.121 + /** 1.122 + * Write a stateString to disk 1.123 + */ 1.124 + _write: function (stateString, telemetry = {}) { 1.125 + let bytes = Encoder.encode(stateString); 1.126 + let startMs = Date.now(); 1.127 + let result = File.writeAtomic(this.path, bytes, {tmpPath: this.path + ".tmp"}); 1.128 + telemetry.FX_SESSION_RESTORE_WRITE_FILE_MS = Date.now() - startMs; 1.129 + telemetry.FX_SESSION_RESTORE_FILE_SIZE_BYTES = bytes.byteLength; 1.130 + return {result: result, telemetry: telemetry}; 1.131 + }, 1.132 + 1.133 + /** 1.134 + * Creates a copy of sessionstore.js. 1.135 + */ 1.136 + createBackupCopy: function (ext) { 1.137 + try { 1.138 + return {result: File.copy(this.path, this.backupPath + ext)}; 1.139 + } catch (ex if isNoSuchFileEx(ex)) { 1.140 + // Ignore exceptions about non-existent files. 1.141 + return {result: true}; 1.142 + } 1.143 + }, 1.144 + 1.145 + /** 1.146 + * Removes a backup copy. 1.147 + */ 1.148 + removeBackupCopy: function (ext) { 1.149 + try { 1.150 + return {result: File.remove(this.backupPath + ext)}; 1.151 + } catch (ex if isNoSuchFileEx(ex)) { 1.152 + // Ignore exceptions about non-existent files. 1.153 + return {result: true}; 1.154 + } 1.155 + }, 1.156 + 1.157 + /** 1.158 + * Wipes all files holding session data from disk. 1.159 + */ 1.160 + wipe: function () { 1.161 + let exn; 1.162 + 1.163 + // Erase session state file 1.164 + try { 1.165 + File.remove(this.path); 1.166 + } catch (ex if isNoSuchFileEx(ex)) { 1.167 + // Ignore exceptions about non-existent files. 1.168 + } catch (ex) { 1.169 + // Don't stop immediately. 1.170 + exn = ex; 1.171 + } 1.172 + 1.173 + // Erase any backup, any file named "sessionstore.bak[-buildID]". 1.174 + let iter = new File.DirectoryIterator(OS.Constants.Path.profileDir); 1.175 + for (let entry in iter) { 1.176 + if (!entry.isDir && entry.path.startsWith(this.backupPath)) { 1.177 + try { 1.178 + File.remove(entry.path); 1.179 + } catch (ex) { 1.180 + // Don't stop immediately. 1.181 + exn = exn || ex; 1.182 + } 1.183 + } 1.184 + } 1.185 + 1.186 + if (exn) { 1.187 + throw exn; 1.188 + } 1.189 + 1.190 + return {result: true}; 1.191 + } 1.192 +}; 1.193 + 1.194 +function isNoSuchFileEx(aReason) { 1.195 + return aReason instanceof OS.File.Error && aReason.becauseNoSuchFile; 1.196 +} 1.197 + 1.198 +/** 1.199 + * Estimate the number of bytes that a data structure will use on disk 1.200 + * once serialized. 1.201 + */ 1.202 +function getByteLength(str) { 1.203 + return Encoder.encode(JSON.stringify(str)).byteLength; 1.204 +} 1.205 + 1.206 +/** 1.207 + * Tools for gathering statistics on a state string. 1.208 + */ 1.209 +let Statistics = { 1.210 + collect: function(stateString) { 1.211 + let start = Date.now(); 1.212 + let TOTAL_PREFIX = "FX_SESSION_RESTORE_TOTAL_"; 1.213 + let INDIVIDUAL_PREFIX = "FX_SESSION_RESTORE_INDIVIDUAL_"; 1.214 + let SIZE_SUFFIX = "_SIZE_BYTES"; 1.215 + 1.216 + let state = JSON.parse(stateString); 1.217 + 1.218 + // Gather all data 1.219 + let subsets = {}; 1.220 + this.gatherSimpleData(state, subsets); 1.221 + this.gatherComplexData(state, subsets); 1.222 + 1.223 + // Extract telemetry 1.224 + let telemetry = {}; 1.225 + for (let k of Object.keys(subsets)) { 1.226 + let obj = subsets[k]; 1.227 + telemetry[TOTAL_PREFIX + k + SIZE_SUFFIX] = getByteLength(obj); 1.228 + 1.229 + if (Array.isArray(obj)) { 1.230 + let size = obj.map(getByteLength); 1.231 + telemetry[INDIVIDUAL_PREFIX + k + SIZE_SUFFIX] = size; 1.232 + } 1.233 + } 1.234 + 1.235 + let stop = Date.now(); 1.236 + telemetry["FX_SESSION_RESTORE_EXTRACTING_STATISTICS_DURATION_MS"] = stop - start; 1.237 + return { 1.238 + telemetry: telemetry 1.239 + }; 1.240 + }, 1.241 + 1.242 + /** 1.243 + * Collect data that doesn't require a recursive walk through the 1.244 + * data structure. 1.245 + */ 1.246 + gatherSimpleData: function(state, subsets) { 1.247 + // The subset of sessionstore.js dealing with open windows 1.248 + subsets.OPEN_WINDOWS = state.windows; 1.249 + 1.250 + // The subset of sessionstore.js dealing with closed windows 1.251 + subsets.CLOSED_WINDOWS = state._closedWindows; 1.252 + 1.253 + // The subset of sessionstore.js dealing with closed tabs 1.254 + // in open windows 1.255 + subsets.CLOSED_TABS_IN_OPEN_WINDOWS = []; 1.256 + 1.257 + // The subset of sessionstore.js dealing with cookies 1.258 + // in both open and closed windows 1.259 + subsets.COOKIES = []; 1.260 + 1.261 + for (let winData of state.windows) { 1.262 + let closedTabs = winData._closedTabs || []; 1.263 + subsets.CLOSED_TABS_IN_OPEN_WINDOWS.push(...closedTabs); 1.264 + 1.265 + let cookies = winData.cookies || []; 1.266 + subsets.COOKIES.push(...cookies); 1.267 + } 1.268 + 1.269 + for (let winData of state._closedWindows) { 1.270 + let cookies = winData.cookies || []; 1.271 + subsets.COOKIES.push(...cookies); 1.272 + } 1.273 + }, 1.274 + 1.275 + /** 1.276 + * Walk through a data structure, recursively. 1.277 + * 1.278 + * @param {object} root The object from which to start walking. 1.279 + * @param {function(key, value)} cb Callback, called for each 1.280 + * item except the root. Returns |true| to walk the subtree rooted 1.281 + * at |value|, |false| otherwise */ 1.282 + walk: function(root, cb) { 1.283 + if (!root || typeof root !== "object") { 1.284 + return; 1.285 + } 1.286 + for (let k of Object.keys(root)) { 1.287 + let obj = root[k]; 1.288 + let stepIn = cb(k, obj); 1.289 + if (stepIn) { 1.290 + this.walk(obj, cb); 1.291 + } 1.292 + } 1.293 + }, 1.294 + 1.295 + /** 1.296 + * Collect data that requires walking through the data structure 1.297 + */ 1.298 + gatherComplexData: function(state, subsets) { 1.299 + // The subset of sessionstore.js dealing with DOM storage 1.300 + subsets.DOM_STORAGE = []; 1.301 + // The subset of sessionstore.js storing form data 1.302 + subsets.FORMDATA = []; 1.303 + // The subset of sessionstore.js storing history 1.304 + subsets.HISTORY = []; 1.305 + 1.306 + 1.307 + this.walk(state, function(k, value) { 1.308 + let dest; 1.309 + switch (k) { 1.310 + case "entries": 1.311 + subsets.HISTORY.push(value); 1.312 + return true; 1.313 + case "storage": 1.314 + subsets.DOM_STORAGE.push(value); 1.315 + // Never visit storage, it's full of weird stuff 1.316 + return false; 1.317 + case "formdata": 1.318 + subsets.FORMDATA.push(value); 1.319 + // Never visit formdata, it's full of weird stuff 1.320 + return false; 1.321 + case "cookies": // Don't visit these places, they are full of weird stuff 1.322 + case "extData": 1.323 + return false; 1.324 + default: 1.325 + return true; 1.326 + } 1.327 + }); 1.328 + 1.329 + return subsets; 1.330 + }, 1.331 + 1.332 +};