Skip to content

Commit 9ded389

Browse files
Merge d2b9225 into f5c9e3b
2 parents f5c9e3b + d2b9225 commit 9ded389

File tree

3 files changed

+148
-65
lines changed

3 files changed

+148
-65
lines changed

firebase-perf/src/main/java/com/google/firebase/perf/util/Timer.java

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -14,92 +14,134 @@
1414

1515
package com.google.firebase.perf.util;
1616

17+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
18+
import static java.util.concurrent.TimeUnit.NANOSECONDS;
19+
20+
import android.os.Build;
1721
import android.os.Parcel;
1822
import android.os.Parcelable;
23+
import android.os.SystemClock;
1924
import androidx.annotation.NonNull;
2025
import com.google.android.gms.common.util.VisibleForTesting;
21-
import java.util.concurrent.TimeUnit;
2226

23-
/** A Timer class provides both wall-clock (epoch) time and high resolution time (nano time). */
27+
/**
28+
* A Timer class provides both wall-clock (epoch) time and monotonic time (elapsedRealtime).
29+
* Timestamps are captured with millisecond-precision, because that's the time unit most widely
30+
* available in Android APIs. However, private fields and public method returns are in microseconds
31+
* due to Fireperf proto requirements.
32+
*/
2433
public class Timer implements Parcelable {
2534

26-
/** Wall-clock time or epoch time in microseconds, */
27-
private long timeInMicros;
2835
/**
29-
* High resolution time in nanoseconds. High resolution time should only be used to calculate
30-
* duration or latency. It is not wall-clock time.
36+
* Wall-clock time or epoch time in microseconds. Do NOT use for duration because wall-clock is
37+
* not guaranteed to be monotonic: it can be set by the user or the phone network, thus it may
38+
* jump forwards or backwards unpredictably. {@see SystemClock}
39+
*/
40+
private long wallClockMicros;
41+
/**
42+
* Monotonic time measured in the {@link SystemClock#elapsedRealtime()} timebase. Only used to
43+
* compute duration between 2 timestamps in the same timebase. It is NOT wall-clock time.
44+
*/
45+
private long elapsedRealtimeMicros;
46+
47+
/**
48+
* Returns a new Timer object as if it was stamped at the given elapsedRealtime. Uses current
49+
* wall-clock as a reference to extrapolate the wall-clock at the given elapsedRealtime.
50+
*
51+
* @param elapsedRealtimeMillis timestamp in the {@link SystemClock#elapsedRealtime()} timebase
52+
*/
53+
public static Timer ofElapsedRealtime(final long elapsedRealtimeMillis) {
54+
long elapsedRealtimeMicros = MILLISECONDS.toMicros(elapsedRealtimeMillis);
55+
long wallClockMicros = wallClockMicros() + (elapsedRealtimeMicros - elapsedRealtimeMicros());
56+
return new Timer(wallClockMicros, elapsedRealtimeMicros);
57+
}
58+
59+
/**
60+
* Helper to get current wall-clock time from system API.
61+
*
62+
* @return wall-clock time in microseconds.
3163
*/
32-
private long highResTime;
64+
private static long wallClockMicros() {
65+
return MILLISECONDS.toMicros(System.currentTimeMillis());
66+
}
3367

3468
/**
35-
* Construct Timer object using System clock. Make it package visible to be only accessible from
36-
* com.google.firebase.perf.util.Clock.
69+
* Helper to get current {@link SystemClock#elapsedRealtime()} from system API.
70+
*
71+
* @return wall-clock time in microseconds.
3772
*/
73+
private static long elapsedRealtimeMicros() {
74+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
75+
return NANOSECONDS.toMicros(SystemClock.elapsedRealtimeNanos());
76+
}
77+
return MILLISECONDS.toMicros(SystemClock.elapsedRealtime());
78+
}
79+
80+
// TODO: make all constructors private, use public static factory methods, per Effective Java
81+
/** Construct Timer object using System clock. */
3882
public Timer() {
39-
timeInMicros = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
40-
highResTime = System.nanoTime();
83+
this(wallClockMicros(), elapsedRealtimeMicros());
4184
}
4285

4386
/**
44-
* Construct a Timer object with input wall-clock time, assume high resolution time is same as
45-
* wall-clock time.
87+
* Construct a Timer object with input wall-clock time and elapsedRealtime.
4688
*
47-
* @param time wall-clock time in microseconds
89+
* @param epochMicros wall-clock time in milliseconds since epoch
90+
* @param elapsedRealtimeMicros monotonic time in microseconds in the {@link
91+
* SystemClock#elapsedRealtime()} timebase
4892
*/
4993
@VisibleForTesting
50-
public Timer(long time) {
51-
this.timeInMicros = time;
52-
highResTime = TimeUnit.MICROSECONDS.toNanos(time);
94+
Timer(long epochMicros, long elapsedRealtimeMicros) {
95+
this.wallClockMicros = epochMicros;
96+
this.elapsedRealtimeMicros = elapsedRealtimeMicros;
5397
}
5498

5599
/**
56-
* Construct a Timer object with input wall-clock time and high resolution time.
100+
* TEST-ONLY constructor that sets both wall-clock time and elapsedRealtime to the same input
101+
* value. Do NOT use this for any real logic because this is mixing 2 different time-bases.
57102
*
58-
* @param time wall-clock time in microseconds
59-
* @param highResTime high resolution time in nanoseconds
103+
* @param testTime value to set both wall-clock and elapsedRealtime to for testing purposes
60104
*/
61105
@VisibleForTesting
62-
public Timer(long time, long highResTime) {
63-
this.timeInMicros = time;
64-
this.highResTime = highResTime;
106+
public Timer(long testTime) {
107+
this(testTime, testTime);
65108
}
66109

67110
private Timer(Parcel in) {
68-
timeInMicros = in.readLong();
69-
highResTime = in.readLong();
111+
this(in.readLong(), in.readLong());
70112
}
71113

72114
/** resets the start time */
73115
public void reset() {
74-
timeInMicros = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
75-
highResTime = System.nanoTime();
116+
// TODO: consider removing this method and make Timer immutable thus fully thread-safe
117+
wallClockMicros = wallClockMicros();
118+
elapsedRealtimeMicros = elapsedRealtimeMicros();
76119
}
77120

78121
/** Return wall-clock time in microseconds. */
79122
public long getMicros() {
80-
return timeInMicros;
123+
return wallClockMicros;
81124
}
82125

83126
/**
84-
* Calculate duration in microseconds using the the high resolution time.
127+
* Calculate duration in microseconds using elapsedRealtime.
85128
*
86129
* <p>The start time is this Timer object, end time is current time.
87130
*
88131
* @return duration in microseconds.
89132
*/
90133
public long getDurationMicros() {
91-
return TimeUnit.NANOSECONDS.toMicros(System.nanoTime() - this.highResTime);
134+
return getDurationMicros(new Timer());
92135
}
93136

94137
/**
95-
* Calculate duration in microseconds using the the high resolution time. The start time is this
96-
* Timer object.
138+
* Calculate duration in microseconds using elapsedRealtime. The start time is this Timer object.
97139
*
98140
* @param end end Timer object
99141
* @return duration in microseconds.
100142
*/
101143
public long getDurationMicros(@NonNull final Timer end) {
102-
return TimeUnit.NANOSECONDS.toMicros(end.highResTime - this.highResTime);
144+
return end.elapsedRealtimeMicros - this.elapsedRealtimeMicros;
103145
}
104146

105147
/**
@@ -111,17 +153,7 @@ public long getDurationMicros(@NonNull final Timer end) {
111153
* was created.
112154
*/
113155
public long getCurrentTimestampMicros() {
114-
return timeInMicros + getDurationMicros();
115-
}
116-
117-
/**
118-
* Return high resolution time in microseconds, only useful for testing..
119-
*
120-
* @return high resolution time in microseconds.
121-
*/
122-
@VisibleForTesting
123-
public long getHighResTime() {
124-
return TimeUnit.NANOSECONDS.toMicros(highResTime);
156+
return wallClockMicros + getDurationMicros();
125157
}
126158

127159
/**
@@ -132,8 +164,8 @@ public long getHighResTime() {
132164
* @param flags always will be the value 0.
133165
*/
134166
public void writeToParcel(Parcel out, int flags) {
135-
out.writeLong(timeInMicros);
136-
out.writeLong(highResTime);
167+
out.writeLong(wallClockMicros);
168+
out.writeLong(elapsedRealtimeMicros);
137169
}
138170

139171
/**

firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
package com.google.firebase.perf.metrics;
1616

17+
import static com.google.firebase.perf.util.TimerTest.getElapsedRealtimeMicros;
1718
import static org.mockito.Mockito.doAnswer;
1819
import static org.mockito.Mockito.mock;
1920
import static org.mockito.Mockito.times;
@@ -63,11 +64,8 @@ public class AppStartTraceTest extends FirebasePerformanceTestBase {
6364
// a mocked current wall-clock time in microseconds.
6465
private long currentTime = 0;
6566

66-
// wall-clock time in microseconds
67-
private long appStartTime;
68-
69-
// high resolution time in microseconds
70-
private long appStartHRT;
67+
// Timer at the beginning of app startup
68+
private Timer appStart;
7169

7270
@Before
7371
public void setUp() {
@@ -83,8 +81,7 @@ public Timer answer(InvocationOnMock invocationOnMock) throws Throwable {
8381
.getTime();
8482
transportManager = mock(TransportManager.class);
8583
traceArgumentCaptor = ArgumentCaptor.forClass(TraceMetric.class);
86-
appStartTime = FirebasePerfProvider.getAppStartTime().getMicros();
87-
appStartHRT = FirebasePerfProvider.getAppStartTime().getHighResTime();
84+
appStart = FirebasePerfProvider.getAppStartTime();
8885
}
8986

9087
@After
@@ -144,14 +141,15 @@ private void verifyFinalState(
144141
TraceMetric metric = traceArgumentCaptor.getValue();
145142

146143
Assert.assertEquals(Constants.TraceNames.APP_START_TRACE_NAME.toString(), metric.getName());
147-
Assert.assertEquals(appStartTime, metric.getClientStartTimeUs());
148-
Assert.assertEquals(resumeTime - appStartHRT, metric.getDurationUs());
144+
Assert.assertEquals(appStart.getMicros(), metric.getClientStartTimeUs());
145+
Assert.assertEquals(resumeTime - getElapsedRealtimeMicros(appStart), metric.getDurationUs());
149146

150147
Assert.assertEquals(3, metric.getSubtracesCount());
151148
Assert.assertEquals(
152149
Constants.TraceNames.ON_CREATE_TRACE_NAME.toString(), metric.getSubtraces(0).getName());
153-
Assert.assertEquals(appStartTime, metric.getSubtraces(0).getClientStartTimeUs());
154-
Assert.assertEquals(createTime - appStartHRT, metric.getSubtraces(0).getDurationUs());
150+
Assert.assertEquals(appStart.getMicros(), metric.getSubtraces(0).getClientStartTimeUs());
151+
Assert.assertEquals(
152+
createTime - getElapsedRealtimeMicros(appStart), metric.getSubtraces(0).getDurationUs());
155153

156154
Assert.assertEquals(
157155
Constants.TraceNames.ON_START_TRACE_NAME.toString(), metric.getSubtraces(1).getName());
@@ -210,7 +208,7 @@ public void testDelayedAppStart() {
210208
FakeScheduledExecutorService fakeExecutorService = new FakeScheduledExecutorService();
211209
AppStartTrace trace = new AppStartTrace(transportManager, clock, fakeExecutorService);
212210
// Delays activity creation after 1 minute from app start time.
213-
currentTime = appStartTime + TimeUnit.MINUTES.toMicros(1) + 1;
211+
currentTime = appStart.getMicros() + TimeUnit.MINUTES.toMicros(1) + 1;
214212
trace.onActivityCreated(activity1, bundle);
215213
Assert.assertEquals(currentTime, trace.getOnCreateTime().getMicros());
216214
++currentTime;

firebase-perf/src/test/java/com/google/firebase/perf/util/TimerTest.java

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,75 @@
1515
package com.google.firebase.perf.util;
1616

1717
import static com.google.common.truth.Truth.assertThat;
18+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
1819

1920
import android.os.Parcel;
20-
import java.util.concurrent.TimeUnit;
21+
import android.os.SystemClock;
22+
import java.time.Duration;
2123
import org.junit.Assert;
2224
import org.junit.Before;
2325
import org.junit.Test;
2426
import org.junit.runner.RunWith;
2527
import org.robolectric.RobolectricTestRunner;
26-
27-
/** Unit tests for {@link Timer}. */
28+
import org.robolectric.shadows.ShadowSystemClock;
29+
30+
/**
31+
* Unit tests for {@link Timer}.
32+
*
33+
* <p>IMPORTANT: {@link SystemClock} is paused in tests, and can only be advanced through specific
34+
* methods such as {@link ShadowSystemClock#advanceBy(Duration)} which are shadowed by Robolectic.
35+
*/
2836
@RunWith(RobolectricTestRunner.class)
2937
public class TimerTest {
3038

3139
@Before
3240
public void setUp() {}
3341

42+
@Test
43+
public void getDurationMicros_returnsDifferenceBetweenElapsedRealtime() {
44+
// Robolectric shadows SystemClock, which is paused and can only change via specific methods.
45+
Timer start = new Timer();
46+
ShadowSystemClock.advanceBy(Duration.ofMillis(100000));
47+
Timer end = new Timer();
48+
assertThat(start.getDurationMicros(end)).isEqualTo(MILLISECONDS.toMicros(100000));
49+
}
50+
51+
@Test
52+
public void ofElapsedRealtime_createsNewTimerWithArgumentElapsedRealtime() {
53+
// Robolectric shadows SystemClock, which is paused and can only change via specific methods.
54+
long refElapsedRealtime = SystemClock.elapsedRealtime();
55+
Timer ref = new Timer();
56+
Timer past = Timer.ofElapsedRealtime(refElapsedRealtime - 100);
57+
Timer future = Timer.ofElapsedRealtime(refElapsedRealtime + 100);
58+
59+
assertThat(past.getDurationMicros(ref)).isEqualTo(MILLISECONDS.toMicros(100));
60+
assertThat(ref.getDurationMicros(future)).isEqualTo(MILLISECONDS.toMicros(100));
61+
}
62+
63+
@Test
64+
public void ofElapsedRealtime_extrapolatesWallTime() {
65+
// Robolectric shadows SystemClock, which is paused and can only change via specific methods.
66+
// Advance SystemClock by a large amount to avoid negative time
67+
ShadowSystemClock.advanceBy(Duration.ofMillis(10000000));
68+
long nowElapsedRealtime = SystemClock.elapsedRealtime();
69+
Timer now = new Timer();
70+
Timer morePast = Timer.ofElapsedRealtime(nowElapsedRealtime - 2000);
71+
Timer past = Timer.ofElapsedRealtime(nowElapsedRealtime - 1000);
72+
Timer future = Timer.ofElapsedRealtime(nowElapsedRealtime + 1000);
73+
Timer moreFuture = Timer.ofElapsedRealtime(nowElapsedRealtime + 2000);
74+
75+
// We cannot manipulate System.currentTimeMillis() so multiple comparisons are used to test
76+
assertThat(morePast.getMicros()).isLessThan(past.getMicros());
77+
assertThat(past.getMicros()).isLessThan(now.getMicros());
78+
assertThat(now.getMicros()).isLessThan(future.getMicros());
79+
assertThat(future.getMicros()).isLessThan(moreFuture.getMicros());
80+
}
81+
3482
@Test
3583
public void testCreate() throws InterruptedException {
3684
Timer timer = new Timer();
3785
Thread.sleep(10);
38-
long currentTimeMicros = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
86+
long currentTimeMicros = MILLISECONDS.toMicros(System.currentTimeMillis());
3987

4088
assertThat(timer.getMicros()).isNotEqualTo(currentTimeMicros);
4189
assertThat(timer.getMicros()).isLessThan(currentTimeMicros);
@@ -45,7 +93,7 @@ public void testCreate() throws InterruptedException {
4593
public void testReset() throws InterruptedException {
4694
Timer timer = new Timer();
4795
Thread.sleep(10);
48-
long currentTimeMicros = TimeUnit.MILLISECONDS.toMicros(System.currentTimeMillis());
96+
long currentTimeMicros = MILLISECONDS.toMicros(System.currentTimeMillis());
4997

5098
assertThat(timer.getMicros()).isNotEqualTo(currentTimeMicros);
5199
timer.reset();
@@ -55,7 +103,7 @@ public void testReset() throws InterruptedException {
55103

56104
@Test
57105
public void testGetCurrentTimestampMicros() {
58-
Timer timer = new Timer(0);
106+
Timer timer = new Timer(0, 0);
59107
long currentTimeSmallest = timer.getCurrentTimestampMicros();
60108

61109
assertThat(timer.getMicros()).isEqualTo(0);
@@ -76,9 +124,14 @@ public void testParcel() {
76124

77125
Timer timer2 = Timer.CREATOR.createFromParcel(p2);
78126
Assert.assertEquals(timer1.getMicros(), timer2.getMicros());
79-
Assert.assertEquals(timer1.getHighResTime(), timer2.getHighResTime());
127+
Assert.assertEquals(timer1.getDurationMicros(timer2), 0);
80128

81129
p1.recycle();
82130
p2.recycle();
83131
}
132+
133+
/** Helper for other tests that returns elapsedRealtimeMicros from a Timer object */
134+
public static long getElapsedRealtimeMicros(Timer timer) {
135+
return new Timer(0, 0).getDurationMicros(timer);
136+
}
84137
}

0 commit comments

Comments
 (0)