Bug 932092 - Part 2: support natively sending UI telemetry events from Java. r=mfinkle

This commit is contained in:
Richard Newman 2013-12-10 10:41:34 -08:00
parent 7a2f204cff
commit fb6845b990
13 changed files with 397 additions and 104 deletions

View File

@ -429,7 +429,7 @@ abstract public class BrowserApp extends GeckoApp
@Override
public void onCreate(Bundle savedInstanceState) {
mAboutHomeStartupTimer = new Telemetry.Timer("FENNEC_STARTUP_TIME_ABOUTHOME");
mAboutHomeStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_ABOUTHOME");
final Intent intent = getIntent();

View File

@ -1153,8 +1153,8 @@ public abstract class GeckoApp
}
// The clock starts...now. Better hurry!
mJavaUiStartupTimer = new Telemetry.Timer("FENNEC_STARTUP_TIME_JAVAUI");
mGeckoReadyStartupTimer = new Telemetry.Timer("FENNEC_STARTUP_TIME_GECKOREADY");
mJavaUiStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_JAVAUI");
mGeckoReadyStartupTimer = new Telemetry.UptimeTimer("FENNEC_STARTUP_TIME_GECKOREADY");
Intent intent = getIntent();
String args = intent.getStringExtra("args");

View File

@ -43,7 +43,7 @@ public class GeckoEvent {
private static final String LOGTAG = "GeckoEvent";
// Make sure to keep these values in sync with the enum in
// AndroidGeckoEvent in widget/android/AndroidJavaWrapper.h
// AndroidGeckoEvent in widget/android/AndroidJavaWrappers.h
@JNITarget
private enum NativeGeckoEvent {
NATIVE_POKE(0),
@ -76,13 +76,16 @@ public class GeckoEvent {
TELEMETRY_HISTOGRAM_ADD(37),
PREFERENCES_OBSERVE(39),
PREFERENCES_GET(40),
PREFERENCES_REMOVE_OBSERVERS(41);
PREFERENCES_REMOVE_OBSERVERS(41),
TELEMETRY_UI_SESSION_START(42),
TELEMETRY_UI_SESSION_STOP(43),
TELEMETRY_UI_EVENT(44);
public final int value;
private NativeGeckoEvent(int value) {
this.value = value;
}
}
}
/**
@ -747,6 +750,30 @@ public class GeckoEvent {
return event;
}
public static GeckoEvent createTelemetryUISessionStartEvent(String session, long timestamp) {
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.TELEMETRY_UI_SESSION_START);
event.mCharacters = session;
event.mTime = timestamp;
return event;
}
public static GeckoEvent createTelemetryUISessionStopEvent(String session, String reason, long timestamp) {
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.TELEMETRY_UI_SESSION_STOP);
event.mCharacters = session;
event.mCharactersExtra = reason;
event.mTime = timestamp;
return event;
}
public static GeckoEvent createTelemetryUIEvent(String action, String method, long timestamp, String extras) {
GeckoEvent event = new GeckoEvent(NativeGeckoEvent.TELEMETRY_UI_EVENT);
event.mData = action;
event.mCharacters = method;
event.mCharactersExtra = extras;
event.mTime = timestamp;
return event;
}
public void setAckNeeded(boolean ackNeeded) {
mAckNeeded = ackNeeded;
}

View File

@ -5,31 +5,52 @@
package org.mozilla.gecko;
import org.mozilla.gecko.mozglue.RobocopTarget;
import android.os.SystemClock;
import android.util.Log;
/**
* All telemetry times are relative to one of two clocks:
*
* * Real time since the device was booted, including deep sleep. Use this
* as a substitute for wall clock.
* * Uptime since the device was booted, excluding deep sleep. Use this to
* avoid timing a user activity when their phone is in their pocket!
*
* The majority of methods in this class are defined in terms of real time.
*/
@RobocopTarget
public class Telemetry {
private static final String LOGTAG = "Telemetry";
public static long uptime() {
return SystemClock.uptimeMillis();
}
public static long realtime() {
return SystemClock.elapsedRealtime();
}
// Define new histograms in:
// toolkit/components/telemetry/Histograms.json
public static void HistogramAdd(String name,
int value) {
GeckoEvent event =
GeckoEvent.createTelemetryHistogramAddEvent(name, value);
public static void HistogramAdd(String name, int value) {
GeckoEvent event = GeckoEvent.createTelemetryHistogramAddEvent(name, value);
GeckoAppShell.sendEventToGecko(event);
}
public static class Timer {
private long mStartTime;
private String mName;
private boolean mHasFinished;
public abstract static class Timer {
private final long mStartTime;
private final String mName;
private volatile boolean mHasFinished = false;
private volatile long mElapsed = -1;
protected abstract long now();
public Timer(String name) {
mName = name;
mStartTime = SystemClock.uptimeMillis();
mHasFinished = false;
mStartTime = now();
}
public void cancel() {
@ -44,17 +65,76 @@ public class Telemetry {
// Only the first stop counts.
if (mHasFinished) {
return;
} else {
mHasFinished = true;
}
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 to add to histogram.");
mHasFinished = true;
final long elapsed = now() - mStartTime;
if (elapsed < 0) {
Log.e(LOGTAG, "Current time less than start time -- clock shenanigans?");
return;
}
mElapsed = elapsed;
if (elapsed > Integer.MAX_VALUE) {
Log.e(LOGTAG, "Duration of " + elapsed + "ms is too great to add to histogram.");
return;
}
HistogramAdd(mName, (int)(elapsed));
}
}
public static class RealtimeTimer extends Timer {
public RealtimeTimer(String name) {
super(name);
}
@Override
protected long now() {
return Telemetry.realtime();
}
}
public static class UptimeTimer extends Timer {
public UptimeTimer(String name) {
super(name);
}
@Override
protected long now() {
return Telemetry.uptime();
}
}
public static void startUISession(String sessionName) {
GeckoEvent event = GeckoEvent.createTelemetryUISessionStartEvent(sessionName, realtime());
GeckoAppShell.sendEventToGecko(event);
}
public static void stopUISession(String sessionName, String reason) {
GeckoEvent event = GeckoEvent.createTelemetryUISessionStopEvent(sessionName, reason, realtime());
GeckoAppShell.sendEventToGecko(event);
}
public static void sendUIEvent(String action, String method, long timestamp, String extras) {
GeckoEvent event = GeckoEvent.createTelemetryUIEvent(action, method, timestamp, extras);
GeckoAppShell.sendEventToGecko(event);
}
public static void sendUIEvent(String action, String method, long timestamp) {
sendUIEvent(action, method, timestamp, null);
}
public static void sendUIEvent(String action, String method, String extras) {
sendUIEvent(action, method, realtime(), extras);
}
public static void sendUIEvent(String action, String method) {
sendUIEvent(action, method, realtime(), null);
}
public static void sendUIEvent(String action) {
sendUIEvent(action, null, realtime(), null);
}
}

View File

@ -16,7 +16,6 @@ skip-if = processor == "x86"
[testBrowserProvider]
[testBrowserSearchVisibility]
[testClearPrivateData]
[testDeviceSearchEngine]
[testDistribution]
[testDoorHanger]
[testFindInPage]
@ -32,14 +31,11 @@ skip-if = processor == "x86"
skip-if = processor == "x86"
[testInputUrlBar]
[testJarReader]
[testJNI]
[testLinkContextMenu]
[testLoad]
[testMailToContextMenu]
[testMasterPassword]
# [testMozPay] # see bug 945675
[testNewTab]
[testOrderedBroadcast]
[testOverscroll]
[testPanCorrectness]
# disabled on x86 only; bug 927476
@ -58,7 +54,6 @@ skip-if = processor == "x86"
[testSessionOOMSave]
[testSessionOOMRestore]
[testSettingsMenuItems]
[testSharedPreferences]
# [testShareLink] # see bug 915897
[testSystemPages]
# disabled on x86 only; bug 907383
@ -66,6 +61,13 @@ skip-if = processor == "x86"
# [testThumbnails] # see bug 813107
# [testVkbOverlap] # see bug 907274
# Using JavascriptTest
[testDeviceSearchEngine]
[testJNI]
# [testMozPay] # see bug 945675
[testOrderedBroadcast]
[testSharedPreferences]
[testUITelemetry]
# Used for Talos, please don't use in mochitest
#[testPan]

View File

@ -0,0 +1,38 @@
package org.mozilla.gecko.tests;
import org.mozilla.gecko.Telemetry;
import android.util.Log;
public class testUITelemetry extends JavascriptTest {
public testUITelemetry() {
super("testUITelemetry.js");
}
@Override
public void testJavascript() throws Exception {
blockForGeckoReady();
Log.i("GeckoTest", "Adding telemetry events.");
try {
Telemetry.sendUIEvent("enone", "method0");
Telemetry.startUISession("foo");
Telemetry.sendUIEvent("efoo", "method1");
Telemetry.startUISession("foo");
Telemetry.sendUIEvent("efoo", "method2");
Telemetry.startUISession("bar");
Telemetry.sendUIEvent("efoobar", "method3", "foobarextras");
Telemetry.stopUISession("foo", "reasonfoo");
Telemetry.sendUIEvent("ebar", "method4", "barextras");
Telemetry.stopUISession("bar", "reasonbar");
Telemetry.stopUISession("bar", "reasonbar2");
Telemetry.sendUIEvent("enone", "method5");
} catch (Exception e) {
Log.e("GeckoTest", "Oops.", e);
}
Log.i("GeckoTest", "Running remaining JS test code.");
super.testJavascript();
}
}

View File

@ -0,0 +1,63 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
Components.utils.import("resource://gre/modules/Services.jsm");
function do_check_array_eq(a1, a2) {
do_check_eq(a1.length, a2.length);
for (let i = 0; i < a1.length; ++i) {
do_check_eq(a1[i], a2[i]);
}
}
add_test(function test_telemetry_events() {
let bridge = Components.classes["@mozilla.org/android/bridge;1"]
.getService(Components.interfaces.nsIAndroidBridge);
let obsXPCOM = bridge.browserApp.getUITelemetryObserver();
do_check_true(!!obsXPCOM);
let obs = obsXPCOM.wrappedJSObject;
do_check_true(!!obs);
let measurements = obs.getUIMeasurements();
let expected = [
["event", "enone", "method0", [], null],
["event", "efoo", "method1", ["foo"], null],
["event", "efoo", "method2", ["foo"], null],
["event", "efoobar", "method3", ["foo", "bar"], "foobarextras"],
["session", "foo", "reasonfoo"],
["event", "ebar", "method4", ["bar"], "barextras"],
["session", "bar", "reasonbar"],
["event", "enone", "method5", [], null],
];
do_check_eq(expected.length, measurements.length);
for (let i = 0; i < measurements.length; ++i) {
let m = measurements[i];
let type = m[0];
if (type == "event") {
let [type, action, method, sessions, extras] = expected[i];
do_check_eq(m.action, action);
do_check_eq(m.method, method);
do_check_array_eq(m.sessions, sessions);
do_check_eq(m.extras, extras);
continue;
}
if (type == "session") {
let [type, name, reason] = expected[i];
do_check_eq(m.name, name);
do_check_eq(m.reason, method);
continue;
}
}
run_next_test();
});
run_next_test();

View File

@ -338,7 +338,6 @@ var BrowserApp = {
DesktopUserAgent.init();
Distribution.init();
Tabs.init();
UITelemetry.init();
#ifdef ACCESSIBILITY
AccessFu.attach(window);
#endif
@ -1530,6 +1529,10 @@ var BrowserApp = {
return this.getTabForId(tabId);
},
getUITelemetryObserver: function() {
return UITelemetry;
},
getPreferences: function getPreferences(requestId, prefNames, count) {
this.handlePreferencesRequest(requestId, prefNames, false);
},

View File

@ -7,80 +7,78 @@
const Cu = Components.utils;
this.EXPORTED_SYMBOLS = [
"UITelemetry"
"UITelemetry",
];
Cu.import("resource://gre/modules/Services.jsm");
/**
* UITelemetry is a helper JSM used to record UI specific telemetry events.
*
* It implements nsIUITelemetryObserver, defined in nsIAndroidBridge.idl.
*/
this.UITelemetry = {
this.UITelemetry = Object.freeze({
_activeSessions: {},
_measurements: [],
measurements: [],
init: function init() {
Services.obs.addObserver(this, "UITelemetry:Event", false);
Services.obs.addObserver(this, "UITelemetry:Session", false);
},
observe: function observe(aMessage, aTopic, aData) {
switch(aTopic) {
case "UITelemetry:Event":
let args = JSON.parse(aData);
this.addEvent(args.action, args.method, args.extras, args.timestamp);
break;
case "UITelemetry:Session":
args = JSON.parse(aData);
let sessionName = args.name;
let timestamp = args.timestamp;
if (args.state == "start") {
this.startSession(sessionName, timestamp);
} else if (args.state == "stop") {
this.stopSession(sessionName, timestamp);
}
break;
}
/**
* This exists exclusively for testing -- our events are not intended to
* be retrieved via an XPCOM interface.
*/
get wrappedJSObject() {
return this;
},
/**
* Adds a single event described by an action, and the calling method. Optional
* paramaters are extras and timestamp. The timestamp will be set here if it is
* not passed in by the caller.
* Holds the functions that provide UITelemetry's simple
* measurements. Those functions are mapped to unique names,
* and should be registered with addSimpleMeasureFunction.
*/
addEvent: function addEvent(aAction, aMethod, aExtras, aTimestamp) {
let timestamp = aTimestamp || Date.now();
_simpleMeasureFunctions: {},
/**
* Adds a single event described by a timestamp, an action, and the calling
* method.
*
* Optionally provide a string 'extras', which will be recorded as part of
* the event.
*
* All extant sessions will be recorded by name for each event.
*/
addEvent: function(aAction, aMethod, aTimestamp, aExtras) {
let sessions = Object.keys(this._activeSessions);
let aEvent = {
type: "event",
action: aAction,
method: aMethod,
timestamp: timestamp
sessions: sessions,
timestamp: aTimestamp,
};
if (aExtras) aEvent.extras = aExtras;
this._logEvent(aEvent);
},
if (aExtras) {
aEvent.extras = aExtras;
}
activeSessions: {},
this._recordEvent(aEvent);
},
/**
* Begins tracking a session by storing a timestamp for session start.
*/
startSession: function startSession(aName, aTimestamp) {
let timestamp = aTimestamp || Date.now();
if (this.activeSessions[aName]) {
// Do not overwrite a previous event start if it already exsts.
return;
}
this.activeSessions[aName] = timestamp;
startSession: function(aName, aTimestamp) {
if (this._activeSessions[aName]) {
// Do not overwrite a previous event start if it already exists.
return;
}
this._activeSessions[aName] = aTimestamp;
},
/**
* Tracks the end of a session with a timestamp.
*/
stopSession: function stopSession(aName, aTimestamp) {
let timestamp = aTimestamp || Date.now();
let sessionStart = this.activeSessions[aName];
stopSession: function(aName, aReason, aTimestamp) {
let sessionStart = this._activeSessions[aName];
delete this._activeSessions[aName];
if (!sessionStart) {
Services.console.logStringMessage("UITelemetry error: no session [" + aName + "] to stop!");
@ -90,24 +88,18 @@ this.UITelemetry = {
let aEvent = {
type: "session",
name: aName,
reason: aReason,
start: sessionStart,
end: timestamp
end: aTimestamp,
};
this._logEvent(aEvent);
this._recordEvent(aEvent);
},
_logEvent: function sendEvent(aEvent) {
this.measurements.push(aEvent);
_recordEvent: function(aEvent) {
this._measurements.push(aEvent);
},
/**
* Holds the functions that provide UITelemety's simple
* measurements. Those functions are mapped to unique names,
* and should be registered with addSimpleMeasureFunction.
*/
_simpleMeasureFuncs: {},
/**
* Called by TelemetryPing to populate the simple measurement
* blob. This function will iterate over all functions added
@ -116,8 +108,8 @@ this.UITelemetry = {
*/
getSimpleMeasures: function() {
let result = {};
for (let name in this._simpleMeasureFuncs) {
result[name] = this._simpleMeasureFuncs[name]();
for (let name in this._simpleMeasureFunctions) {
result[name] = this._simpleMeasureFunctions[name]();
}
return result;
},
@ -132,22 +124,22 @@ this.UITelemetry = {
* registered for it.
*/
addSimpleMeasureFunction: function(aName, aFunction) {
if (aName in this._simpleMeasureFuncs) {
throw new Error("A simple measurement function is already registered for "
+ aName);
}
if (!aFunction || typeof aFunction !== 'function') {
throw new Error("A function must be passed as the second argument.");
if (aName in this._simpleMeasureFunctions) {
throw new Error("A simple measurement function is already registered for " + aName);
}
this._simpleMeasureFuncs[aName] = aFunction;
if (!aFunction || typeof aFunction !== 'function') {
throw new Error("addSimpleMeasureFunction called with non-function argument.");
}
this._simpleMeasureFunctions[aName] = aFunction;
},
removeSimpleMeasureFunction: function(aName) {
delete this._simpleMeasureFuncs[aName];
delete this._simpleMeasureFunctions[aName];
},
getUIMeasurements: function getUIMeasurements() {
return this.measurements.slice();
return this._measurements.slice();
}
};
});

View File

@ -564,6 +564,27 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jobject jobj)
break;
}
case TELEMETRY_UI_SESSION_START: {
ReadCharactersField(jenv);
mTime = jenv->GetLongField(jobj, jTimeField);
break;
}
case TELEMETRY_UI_SESSION_STOP: {
ReadCharactersField(jenv);
ReadCharactersExtraField(jenv);
mTime = jenv->GetLongField(jobj, jTimeField);
break;
}
case TELEMETRY_UI_EVENT: {
ReadCharactersField(jenv);
ReadCharactersExtraField(jenv);
ReadDataField(jenv);
mTime = jenv->GetLongField(jobj, jTimeField);
break;
}
case PREFERENCES_OBSERVE:
case PREFERENCES_GET: {
ReadStringArray(mPrefNames, jenv, jPrefNamesField);

View File

@ -686,11 +686,14 @@ public:
PREFERENCES_OBSERVE = 39,
PREFERENCES_GET = 40,
PREFERENCES_REMOVE_OBSERVERS = 41,
TELEMETRY_UI_SESSION_START = 42,
TELEMETRY_UI_SESSION_STOP = 43,
TELEMETRY_UI_EVENT = 44,
dummy_java_enum_list_end
};
enum {
// Memory pressue levels, keep in sync with those in MemoryMonitor.java
// Memory pressure levels. Keep these in sync with those in MemoryMonitor.java.
MEMORY_PRESSURE_NONE = 0,
MEMORY_PRESSURE_CLEANUP = 1,
MEMORY_PRESSURE_LOW = 2,

View File

@ -259,8 +259,7 @@ nsAppShell::ProcessNextNativeEvent(bool mayWait)
NativeEventCallback();
break;
case AndroidGeckoEvent::SENSOR_EVENT:
{
case AndroidGeckoEvent::SENSOR_EVENT: {
InfallibleTArray<float> values;
mozilla::hal::SensorType type = (mozilla::hal::SensorType) curEvent->Flags();
@ -371,7 +370,6 @@ nsAppShell::ProcessNextNativeEvent(bool mayWait)
case AndroidGeckoEvent::VIEWPORT:
case AndroidGeckoEvent::BROADCAST: {
if (curEvent->Characters().Length() == 0)
break;
@ -385,6 +383,57 @@ nsAppShell::ProcessNextNativeEvent(bool mayWait)
break;
}
case AndroidGeckoEvent::TELEMETRY_UI_SESSION_STOP: {
if (curEvent->Characters().Length() == 0)
break;
nsCOMPtr<nsIUITelemetryObserver> obs;
mBrowserApp->GetUITelemetryObserver(getter_AddRefs(obs));
if (!obs)
break;
obs->StopSession(
nsString(curEvent->Characters()).get(),
nsString(curEvent->CharactersExtra()).get(),
curEvent->Time()
);
break;
}
case AndroidGeckoEvent::TELEMETRY_UI_SESSION_START: {
if (curEvent->Characters().Length() == 0)
break;
nsCOMPtr<nsIUITelemetryObserver> obs;
mBrowserApp->GetUITelemetryObserver(getter_AddRefs(obs));
if (!obs)
break;
obs->StartSession(
nsString(curEvent->Characters()).get(),
curEvent->Time()
);
break;
}
case AndroidGeckoEvent::TELEMETRY_UI_EVENT: {
if (curEvent->Characters().Length() == 0)
break;
nsCOMPtr<nsIUITelemetryObserver> obs;
mBrowserApp->GetUITelemetryObserver(getter_AddRefs(obs));
if (!obs)
break;
obs->AddEvent(
nsString(curEvent->Data()).get(),
nsString(curEvent->Characters()).get(),
curEvent->Time(),
nsString(curEvent->CharactersExtra()).get()
);
break;
}
case AndroidGeckoEvent::LOAD_URI: {
nsCOMPtr<nsICommandLineRunner> cmdline
(do_CreateInstance("@mozilla.org/toolkit/command-line;1"));

View File

@ -11,7 +11,20 @@ interface nsIBrowserTab : nsISupports {
readonly attribute float scale;
};
[scriptable, uuid(7508b826-4129-40a0-91da-2a6bba33681f)]
[scriptable, uuid(08426a73-e70b-4680-9282-630932e2b2bb)]
interface nsIUITelemetryObserver : nsISupports {
void startSession(in wstring name,
in unsigned long timestamp);
void stopSession(in wstring name,
in wstring reason,
in unsigned long timestamp);
void addEvent(in wstring action,
in wstring method,
in unsigned long timestamp,
in wstring extras);
};
[scriptable, uuid(c31331d2-afad-460f-9c66-728b8c838cec)]
interface nsIAndroidBrowserApp : nsISupports {
nsIBrowserTab getBrowserTab(in int32_t tabId);
void getPreferences(in int32_t requestId,
@ -21,7 +34,9 @@ interface nsIAndroidBrowserApp : nsISupports {
[array, size_is(count)] in wstring prefNames,
in unsigned long count);
void removePreferenceObservers(in int32_t requestId);
nsIUITelemetryObserver getUITelemetryObserver();
};
[scriptable, uuid(59cfcb35-69b7-47b2-8155-32b193272666)]
interface nsIAndroidViewport : nsISupports {
readonly attribute float x;