зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1243595 - Add SessionMeasurements and tests. r=ahunt
MozReview-Commit-ID: NEKDPblKEE --HG-- extra : rebase_source : d1a75c6ad5c202718a785b4b99dda27d1a08b7b9
This commit is contained in:
Родитель
d6c3b6486a
Коммит
cd62b5cf48
|
@ -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);
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче