Bug 1243595 - Add SessionMeasurements and tests. r=ahunt

MozReview-Commit-ID: NEKDPblKEE

--HG--
extra : rebase_source : d1a75c6ad5c202718a785b4b99dda27d1a08b7b9
This commit is contained in:
Michael Comella 2016-05-17 14:56:00 -07:00
Родитель d6c3b6486a
Коммит cd62b5cf48
3 изменённых файлов: 224 добавлений и 0 удалений

Просмотреть файл

@ -0,0 +1,99 @@
/*
* 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.telemetry.measurements;
import android.content.Context;
import android.content.SharedPreferences;
import android.support.annotation.UiThread;
import android.support.annotation.VisibleForTesting;
import org.mozilla.gecko.GeckoSharedPrefs;
import java.util.concurrent.TimeUnit;
/**
* A class to measure the number of user sessions & their durations. It was created for use with the
* telemetry core ping. A session is the time between {@link #recordSessionStart()} and
* {@link #recordSessionEnd(Context)}.
*
* This class is thread-safe, provided the thread annotations are followed. Under the hood, this class uses
* SharedPreferences & because there is no atomic getAndSet operation, we synchronize access to it.
*/
public class SessionMeasurements {
@VisibleForTesting static final String PREF_SESSION_COUNT = "measurements-session-count";
@VisibleForTesting static final String PREF_SESSION_DURATION = "measurements-session-duration";
private boolean sessionStarted = false;
private long timeAtSessionStartNano = -1;
@UiThread // we assume this will be called on the same thread as session end so we don't have to synchronize sessionStarted.
public void recordSessionStart() {
if (sessionStarted) {
throw new IllegalStateException("Trying to start session but it is already started");
}
sessionStarted = true;
timeAtSessionStartNano = getSystemTimeNano();
}
@UiThread // we assume this will be called on the same thread as session start so we don't have to synchronize sessionStarted.
public void recordSessionEnd(final Context context) {
if (!sessionStarted) {
throw new IllegalStateException("Expected session to be started before session end is called");
}
sessionStarted = false;
final long sessionElapsedSeconds = TimeUnit.NANOSECONDS.toSeconds(getSystemTimeNano() - timeAtSessionStartNano);
final SharedPreferences sharedPrefs = getSharedPreferences(context);
synchronized (this) {
final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0);
final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0);
sharedPrefs.edit()
.putInt(PREF_SESSION_COUNT, sessionCount + 1)
.putLong(PREF_SESSION_DURATION, totalElapsedSeconds + sessionElapsedSeconds)
.apply();
}
}
/**
* Gets the session measurements since the last time the measurements were last retrieved.
*/
public synchronized SessionMeasurementsContainer getAndResetSessionMeasurements(final Context context) {
final SharedPreferences sharedPrefs = getSharedPreferences(context);
final int sessionCount = sharedPrefs.getInt(PREF_SESSION_COUNT, 0);
final long totalElapsedSeconds = sharedPrefs.getLong(PREF_SESSION_DURATION, 0);
sharedPrefs.edit()
.putInt(PREF_SESSION_COUNT, 0)
.putLong(PREF_SESSION_DURATION, 0)
.apply();
return new SessionMeasurementsContainer(sessionCount, totalElapsedSeconds);
}
@VisibleForTesting SharedPreferences getSharedPreferences(final Context context) {
return GeckoSharedPrefs.forProfile(context);
}
/**
* Returns (roughly) the system uptime in nanoseconds. A less coupled implementation would
* take this value from the caller of recordSession*, however, we do this internally to ensure
* the caller uses both a time system consistent between the start & end calls and uses the
* appropriate time system (i.e. not wall time, which can change when the clock is changed).
*/
@VisibleForTesting long getSystemTimeNano() { // TODO: necessary?
return System.nanoTime();
}
public static final class SessionMeasurementsContainer {
/** The number of sessions. */
public final int sessionCount;
/** The number of seconds elapsed in ALL sessions included in {@link #sessionCount}. */
public final long elapsedSeconds;
private SessionMeasurementsContainer(final int sessionCount, final long elapsedSeconds) {
this.sessionCount = sessionCount;
this.elapsedSeconds = elapsedSeconds;
}
}
}

Просмотреть файл

@ -580,6 +580,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/measurements/SearchCountMeasurements.java',
'telemetry/measurements/SessionMeasurements.java',
'telemetry/pingbuilders/TelemetryCorePingBuilder.java',
'telemetry/pingbuilders/TelemetryPingBuilder.java',
'telemetry/schedulers/TelemetryUploadAllPingsImmediatelyScheduler.java',

Просмотреть файл

@ -0,0 +1,124 @@
/*
* 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.telemetry.measurements;
import android.content.Context;
import android.content.SharedPreferences;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.background.testhelpers.TestRunner;
import org.mozilla.gecko.telemetry.measurements.SessionMeasurements.SessionMeasurementsContainer;
import org.robolectric.RuntimeEnvironment;
import java.util.concurrent.TimeUnit;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
/**
* Tests the session measurements class.
*/
@RunWith(TestRunner.class)
public class TestSessionMeasurements {
private SessionMeasurements testMeasurements;
private SharedPreferences sharedPrefs;
private Context context;
@Before
public void setUp() throws Exception {
testMeasurements = spy(SessionMeasurements.class);
sharedPrefs = RuntimeEnvironment.application.getSharedPreferences(
TestSessionMeasurements.class.getSimpleName(), Context.MODE_PRIVATE);
doReturn(sharedPrefs).when(testMeasurements).getSharedPreferences(any(Context.class));
context = RuntimeEnvironment.application;
}
private void assertSessionCount(final String postfix, final int expectedSessionCount) {
final int actual = sharedPrefs.getInt(SessionMeasurements.PREF_SESSION_COUNT, -1);
assertEquals("Expected number of sessions occurred " + postfix, expectedSessionCount, actual);
}
private void assertSessionDuration(final String postfix, final long expectedSessionDuration) {
final long actual = sharedPrefs.getLong(SessionMeasurements.PREF_SESSION_DURATION, -1);
assertEquals("Expected session duration received " + postfix, expectedSessionDuration, actual);
}
private void mockGetSystemTimeNanosToReturn(final long value) {
doReturn(value).when(testMeasurements).getSystemTimeNano();
}
@Test
public void testRecordSessionStartAndEndCalledOnce() throws Exception {
final long expectedElapsedSeconds = 4;
mockGetSystemTimeNanosToReturn(0);
testMeasurements.recordSessionStart();
mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos(expectedElapsedSeconds));
testMeasurements.recordSessionEnd(context);
final String postfix = "after recordSessionStart/End called once";
assertSessionCount(postfix, 1);
assertSessionDuration(postfix, expectedElapsedSeconds);
}
@Test
public void testRecordSessionStartAndEndCalledTwice() throws Exception {
final long expectedElapsedSeconds = 100;
mockGetSystemTimeNanosToReturn(0L);
for (int i = 1; i <= 2; ++i) {
testMeasurements.recordSessionStart();
mockGetSystemTimeNanosToReturn(TimeUnit.SECONDS.toNanos((expectedElapsedSeconds / 2) * i));
testMeasurements.recordSessionEnd(context);
}
final String postfix = "after recordSessionStart/End called twice";
assertSessionCount(postfix, 2);
assertSessionDuration(postfix, expectedElapsedSeconds);
}
@Test(expected = IllegalStateException.class)
public void testRecordSessionStartThrowsIfSessionAlreadyStarted() throws Exception {
// First call will start the session, next expected to throw.
for (int i = 0; i < 2; ++i) {
testMeasurements.recordSessionStart();
}
}
@Test(expected = IllegalStateException.class)
public void testRecordSessionEndThrowsIfCalledBeforeSessionStarted() {
testMeasurements.recordSessionEnd(context);
}
@Test // assumes the underlying format in SessionMeasurements
public void testGetAndResetSessionMeasurementsReturnsSetData() throws Exception {
final int expectedSessionCount = 42;
final long expectedSessionDuration = 1234567890;
sharedPrefs.edit()
.putInt(SessionMeasurements.PREF_SESSION_COUNT, expectedSessionCount)
.putLong(SessionMeasurements.PREF_SESSION_DURATION, expectedSessionDuration)
.apply();
final SessionMeasurementsContainer actual = testMeasurements.getAndResetSessionMeasurements(context);
assertEquals("Returned session count matches expected", expectedSessionCount, actual.sessionCount);
assertEquals("Returned session duration matches expected", expectedSessionDuration, actual.elapsedSeconds);
}
@Test
public void testGetAndResetSessionMeasurementsResetsData() throws Exception {
sharedPrefs.edit()
.putInt(SessionMeasurements.PREF_SESSION_COUNT, 10)
.putLong(SessionMeasurements.PREF_SESSION_DURATION, 10)
.apply();
testMeasurements.getAndResetSessionMeasurements(context);
final String postfix = "is reset after retrieval";
assertSessionCount(postfix, 0);
assertSessionDuration(postfix, 0);
}
}