Bug 932092 - Part 2: support natively sending UI telemetry events from Java. r=mfinkle, a=bkerensa
authorRichard Newman <rnewman@mozilla.com>
Tue, 10 Dec 2013 10:41:34 -0800
changeset 174258 fd6c34135c45fe1963cb7b2d5894ddbaf55605fc
parent 174257 08035fdd06d387d03375f3856bf6eb308cc3eadd
child 174259 d9a1af3800c5ccc6008b194bf8f227686a6cad79
push id3224
push userlsblakk@mozilla.com
push dateTue, 04 Feb 2014 01:06:49 +0000
treeherdermozilla-beta@60c04d0987f1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmfinkle, bkerensa
bugs932092
milestone28.0a2
Bug 932092 - Part 2: support natively sending UI telemetry events from Java. r=mfinkle, a=bkerensa
mobile/android/base/BrowserApp.java
mobile/android/base/GeckoApp.java
mobile/android/base/GeckoEvent.java
mobile/android/base/Telemetry.java
mobile/android/base/tests/robocop.ini
mobile/android/base/tests/testUITelemetry.java
mobile/android/base/tests/testUITelemetry.js
mobile/android/chrome/content/browser.js
toolkit/components/telemetry/UITelemetry.jsm
widget/android/AndroidJavaWrappers.cpp
widget/android/AndroidJavaWrappers.h
widget/android/nsAppShell.cpp
widget/android/nsIAndroidBridge.idl
--- a/mobile/android/base/BrowserApp.java
+++ b/mobile/android/base/BrowserApp.java
@@ -424,17 +424,17 @@ abstract public class BrowserApp extends
                 final int count = BrowserDB.getReadingListCount(getContentResolver());
                 GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Reader:ListCountUpdated", Integer.toString(count)));
             }
         });
     }
 
     @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();
 
         String args = intent.getStringExtra("args");
         if (args != null && args.contains(GUEST_BROWSING_ARG)) {
             mProfile = GeckoProfile.createGuestProfile(this);
         } else {
             GeckoProfile.maybeCleanupGuestProfile(this);
--- a/mobile/android/base/GeckoApp.java
+++ b/mobile/android/base/GeckoApp.java
@@ -1148,18 +1148,18 @@ public abstract class GeckoApp
         GeckoAppShell.registerGlobalExceptionHandler();
 
         // Enable Android Strict Mode for developers' local builds (the "default" channel).
         if ("default".equals(AppConstants.MOZ_UPDATE_CHANNEL)) {
             enableStrictMode();
         }
 
         // 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");
         earlyStartJavaSampler(intent);
 
         if (mProfile == null) {
             String profileName = null;
             String profilePath = null;
--- a/mobile/android/base/GeckoEvent.java
+++ b/mobile/android/base/GeckoEvent.java
@@ -38,17 +38,17 @@ import java.nio.ByteBuffer;
 /* This class is referenced by Robocop via reflection; use care when
  * modifying the signature.
  */
 @JNITarget
 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),
         KEY_EVENT(1),
         MOTION_EVENT(2),
         SENSOR_EVENT(3),
         LOCATION_EVENT(5),
         IME_EVENT(6),
@@ -71,23 +71,26 @@ public class GeckoEvent {
         IME_KEY_EVENT(32),
         CALL_OBSERVER(33),
         REMOVE_OBSERVER(34),
         LOW_MEMORY(35),
         NETWORK_LINK_CHANGE(36),
         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;
-         }
+        }
     }
 
     /**
      * The DomKeyLocation enum encapsulates the DOM KeyboardEvent's constants.
      * @see https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent#Key_location_constants
      */
     @GeneratorOptions(generatedClassName = "JavaDomKeyLocation")
     @WrapEntireClassForJNI
@@ -742,12 +745,36 @@ public class GeckoEvent {
     public static GeckoEvent createTelemetryHistogramAddEvent(String histogram,
                                                               int value) {
         GeckoEvent event = new GeckoEvent(NativeGeckoEvent.TELEMETRY_HISTOGRAM_ADD);
         event.mCharacters = histogram;
         event.mCount = value;
         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;
     }
 }
--- a/mobile/android/base/Telemetry.java
+++ b/mobile/android/base/Telemetry.java
@@ -1,60 +1,140 @@
 /* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
  * 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/. */
 
 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() {
             mHasFinished = true;
         }
 
         public long getElapsed() {
           return mElapsed;
         }
 
         public void stop() {
             // Only the first stop counts.
             if (mHasFinished) {
                 return;
-            } else {
-                mHasFinished = true;
+            }
+
+            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;
             }
 
-            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.");
-            }
+            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);
+    }
 }
--- a/mobile/android/base/tests/robocop.ini
+++ b/mobile/android/base/tests/robocop.ini
@@ -11,40 +11,36 @@ skip-if = processor == "x86"
 # [testBookmark] # see bug 915350
 [testBookmarksPage]
 [testBookmarkFolders]
 # [testBookmarklets] # see bug 915350
 # [testBookmarkKeyword] # see bug 915350
 [testBrowserProvider]
 [testBrowserSearchVisibility]
 [testClearPrivateData]
-[testDeviceSearchEngine]
 [testDistribution]
 [testDoorHanger]
 [testFindInPage]
 [testFlingCorrectness]
 # disabled on x86 only; bug 927476
 skip-if = processor == "x86"
 [testFormHistory]
 [testGetUserMedia]
 # [testHistory] # see bug 915350
 [testHomeBanner]
 [testImportFromAndroid]
 # disabled on x86 only; bug 900664
 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
 skip-if = processor == "x86"
 # [testPasswordEncrypt] # see bug 824067
 [testPasswordProvider]
 # [testPermissions] # see bug 757475
 [testPictureLinkContextMenu]
@@ -53,24 +49,30 @@ skip-if = processor == "x86"
 [testPromptGridInput]
 # [testReaderMode] # see bug 913254
 # disabled on x86 only; bug 936224
 # skip-if = processor == "x86"
 [testSearchSuggestions]
 [testSessionOOMSave]
 [testSessionOOMRestore]
 [testSettingsMenuItems]
-[testSharedPreferences]
 # [testShareLink] # see bug 915897
 [testSystemPages]
 # disabled on x86 only; bug 907383
 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]
 #[testCheck]
 #[testCheck2]
 #[testBrowserProviderPerf]
 
 # Using UITest
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testUITelemetry.java
@@ -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();
+    }
+}
+
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/tests/testUITelemetry.js
@@ -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();
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -333,17 +333,16 @@ var BrowserApp = {
     ActivityObserver.init();
     WebappsUI.init();
     RemoteDebugger.init();
     Reader.init();
     UserAgentOverrides.init();
     DesktopUserAgent.init();
     Distribution.init();
     Tabs.init();
-    UITelemetry.init();
 #ifdef ACCESSIBILITY
     AccessFu.attach(window);
 #endif
 
     // Init LoginManager
     Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
 
     let url = null;
@@ -1525,16 +1524,20 @@ var BrowserApp = {
     return this.defaultBrowserWidth = width;
   },
 
   // nsIAndroidBrowserApp
   getBrowserTab: function(tabId) {
     return this.getTabForId(tabId);
   },
 
+  getUITelemetryObserver: function() {
+    return UITelemetry;
+  },
+
   getPreferences: function getPreferences(requestId, prefNames, count) {
     this.handlePreferencesRequest(requestId, prefNames, false);
   },
 
   observePreferences: function observePreferences(requestId, prefNames, count) {
     this.handlePreferencesRequest(requestId, prefNames, true);
   },
 
--- a/toolkit/components/telemetry/UITelemetry.jsm
+++ b/toolkit/components/telemetry/UITelemetry.jsm
@@ -2,152 +2,144 @@
  * 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/. */
 
 "use strict";
 
 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 =  {
-
-  measurements: [],
-
-  init: function init() {
-    Services.obs.addObserver(this, "UITelemetry:Event", false);
-    Services.obs.addObserver(this, "UITelemetry:Session", false);
-  },
+this.UITelemetry = Object.freeze({
+  _activeSessions: {},
+  _measurements: [],
 
-  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;
+    }
+
+    this._recordEvent(aEvent);
   },
 
-  activeSessions: {},
-
   /**
    * 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!");
       return;
     }
 
     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
    * via addSimpleMeasureFunction and return an object with the
    * results of those functions.
    */
   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;
   },
 
   /**
    * Allows the caller to register functions that will get called
    * for simple measures during a Telemetry ping. aName is a unique
    * identifier used as they key for the simple measurement in the
    * object that getSimpleMeasures returns.
    *
    * This function throws an exception if aName already has a function
    * 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();
   }
-};
+});
--- a/widget/android/AndroidJavaWrappers.cpp
+++ b/widget/android/AndroidJavaWrappers.cpp
@@ -559,16 +559,37 @@ AndroidGeckoEvent::Init(JNIEnv *jenv, jo
         }
 
         case TELEMETRY_HISTOGRAM_ADD: {
             ReadCharactersField(jenv);
             mCount = jenv->GetIntField(jobj, jCountField);
             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);
             mCount = jenv->GetIntField(jobj, jCountField);
             break;
         }
 
         case PREFERENCES_REMOVE_OBSERVERS: {
--- a/widget/android/AndroidJavaWrappers.h
+++ b/widget/android/AndroidJavaWrappers.h
@@ -681,21 +681,24 @@ public:
         REMOVE_OBSERVER = 34,
         LOW_MEMORY = 35,
         NETWORK_LINK_CHANGE = 36,
         TELEMETRY_HISTOGRAM_ADD = 37,
         ADD_OBSERVER = 38,
         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,
         MEMORY_PRESSURE_MEDIUM = 3,
         MEMORY_PRESSURE_HIGH = 4
     };
 
     enum {
--- a/widget/android/nsAppShell.cpp
+++ b/widget/android/nsAppShell.cpp
@@ -254,18 +254,17 @@ nsAppShell::ProcessNextNativeEvent(bool 
 
     EVLOG("nsAppShell: event %p %d", (void*)curEvent.get(), curEvent->Type());
 
     switch (curEvent->Type()) {
     case AndroidGeckoEvent::NATIVE_POKE:
         NativeEventCallback();
         break;
 
-    case AndroidGeckoEvent::SENSOR_EVENT:
-      {
+    case AndroidGeckoEvent::SENSOR_EVENT: {
         InfallibleTArray<float> values;
         mozilla::hal::SensorType type = (mozilla::hal::SensorType) curEvent->Flags();
 
         switch (type) {
           // Bug 938035, transfer HAL data for orientation sensor to meet w3c
           // spec, ex: HAL report alpha=90 means East but alpha=90 means West
           // in w3c spec
           case hal::SENSOR_ORIENTATION:
@@ -366,30 +365,80 @@ nsAppShell::ProcessNextNativeEvent(bool 
         RefCountedJavaObject* buffer = curEvent->ByteBuffer();
         nsCOMPtr<ThumbnailRunnable> sr = new ThumbnailRunnable(mBrowserApp, tabId, points, buffer);
         MessageLoop::current()->PostIdleTask(FROM_HERE, NewRunnableMethod(sr.get(), &ThumbnailRunnable::Run));
         break;
     }
 
     case AndroidGeckoEvent::VIEWPORT:
     case AndroidGeckoEvent::BROADCAST: {
-
         if (curEvent->Characters().Length() == 0)
             break;
 
         nsCOMPtr<nsIObserverService> obsServ =
             mozilla::services::GetObserverService();
 
         const NS_ConvertUTF16toUTF8 topic(curEvent->Characters());
         const nsPromiseFlatString& data = PromiseFlatString(curEvent->CharactersExtra());
 
         obsServ->NotifyObservers(nullptr, topic.get(), data.get());
         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"));
         if (!cmdline)
             break;
 
         if (curEvent->Characters().Length() == 0)
             break;
--- a/widget/android/nsIAndroidBridge.idl
+++ b/widget/android/nsIAndroidBridge.idl
@@ -6,27 +6,42 @@
 #include "nsIDOMWindow.idl"
 
 [scriptable, uuid(0843f3c1-043e-4c64-9d8c-091370548c05)]
 interface nsIBrowserTab : nsISupports {
   readonly attribute nsIDOMWindow window;
   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,
                       [array, size_is(count)] in wstring prefNames,
                       in unsigned long count);
   void observePreferences(in int32_t requestId,
                           [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;
   readonly attribute float y;
   readonly attribute float width;
   readonly attribute float height;
   readonly attribute float pageLeft;
   readonly attribute float pageTop;