Merge m-c to inbound.

This commit is contained in:
Ryan VanderMeulen 2013-06-05 16:37:16 -04:00
commit 80c2c4083c
11 changed files with 528 additions and 79 deletions

View File

@ -116,15 +116,40 @@ SpecialPowers.addPermission("sms", true, document);
let sms = window.navigator.mozSms;
ok(sms instanceof MozSmsManager);
function repeat(func, array, oncomplete) {
(function do_call(index) {
let next = index < (array.length - 1) ? do_call.bind(null, index + 1) : oncomplete;
array[index].push(next);
func.apply(null, array[index]);
})(0);
}
let tasks = {
// List of test fuctions. Each of them should call |tasks.next()| when
// completed or |tasks.finish()| to jump to the last one.
_tasks: [],
_nextTaskIndex: 0,
function testStrict7BitEncodingHelper(sent, received, next) {
push: function push(func) {
this._tasks.push(func);
},
next: function next() {
let index = this._nextTaskIndex++;
let task = this._tasks[index];
try {
task();
} catch (ex) {
ok(false, "test task[" + index + "] throws: " + ex);
// Run last task as clean up if possible.
if (index != this._tasks.length - 1) {
this.finish();
}
}
},
finish: function finish() {
this._tasks[this._tasks.length - 1]();
},
run: function run() {
this.next();
}
};
function testStrict7BitEncodingHelper(sent, received) {
// The log message contains unicode and Marionette seems unable to process
// it and throws: |UnicodeEncodeError: 'ascii' codec can't encode character
// u'\xa5' in position 14: ordinal not in range(128)|.
@ -135,7 +160,7 @@ function testStrict7BitEncodingHelper(sent, received, next) {
function done(step) {
count += step;
if (count >= 2) {
window.setTimeout(next, 0);
window.setTimeout(tasks.next.bind(tasks), 0);
}
}
@ -161,53 +186,75 @@ function testStrict7BitEncodingHelper(sent, received, next) {
});
}
function test_enabled() {
// Bug 877141 - If you send several spaces together in a sms, the other
// dipositive receives a "*" for each space.
//
// This function is called twice, with strict 7bit encoding enabled or
// disabled. Expect the same result in both sent and received text and with
// either strict 7bit encoding enabled or disabled.
function testBug877141() {
log("Testing bug 877141");
let sent = "1 2 3";
testStrict7BitEncodingHelper(sent, sent);
}
tasks.push(function () {
log("Testing with dom.sms.strict7BitEncoding enabled");
SpecialPowers.setBoolPref("dom.sms.strict7BitEncoding", true);
tasks.next();
});
let cases = [];
// Test for combined string.
// Test for combined string.
tasks.push(function () {
let sent = "", received = "";
for (let c in GSM_SMS_STRICT_7BIT_CHARMAP) {
sent += c;
received += GSM_SMS_STRICT_7BIT_CHARMAP[c];
}
cases.push([sent, received]);
testStrict7BitEncodingHelper(sent, received);
});
// When strict7BitEncoding is enabled, we should replace characters that
// can't be encoded with GSM 7-Bit alphabets with '*'.
cases.push(["\u65b0\u5e74\u5feb\u6a02", "****"]); // "Happy New Year" in Chinese.
// When strict7BitEncoding is enabled, we should replace characters that
// can't be encoded with GSM 7-Bit alphabets with '*'.
tasks.push(function () {
// "Happy New Year" in Chinese.
let sent = "\u65b0\u5e74\u5feb\u6a02", received = "****";
testStrict7BitEncodingHelper(sent, received);
});
repeat(testStrict7BitEncodingHelper, cases, test_disabled);
}
tasks.push(testBug877141);
function test_disabled() {
tasks.push(function () {
log("Testing with dom.sms.strict7BitEncoding disabled");
SpecialPowers.setBoolPref("dom.sms.strict7BitEncoding", false);
tasks.next();
});
let cases = [];
// Test for combined string.
// Test for combined string.
tasks.push(function () {
let sent = "";
for (let c in GSM_SMS_STRICT_7BIT_CHARMAP) {
sent += c;
}
cases.push([sent, sent]);
testStrict7BitEncodingHelper(sent, sent);
});
cases.push(["\u65b0\u5e74\u5feb\u6a02", "\u65b0\u5e74\u5feb\u6a02"]);
tasks.push(function () {
// "Happy New Year" in Chinese.
let sent = "\u65b0\u5e74\u5feb\u6a02";
testStrict7BitEncodingHelper(sent, sent);
});
repeat(testStrict7BitEncodingHelper, cases, cleanUp);
}
tasks.push(testBug877141);
function cleanUp() {
// WARNING: All tasks should be pushed before this!!!
tasks.push(function cleanUp() {
SpecialPowers.removePermission("sms", document);
SpecialPowers.clearUserPref("dom.sms.enabled");
SpecialPowers.clearUserPref("dom.sms.strict7BitEncoding");
finish();
}
});
test_enabled();
tasks.run();

View File

@ -130,6 +130,13 @@ function isComplete(code) {
return (type != NETD_COMMAND_PROCEEDING);
}
function defineLazyRegExp(obj, name, pattern) {
obj.__defineGetter__(name, function() {
delete obj[name];
return obj[name] = new RegExp(pattern);
});
}
/**
* This component watches for network interfaces changing state and then
* adjusts routes etc. accordingly.
@ -216,6 +223,10 @@ function NetworkManager() {
});
ppmm.addMessageListener('NetworkInterfaceList:ListInterface', this);
// Used in resolveHostname().
defineLazyRegExp(this, "REGEXP_IPV4", "^\\d{1,3}(?:\\.\\d{1,3}){3}$");
defineLazyRegExp(this, "REGEXP_IPV6", "^[\\da-fA-F]{4}(?::[\\da-fA-F]{4}){7}$");
}
NetworkManager.prototype = {
classID: NETWORKMANAGER_CID,
@ -611,17 +622,25 @@ NetworkManager.prototype = {
resolveHostname: function resolveHostname(hosts) {
let retval = [];
for(var i = 0; i < hosts.length; i++) {
let hostname = hosts[i].split('/')[2];
if (!hostname) {
for (let hostname of hosts) {
try {
let uri = Services.io.newURI(hostname, null, null);
hostname = uri.host;
} catch (e) {}
if (hostname.match(this.REGEXP_IPV4) ||
hostname.match(this.REGEXP_IPV6)) {
retval.push(hostname);
continue;
}
let hostnameIps = gDNSService.resolve(hostname, 0);
while (hostnameIps.hasMore()) {
retval.push(hostnameIps.getNextAddrAsString());
debug("Found IP at: " + JSON.stringify(retval));
}
try {
let hostnameIps = gDNSService.resolve(hostname, 0);
while (hostnameIps.hasMore()) {
retval.push(hostnameIps.getNextAddrAsString());
debug("Found IP at: " + JSON.stringify(retval));
}
} catch (e) {}
}
return retval;

View File

@ -1166,7 +1166,7 @@ SetNiceForPid(int aPid, int aNice)
}
int newtaskpriority =
std::max(origtaskpriority + aNice - origProcPriority, origProcPriority);
std::max(origtaskpriority - origProcPriority + aNice, aNice);
rv = setpriority(PRIO_PROCESS, tid, newtaskpriority);
if (rv) {

View File

@ -16,6 +16,7 @@ import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuInflater;
import org.mozilla.gecko.menu.MenuPanel;
import org.mozilla.gecko.health.BrowserHealthRecorder;
import org.mozilla.gecko.health.BrowserHealthRecorder.SessionInformation;
import org.mozilla.gecko.updater.UpdateService;
import org.mozilla.gecko.updater.UpdateServiceHelper;
import org.mozilla.gecko.util.EventDispatcher;
@ -248,6 +249,10 @@ abstract public class GeckoApp
return this;
}
public static SharedPreferences getAppSharedPreferences() {
return GeckoApp.sAppContext.getSharedPreferences(PREFS_NAME, 0);
}
public SurfaceView getCameraView() {
return mCameraView;
}
@ -544,6 +549,11 @@ abstract public class GeckoApp
} else if (event.equals("Gecko:Ready")) {
mGeckoReadyStartupTimer.stop();
geckoConnected();
// This method is already running on the background thread, so we
// know that mHealthRecorder will exist. This method is cheap, so
// don't spawn a new runnable.
mHealthRecorder.recordGeckoStartupTime(mGeckoReadyStartupTimer.getElapsed());
} else if (event.equals("ToggleChrome:Hide")) {
toggleChrome(false);
} else if (event.equals("ToggleChrome:Show")) {
@ -1240,20 +1250,20 @@ abstract public class GeckoApp
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
SharedPreferences prefs =
GeckoApp.sAppContext.getSharedPreferences(PREFS_NAME, 0);
final SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
boolean wasOOM = prefs.getBoolean(PREFS_OOM_EXCEPTION, false);
boolean wasStopped = prefs.getBoolean(PREFS_WAS_STOPPED, true);
if (wasOOM || !wasStopped) {
SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs);
if (previousSession.wasKilled()) {
Telemetry.HistogramAdd("FENNEC_WAS_KILLED", 1);
}
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false);
// Put a flag to check if we got a normal onSaveInstanceState
// on exit, or if we were suddenly killed (crash or native OOM)
// Put a flag to check if we got a normal `onSaveInstanceState`
// on exit, or if we were suddenly killed (crash or native OOM).
editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
editor.commit();
// The lifecycle of mHealthRecorder is "shortly after onCreate"
@ -1262,7 +1272,8 @@ abstract public class GeckoApp
final String profilePath = getProfile().getDir().getAbsolutePath();
final EventDispatcher dispatcher = GeckoAppShell.getEventDispatcher();
Log.i(LOGTAG, "Creating BrowserHealthRecorder.");
mHealthRecorder = new BrowserHealthRecorder(sAppContext, profilePath, dispatcher);
mHealthRecorder = new BrowserHealthRecorder(sAppContext, profilePath, dispatcher,
previousSession);
}
});
@ -1473,12 +1484,16 @@ abstract public class GeckoApp
}
});
// End of the startup of our Java App
// Trigger the completion of the telemetry timer that wraps activity startup,
// then grab the duration to give to FHR.
mJavaUiStartupTimer.stop();
final long javaDuration = mJavaUiStartupTimer.getElapsed();
ThreadUtils.getBackgroundHandler().postDelayed(new Runnable() {
@Override
public void run() {
mHealthRecorder.recordJavaStartupTime(javaDuration);
// Sync settings need Gecko to be loaded, so
// no hurry in starting this.
checkMigrateSync();
@ -1593,7 +1608,7 @@ abstract public class GeckoApp
return RESTORE_OOM;
}
final SharedPreferences prefs = GeckoApp.sAppContext.getSharedPreferences(PREFS_NAME, 0);
final SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
// We record crashes in the crash reporter. If sessionstore.js
// exists, but we didn't flag a crash in the crash reporter, we
@ -1792,19 +1807,34 @@ abstract public class GeckoApp
GeckoAccessibility.updateAccessibilitySettings(this);
if (mAppStateListeners != null) {
for(GeckoAppShell.AppStateListener listener: mAppStateListeners) {
for (GeckoAppShell.AppStateListener listener: mAppStateListeners) {
listener.onResume();
}
}
// We use two times: a pseudo-unique wall-clock time to identify the
// current session across power cycles, and the elapsed realtime to
// track the duration of the session.
final long now = System.currentTimeMillis();
final long realTime = android.os.SystemClock.elapsedRealtime();
final BrowserHealthRecorder rec = mHealthRecorder;
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
SharedPreferences prefs =
GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0);
// Now construct the new session on BrowserHealthRecorder's behalf. We do this here
// so it can benefit from a single near-startup prefs commit.
SessionInformation currentSession = new SessionInformation(now, realTime);
SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
currentSession.recordBegin(editor);
editor.commit();
if (rec != null) {
rec.setCurrentSession(currentSession);
}
}
});
}
@ -1822,16 +1852,20 @@ abstract public class GeckoApp
@Override
public void onPause()
{
final BrowserHealthRecorder rec = mHealthRecorder;
// In some way it's sad that Android will trigger StrictMode warnings
// here as the whole point is to save to disk while the activity is not
// interacting with the user.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
SharedPreferences prefs =
GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0);
SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
if (rec != null) {
rec.recordSessionEnd("P", editor);
}
editor.commit();
}
});
@ -1853,8 +1887,7 @@ abstract public class GeckoApp
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
SharedPreferences prefs =
GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0);
SharedPreferences prefs = GeckoApp.getAppSharedPreferences();
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, false);
editor.commit();
@ -1927,9 +1960,16 @@ abstract public class GeckoApp
SmsManager.getInstance().shutdown();
}
if (mHealthRecorder != null) {
mHealthRecorder.close();
mHealthRecorder = null;
final BrowserHealthRecorder rec = mHealthRecorder;
mHealthRecorder = null;
if (rec != null) {
// Closing a BrowserHealthRecorder could incur a write.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
rec.close();
}
});
}
super.onDestroy();

View File

@ -36,6 +36,7 @@ public class Telemetry {
private long mStartTime;
private String mName;
private boolean mHasFinished;
private volatile long mElapsed = -1;
public Timer(String name) {
mName = name;
@ -47,6 +48,10 @@ public class Telemetry {
mHasFinished = true;
}
public long getElapsed() {
return mElapsed;
}
public void stop() {
// Only the first stop counts.
if (mHasFinished) {
@ -55,11 +60,12 @@ public class Telemetry {
mHasFinished = true;
}
long elapsed = SystemClock.uptimeMillis() - mStartTime;
final long elapsed = SystemClock.uptimeMillis() - mStartTime;
mElapsed = elapsed;
if (elapsed < Integer.MAX_VALUE) {
HistogramAdd(mName, (int)(elapsed));
} else {
Log.e(LOGTAG, "Duration of " + elapsed + " ms is too long.");
Log.e(LOGTAG, "Duration of " + elapsed + " ms is too long to add to histogram.");
}
}
}

View File

@ -241,6 +241,10 @@ public abstract class Environment {
}
public void setJSONForAddons(String json) throws Exception {
if (json == null || "null".equals(json)) {
addons = null;
return;
}
addons = new JSONObject(json);
}

View File

@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.json.JSONObject;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields.FieldSpec;
@ -215,7 +216,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
int mode,
SQLiteDatabase.CursorFactory factory) {
final File path = getDatabasePath(name);
Logger.info(LOG_TAG, "Opening database through absolute path " + path.getAbsolutePath());
Logger.pii(LOG_TAG, "Opening database through absolute path " + path.getAbsolutePath());
return SQLiteDatabase.openOrCreateDatabase(path, null);
}
}
@ -233,7 +234,7 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
CURRENT_VERSION);
if (CAN_USE_ABSOLUTE_DB_PATH) {
Logger.info(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name));
Logger.pii(LOG_TAG, "Opening: " + getAbsolutePath(profileDirectory, name));
}
}
@ -1034,6 +1035,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
}
}
@Override
public void recordDailyLast(int env, int day, int field, JSONObject value) {
this.recordDailyLast(env, day, field, value == null ? "null" : value.toString(), EVENTS_TEXTUAL);
}
@Override
public void recordDailyLast(int env, int day, int field, String value) {
this.recordDailyLast(env, day, field, value, EVENTS_TEXTUAL);
@ -1060,6 +1066,11 @@ public class HealthReportDatabaseStorage implements HealthReportStorage {
db.insert(table, null, v);
}
@Override
public void recordDailyDiscrete(int env, int day, int field, JSONObject value) {
this.recordDailyDiscrete(env, day, field, value == null ? "null" : value.toString(), EVENTS_TEXTUAL);
}
@Override
public void recordDailyDiscrete(int env, int day, int field, String value) {
this.recordDailyDiscrete(env, day, field, value, EVENTS_TEXTUAL);

View File

@ -112,7 +112,7 @@ public class HealthReportGenerator {
JSONObject days = new JSONObject();
Cursor cursor = storage.getRawEventsSince(since);
try {
if (!cursor.moveToNext()) {
if (!cursor.moveToFirst()) {
return days;
}
@ -186,6 +186,22 @@ public class HealthReportGenerator {
return days;
}
/**
* Return the {@link JSONObject} parsed from the provided index of the given
* cursor, or {@link JSONObject#NULL} if either SQL <code>NULL</code> or
* string <code>"null"</code> is present at that index.
*/
private static Object getJSONAtIndex(Cursor cursor, int index) throws JSONException {
if (cursor.isNull(index)) {
return JSONObject.NULL;
}
final String value = cursor.getString(index);
if ("null".equals(value)) {
return JSONObject.NULL;
}
return new JSONObject(value);
}
protected static void recordMeasurementFromCursor(final Field field,
JSONObject measurement,
Cursor cursor)
@ -205,6 +221,10 @@ public class HealthReportGenerator {
HealthReportUtils.append(measurement, field.fieldName, cursor.getString(3));
return;
}
if (field.isJSONField()) {
HealthReportUtils.append(measurement, field.fieldName, getJSONAtIndex(cursor, 3));
return;
}
if (field.isIntegerField()) {
HealthReportUtils.append(measurement, field.fieldName, cursor.getLong(3));
return;
@ -217,6 +237,10 @@ public class HealthReportGenerator {
measurement.put(field.fieldName, cursor.getString(3));
return;
}
if (field.isJSONField()) {
measurement.put(field.fieldName, getJSONAtIndex(cursor, 3));
return;
}
measurement.put(field.fieldName, cursor.getLong(3));
}

View File

@ -4,6 +4,8 @@
package org.mozilla.gecko.background.healthreport;
import org.json.JSONObject;
import android.database.Cursor;
import android.util.SparseArray;
@ -29,6 +31,7 @@ public interface HealthReportStorage {
protected static final int FLAG_INTEGER = 1 << 0;
protected static final int FLAG_STRING = 1 << 1;
protected static final int FLAG_JSON = 1 << 2;
protected static final int FLAG_DISCRETE = 1 << 8;
protected static final int FLAG_LAST = 1 << 9;
@ -43,6 +46,9 @@ public interface HealthReportStorage {
public static final int TYPE_STRING_DISCRETE = FLAG_STRING | FLAG_DISCRETE;
public static final int TYPE_STRING_LAST = FLAG_STRING | FLAG_LAST;
public static final int TYPE_JSON_DISCRETE = FLAG_JSON | FLAG_DISCRETE;
public static final int TYPE_JSON_LAST = FLAG_JSON | FLAG_LAST;
public static final int TYPE_COUNTED_STRING_DISCRETE = FLAG_COUNTED | TYPE_STRING_DISCRETE;
protected int fieldID = UNKNOWN_TYPE_OR_FIELD_ID;
@ -73,6 +79,14 @@ public interface HealthReportStorage {
return (this.flags & FLAG_STRING) > 0;
}
public boolean isJSONField() {
return (this.flags & FLAG_JSON) > 0;
}
public boolean isStoredAsString() {
return (this.flags & (FLAG_JSON | FLAG_STRING)) > 0;
}
public boolean isDiscreteField() {
return (this.flags & FLAG_DISCRETE) > 0;
}
@ -154,8 +168,10 @@ public interface HealthReportStorage {
*/
public SparseArray<Field> getFieldsByID();
public void recordDailyLast(int env, int day, int field, JSONObject value);
public void recordDailyLast(int env, int day, int field, String value);
public void recordDailyLast(int env, int day, int field, int value);
public void recordDailyDiscrete(int env, int day, int field, JSONObject value);
public void recordDailyDiscrete(int env, int day, int field, String value);
public void recordDailyDiscrete(int env, int day, int field, int value);
public void incrementDailyCount(int env, int day, int field, int by);

View File

@ -9,9 +9,11 @@ import java.util.ArrayList;
import android.content.Context;
import android.content.ContentProviderClient;
import android.content.SharedPreferences;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.PrefsHelper;
@ -28,6 +30,7 @@ import org.mozilla.gecko.util.EventDispatcher;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
@ -36,6 +39,7 @@ import java.io.OutputStreamWriter;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* BrowserHealthRecorder is the browser's interface to the Firefox Health
@ -52,7 +56,7 @@ import java.util.Scanner;
*
* Use it to record events: {@link #recordSearch(String, String)}.
*
* Shut it down when you're done being a browser: {@link #close(EventDispatcher)}.
* Shut it down when you're done being a browser: {@link #close()}.
*/
public class BrowserHealthRecorder implements GeckoEventListener {
private static final String LOG_TAG = "GeckoHealthRec";
@ -78,12 +82,139 @@ public class BrowserHealthRecorder implements GeckoEventListener {
protected volatile State state = State.NOT_INITIALIZED;
private final AtomicBoolean orphanChecked = new AtomicBoolean(false);
private volatile int env = -1;
private volatile HealthReportDatabaseStorage storage;
private final ProfileInformationCache profileCache;
private ContentProviderClient client;
private final EventDispatcher dispatcher;
public static class SessionInformation {
private static final String LOG_TAG = "GeckoSessInfo";
public static final String PREFS_SESSION_START = "sessionStart";
public final long wallStartTime; // System wall clock.
public final long realStartTime; // Realtime clock.
private final boolean wasOOM;
private final boolean wasStopped;
private volatile long timedGeckoStartup = -1;
private volatile long timedJavaStartup = -1;
// Current sessions don't (right now) care about wasOOM/wasStopped.
// Eventually we might want to lift that logic out of GeckoApp.
public SessionInformation(long wallTime, long realTime) {
this(wallTime, realTime, false, false);
}
// Previous sessions do...
public SessionInformation(long wallTime, long realTime, boolean wasOOM, boolean wasStopped) {
this.wallStartTime = wallTime;
this.realStartTime = realTime;
this.wasOOM = wasOOM;
this.wasStopped = wasStopped;
}
/**
* Initialize a new SessionInformation instance from the supplied prefs object.
*
* This includes retrieving OOM/crash data, as well as timings.
*
* If no wallStartTime was found, that implies that the previous
* session was correctly recorded, and an object with a zero
* wallStartTime is returned.
*/
public static SessionInformation fromSharedPrefs(SharedPreferences prefs) {
boolean wasOOM = prefs.getBoolean(GeckoApp.PREFS_OOM_EXCEPTION, false);
boolean wasStopped = prefs.getBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
long wallStartTime = prefs.getLong(PREFS_SESSION_START, 0L);
long realStartTime = 0L;
Log.d(LOG_TAG, "Building SessionInformation from prefs: " +
wallStartTime + ", " + realStartTime + ", " +
wasStopped + ", " + wasOOM);
return new SessionInformation(wallStartTime, realStartTime, wasOOM, wasStopped);
}
public boolean wasKilled() {
return wasOOM || !wasStopped;
}
/**
* Record the beginning of this session to SharedPreferences by
* recording our start time. If a session was already recorded, it is
* overwritten (there can only be one running session at a time). Does
* not commit the editor.
*/
public void recordBegin(SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording start of session: " + this.wallStartTime);
editor.putLong(PREFS_SESSION_START, this.wallStartTime);
}
/**
* Record the completion of this session to SharedPreferences by
* deleting our start time. Does not commit the editor.
*/
public void recordCompletion(SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording session done: " + this.wallStartTime);
editor.remove(PREFS_SESSION_START);
}
/**
* Return the JSON that we'll put in the DB for this session.
*/
public JSONObject getCompletionJSON(String reason, long realEndTime) throws JSONException {
long durationSecs = (realEndTime - this.realStartTime) / 1000;
JSONObject out = new JSONObject();
out.put("r", reason);
out.put("d", durationSecs);
if (this.timedGeckoStartup > 0) {
out.put("sg", this.timedGeckoStartup);
}
if (this.timedJavaStartup > 0) {
out.put("sj", this.timedJavaStartup);
}
return out;
}
public JSONObject getCrashedJSON() throws JSONException {
JSONObject out = new JSONObject();
// We use ints here instead of booleans, because we're packing
// stuff into JSON, and saving bytes in the DB is a worthwhile
// goal.
out.put("oom", this.wasOOM ? 1 : 0);
out.put("stopped", this.wasStopped ? 1 : 0);
out.put("r", "A");
return out;
}
}
// We track previousSession to avoid order-of-initialization confusion. We
// accept it in the constructor, and process it after init.
private final SessionInformation previousSession;
private volatile SessionInformation session = null;
public SessionInformation getCurrentSession() {
return this.session;
}
public void setCurrentSession(SessionInformation session) {
this.session = session;
}
public void recordGeckoStartupTime(long duration) {
if (this.session == null) {
return;
}
this.session.timedGeckoStartup = duration;
}
public void recordJavaStartupTime(long duration) {
if (this.session == null) {
return;
}
this.session.timedJavaStartup = duration;
}
/**
* Persist the opaque identifier for the current Firefox Health Report environment.
* This changes in certain circumstances; be sure to use the current value when recording data.
@ -95,9 +226,11 @@ public class BrowserHealthRecorder implements GeckoEventListener {
/**
* This constructor does IO. Run it on a background thread.
*/
public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher) {
public BrowserHealthRecorder(final Context context, final String profilePath, final EventDispatcher dispatcher, SessionInformation previousSession) {
Log.d(LOG_TAG, "Initializing. Dispatcher is " + dispatcher);
this.dispatcher = dispatcher;
this.previousSession = previousSession;
this.client = EnvironmentBuilder.getContentProviderClient(context);
if (this.client == null) {
throw new IllegalStateException("Could not fetch Health Report content provider.");
@ -114,8 +247,6 @@ public class BrowserHealthRecorder implements GeckoEventListener {
} catch (Exception e) {
Log.e(LOG_TAG, "Exception initializing.", e);
}
// TODO: record session start and end?
}
/**
@ -353,6 +484,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
dispatcher.registerEventListener(EVENT_PREF_CHANGE, self);
// Initialize each provider here.
initializeSessionsProvider();
initializeSearchProvider();
Log.d(LOG_TAG, "Ensuring environment.");
@ -365,7 +497,11 @@ public class BrowserHealthRecorder implements GeckoEventListener {
state = State.INITIALIZATION_FAILED;
storage.abortInitialization();
Log.e(LOG_TAG, "Initialization failed.", e);
return;
}
// Now do whatever we do after we start up.
checkForOrphanSessions();
}
}
});
@ -383,7 +519,7 @@ public class BrowserHealthRecorder implements GeckoEventListener {
// If we can restore state from last time, great.
if (this.profileCache.restoreUnlessInitialized()) {
Log.i(LOG_TAG, "Successfully restored state. Initializing storage.");
Log.d(LOG_TAG, "Successfully restored state. Initializing storage.");
initializeStorage();
return;
}
@ -641,5 +777,155 @@ public class BrowserHealthRecorder implements GeckoEventListener {
}
});
}
/*
* Sessions.
*
* We record session beginnings in SharedPreferences, because it's cheaper
* to do that than to either write to then update the DB (which requires
* keeping a row identifier to update, as well as two writes) or to record
* two events (which doubles storage space and requires rollup logic).
*
* The pattern is:
*
* 1. On startup, determine whether an orphan session exists by looking for
* a saved timestamp in prefs. If it does, then record the orphan in FHR
* storage.
* 2. Record in prefs that a new session has begun. Track the timestamp (so
* we know to which day the session belongs).
* 3. As startup timings become available, accumulate them in memory.
* 4. On clean shutdown, read the values from here, write them to the DB, and
* delete the sentinel time from SharedPreferences.
* 5. On a dirty shutdown, the in-memory session will not be written to the
* DB, and the current session will be orphaned.
*
* Sessions are begun in onResume (and thus implicitly onStart) and ended
* in onPause.
*
* Session objects are stored as discrete JSON.
*
* "org.mozilla.appSessions": {
* _v: 4,
* "normal": [
* {"r":"P", "d": 123},
* ],
* "abnormal": [
* {"r":"A", "oom": true, "stopped": false}
* ]
* }
*
* "r": reason. Values are "P" (activity paused), "A" (abnormal termination)
* "d": duration. Value in seconds.
* "sg": Gecko startup time. Present if this is a clean launch.
* "sj": Java startup time. Present if this is a clean launch.
*
* Abnormal terminations will be missing a duration and will feature these keys:
*
* "oom": was the session killed by an OOM exception?
* "stopped": was the session stopped gently?
*/
public static final String MEASUREMENT_NAME_SESSIONS = "org.mozilla.appSessions";
public static final int MEASUREMENT_VERSION_SESSIONS = 4;
private void initializeSessionsProvider() {
this.storage.ensureMeasurementInitialized(
MEASUREMENT_NAME_SESSIONS,
MEASUREMENT_VERSION_SESSIONS,
new MeasurementFields() {
@Override
public Iterable<FieldSpec> getFields() {
ArrayList<FieldSpec> out = new ArrayList<FieldSpec>(2);
out.add(new FieldSpec("normal", Field.TYPE_JSON_DISCRETE));
out.add(new FieldSpec("abnormal", Field.TYPE_JSON_DISCRETE));
return out;
}
});
}
/**
* Logic shared between crashed and normal sessions.
*/
private void recordSessionEntry(String field, SessionInformation session, JSONObject value) {
try {
final int sessionField = storage.getField(MEASUREMENT_NAME_SESSIONS,
MEASUREMENT_VERSION_SESSIONS,
field)
.getID();
final int day = storage.getDay(session.wallStartTime);
storage.recordDailyDiscrete(env, day, sessionField, value);
} catch (Exception e) {
Log.w(LOG_TAG, "Unable to record session completion.", e);
}
}
public void checkForOrphanSessions() {
if (!this.orphanChecked.compareAndSet(false, true)) {
Log.w(LOG_TAG, "Attempting to check for orphan sessions more than once.");
return;
}
Log.d(LOG_TAG, "Checking for orphan session.");
if (this.previousSession == null) {
return;
}
if (this.previousSession.wallStartTime == 0) {
return;
}
if (state != State.INITIALIZED) {
// Something has gone awry.
Log.e(LOG_TAG, "Attempted to record bad session end without initialized recorder.");
return;
}
try {
recordSessionEntry("abnormal", this.previousSession, this.previousSession.getCrashedJSON());
} catch (Exception e) {
Log.w(LOG_TAG, "Unable to generate session JSON.", e);
// Future: record this exception in FHR's own error submitter.
}
}
/**
* Record that the current session ended. Does not commit the provided editor.
*/
public void recordSessionEnd(String reason, SharedPreferences.Editor editor) {
Log.d(LOG_TAG, "Recording session end: " + reason);
if (state != State.INITIALIZED) {
// Something has gone awry.
Log.e(LOG_TAG, "Attempted to record session end without initialized recorder.");
return;
}
final SessionInformation session = this.session;
this.session = null; // So it can't be double-recorded.
if (session == null) {
Log.w(LOG_TAG, "Unable to record session end: no session. Already ended?");
return;
}
if (session.wallStartTime <= 0) {
Log.e(LOG_TAG, "Session start " + session.wallStartTime + " isn't valid! Can't record end.");
return;
}
long realEndTime = android.os.SystemClock.elapsedRealtime();
try {
JSONObject json = session.getCompletionJSON(reason, realEndTime);
recordSessionEntry("normal", session, json);
} catch (JSONException e) {
Log.w(LOG_TAG, "Unable to generate session JSON.", e);
// Continue so we don't hit it next time.
// Future: record this exception in FHR's own error submitter.
}
// Track the end of this session in shared prefs, so it doesn't get
// double-counted on next run.
session.recordCompletion(editor);
}
}

View File

@ -35,23 +35,18 @@ function test_simple_source_map()
let numNewSources = 0;
gClient.addListener("newSource", function _onNewSource(aEvent, aPacket) {
if (++numNewSources !== 3) {
return;
}
gClient.removeListener("newSource", _onNewSource);
gClient.addOneTimeListener("paused", function (aEvent, aPacket) {
gThreadClient.getSources(function (aResponse) {
do_check_true(!aResponse.error, "Should not get an error");
for (let s of aResponse.sources) {
do_check_true(expectedSources.has(s.url),
"The source's url should be one of our original sources");
do_check_neq(s.url, "http://example.com/www/js/abc.js",
"Shouldn't get the generated source's url.")
expectedSources.delete(s.url);
}
do_check_eq(expectedSources.size, 0,
"Shouldn't be expecting any more sources");
"Should have found all the expected sources sources by now.");
finishClient(gClient);
});
@ -61,6 +56,7 @@ function test_simple_source_map()
new SourceNode(1, 0, "a.js", "function a() { return 'a'; }\n"),
new SourceNode(1, 0, "b.js", "function b() { return 'b'; }\n"),
new SourceNode(1, 0, "c.js", "function c() { return 'c'; }\n"),
"debugger;\n"
])).toStringWithSourceMap({
file: "abc.js",
sourceRoot: "http://example.com/www/js/"