mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-14 13:55:43 +00:00
Merge m-c to inbound.
This commit is contained in:
commit
80c2c4083c
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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/"
|
||||
|
Loading…
Reference in New Issue
Block a user