diff --git a/dom/mobilemessage/tests/marionette/test_strict_7bit_encoding.js b/dom/mobilemessage/tests/marionette/test_strict_7bit_encoding.js
index 0dc80f85cbb9..b0da1372144f 100644
--- a/dom/mobilemessage/tests/marionette/test_strict_7bit_encoding.js
+++ b/dom/mobilemessage/tests/marionette/test_strict_7bit_encoding.js
@@ -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();
diff --git a/dom/system/gonk/NetworkManager.js b/dom/system/gonk/NetworkManager.js
index 3affc853603f..fa7c4290b15a 100644
--- a/dom/system/gonk/NetworkManager.js
+++ b/dom/system/gonk/NetworkManager.js
@@ -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;
diff --git a/hal/gonk/GonkHal.cpp b/hal/gonk/GonkHal.cpp
index 8c2dce347082..0a60cf7da30d 100644
--- a/hal/gonk/GonkHal.cpp
+++ b/hal/gonk/GonkHal.cpp
@@ -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) {
diff --git a/mobile/android/base/GeckoApp.java b/mobile/android/base/GeckoApp.java
index 910d50ea58d9..5bdd58928c3f 100644
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -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();
diff --git a/mobile/android/base/Telemetry.java b/mobile/android/base/Telemetry.java
index 256f418f61a1..ae115cb459d7 100644
--- a/mobile/android/base/Telemetry.java
+++ b/mobile/android/base/Telemetry.java
@@ -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.");
}
}
}
diff --git a/mobile/android/base/background/healthreport/Environment.java b/mobile/android/base/background/healthreport/Environment.java
index 211b28ca2f75..41b8a4117b94 100644
--- a/mobile/android/base/background/healthreport/Environment.java
+++ b/mobile/android/base/background/healthreport/Environment.java
@@ -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);
}
diff --git a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
index fa9cc366d0bd..5e71e6b2bfd7 100644
--- a/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
+++ b/mobile/android/base/background/healthreport/HealthReportDatabaseStorage.java
@@ -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);
diff --git a/mobile/android/base/background/healthreport/HealthReportGenerator.java b/mobile/android/base/background/healthreport/HealthReportGenerator.java
index c689e8f01601..0b791768834d 100644
--- a/mobile/android/base/background/healthreport/HealthReportGenerator.java
+++ b/mobile/android/base/background/healthreport/HealthReportGenerator.java
@@ -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 NULL
or
+ * string "null"
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));
}
diff --git a/mobile/android/base/background/healthreport/HealthReportStorage.java b/mobile/android/base/background/healthreport/HealthReportStorage.java
index 7f19f5bdd20a..262c0fbcff6b 100644
--- a/mobile/android/base/background/healthreport/HealthReportStorage.java
+++ b/mobile/android/base/background/healthreport/HealthReportStorage.java
@@ -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 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);
diff --git a/mobile/android/base/health/BrowserHealthRecorder.java b/mobile/android/base/health/BrowserHealthRecorder.java
index 224f0850999f..b640b207ec09 100644
--- a/mobile/android/base/health/BrowserHealthRecorder.java
+++ b/mobile/android/base/health/BrowserHealthRecorder.java
@@ -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 getFields() {
+ ArrayList out = new ArrayList(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);
+ }
}
diff --git a/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js b/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js
index c2df4b632e16..bc60f56fdafe 100644
--- a/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js
+++ b/toolkit/devtools/server/tests/unit/test_sourcemaps-02.js
@@ -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/"