Skip to content

Commit 6540c0f

Browse files
Merge 0ae206b into 1d344e8
2 parents 1d344e8 + 0ae206b commit 6540c0f

File tree

7 files changed

+260
-20
lines changed

7 files changed

+260
-20
lines changed

firebase-perf/dev-app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@
3838
<meta-data
3939
android:name="fragment_sampling_percentage"
4040
android:value="100.0" />
41+
<meta-data
42+
android:name="experiment_app_start_ttid"
43+
android:value="true" />
4144

4245
<receiver
4346
android:name=".FirebasePerfTestReceiver"

firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.google.firebase.perf.BuildConfig;
2424
import com.google.firebase.perf.config.ConfigurationConstants.CollectionDeactivated;
2525
import com.google.firebase.perf.config.ConfigurationConstants.CollectionEnabled;
26+
import com.google.firebase.perf.config.ConfigurationConstants.ExperimentTTID;
2627
import com.google.firebase.perf.config.ConfigurationConstants.FragmentSamplingRate;
2728
import com.google.firebase.perf.config.ConfigurationConstants.LogSourceName;
2829
import com.google.firebase.perf.config.ConfigurationConstants.NetworkEventCountBackground;
@@ -767,6 +768,38 @@ public float getFragmentSamplingRate() {
767768
return config.getDefault();
768769
}
769770

771+
/** Returns if _experiment_app_start_ttid should be captured. */
772+
public boolean getIsExperimentTTIDEnabled() {
773+
// Order of precedence is:
774+
// 1. If the value exists in Android Manifest, return this value.
775+
// 2. If the value exists through Firebase Remote Config, cache and return this value.
776+
// 3. If the value exists in device cache, return this value.
777+
// 4. Otherwise, return default value.
778+
ExperimentTTID config = ExperimentTTID.getInstance();
779+
780+
// 1. Reads value in Android Manifest (it is set by developers during build time).
781+
Optional<Boolean> metadataValue = getMetadataBoolean(config);
782+
if (metadataValue.isAvailable()) {
783+
return metadataValue.get();
784+
}
785+
786+
// 2. Reads value from Firebase Remote Config, saves this value in cache layer if valid.
787+
Optional<Boolean> rcValue = getRemoteConfigBoolean(config);
788+
if (rcValue.isAvailable()) {
789+
deviceCacheManager.setValue(config.getDeviceCacheFlag(), rcValue.get());
790+
return rcValue.get();
791+
}
792+
793+
// 3. Reads value from cache layer.
794+
Optional<Boolean> deviceCacheValue = getDeviceCacheBoolean(config);
795+
if (deviceCacheValue.isAvailable()) {
796+
return deviceCacheValue.get();
797+
}
798+
799+
// 4. Returns default value if there is no valid value from above approaches.
800+
return config.getDefault();
801+
}
802+
770803
// endregion
771804

772805
// Helper functions for interaction with Metadata layer.

firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigurationConstants.java

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,4 +661,39 @@ protected String getMetadataFlag() {
661661
return "fragment_sampling_percentage";
662662
}
663663
}
664+
665+
protected static final class ExperimentTTID extends ConfigurationFlag<Boolean> {
666+
private static ExperimentTTID instance;
667+
668+
private ExperimentTTID() {
669+
super();
670+
}
671+
672+
protected static synchronized ExperimentTTID getInstance() {
673+
if (instance == null) {
674+
instance = new ExperimentTTID();
675+
}
676+
return instance;
677+
}
678+
679+
@Override
680+
protected Boolean getDefault() {
681+
return false;
682+
}
683+
684+
@Override
685+
protected String getRemoteConfigFlag() {
686+
return "fpr_experiment_app_start_ttid";
687+
}
688+
689+
@Override
690+
protected String getDeviceCacheFlag() {
691+
return "com.google.firebase.perf.ExperimentTTID";
692+
}
693+
694+
@Override
695+
protected String getMetadataFlag() {
696+
return "experiment_app_start_ttid";
697+
}
698+
}
664699
}

firebase-perf/src/main/java/com/google/firebase/perf/config/RemoteConfigManager.java

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414

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

17+
import static com.google.firebase.perf.config.ConfigurationConstants.ExperimentTTID;
18+
1719
import android.content.Context;
1820
import android.content.pm.PackageInfo;
1921
import android.content.pm.PackageManager.NameNotFoundException;
@@ -53,6 +55,7 @@ public class RemoteConfigManager {
5355
private static final long MIN_APP_START_CONFIG_FETCH_DELAY_MS = 5000;
5456
private static final int RANDOM_APP_START_CONFIG_FETCH_DELAY_MS = 25000;
5557

58+
private final DeviceCacheManager cache;
5659
private final ConcurrentHashMap<String, FirebaseRemoteConfigValue> allRcConfigMap;
5760
private final Executor executor;
5861
private final long appStartTimeInMs;
@@ -65,29 +68,25 @@ public class RemoteConfigManager {
6568

6669
private RemoteConfigManager() {
6770
this(
71+
DeviceCacheManager.getInstance(),
6872
new ThreadPoolExecutor(
6973
/* corePoolSize= */ 0,
7074
/* maximumPoolSize= */ 1,
7175
/* keepAliveTime= */ 0L,
7276
TimeUnit.SECONDS,
7377
new LinkedBlockingQueue<Runnable>()),
74-
/* firebaseRemoteConfig= */ null // set once FirebaseRemoteConfig is initialized
75-
);
76-
}
77-
78-
RemoteConfigManager(Executor executor, FirebaseRemoteConfig firebaseRemoteConfig) {
79-
this(
80-
executor,
81-
firebaseRemoteConfig,
78+
/* firebaseRemoteConfig= */ null, // set once FirebaseRemoteConfig is initialized
8279
MIN_APP_START_CONFIG_FETCH_DELAY_MS
8380
+ new Random().nextInt(RANDOM_APP_START_CONFIG_FETCH_DELAY_MS));
8481
}
8582

8683
@VisibleForTesting
8784
RemoteConfigManager(
85+
DeviceCacheManager cache,
8886
Executor executor,
8987
FirebaseRemoteConfig firebaseRemoteConfig,
9088
long appStartConfigFetchDelayInMs) {
89+
this.cache = cache;
9190
this.executor = executor;
9291
this.firebaseRemoteConfig = firebaseRemoteConfig;
9392
this.allRcConfigMap =
@@ -315,7 +314,7 @@ private void triggerRemoteConfigFetchIfNecessary() {
315314
return;
316315
}
317316
if (allRcConfigMap.isEmpty()) { // Initial fetch.
318-
syncConfigValues(firebaseRemoteConfig.getAll());
317+
allRcConfigMap.putAll(firebaseRemoteConfig.getAll());
319318
}
320319
if (shouldFetchAndActivateRemoteConfigValues()) {
321320
triggerFirebaseRemoteConfigFetchAndActivateOnSuccessfulFetch();
@@ -342,6 +341,21 @@ protected void syncConfigValues(Map<String, FirebaseRemoteConfigValue> newlyFetc
342341
allRcConfigMap.remove(existingKey);
343342
}
344343
}
344+
345+
// TODO: remove after experiment is over and experiment RC flag is no longer needed
346+
// Save ExperimentTTID flag to device cache upon successful RC fetchAndActivate, because reading
347+
// is done quite early and it is possible that RC isn't initialized yet.
348+
ExperimentTTID flag = ExperimentTTID.getInstance();
349+
FirebaseRemoteConfigValue rcValue = allRcConfigMap.get(flag.getRemoteConfigFlag());
350+
if (rcValue != null) {
351+
try {
352+
cache.setValue(flag.getDeviceCacheFlag(), rcValue.asBoolean());
353+
} catch (Exception exception) {
354+
logger.debug("ExperimentTTID remote config flag has invalid value, expected boolean.");
355+
}
356+
} else {
357+
logger.debug("ExperimentTTID remote config flag does not exist.");
358+
}
345359
}
346360

347361
@VisibleForTesting

firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java

Lines changed: 69 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,23 @@
1818
import android.app.Application;
1919
import android.app.Application.ActivityLifecycleCallbacks;
2020
import android.content.Context;
21+
import android.os.Build;
2122
import android.os.Bundle;
23+
import android.os.Process;
24+
import android.view.View;
2225
import androidx.annotation.Keep;
2326
import androidx.annotation.NonNull;
2427
import androidx.annotation.Nullable;
2528
import com.google.android.gms.common.util.VisibleForTesting;
29+
import com.google.firebase.perf.config.ConfigResolver;
2630
import com.google.firebase.perf.logging.AndroidLogger;
2731
import com.google.firebase.perf.provider.FirebasePerfProvider;
2832
import com.google.firebase.perf.session.PerfSession;
2933
import com.google.firebase.perf.session.SessionManager;
3034
import com.google.firebase.perf.transport.TransportManager;
3135
import com.google.firebase.perf.util.Clock;
3236
import com.google.firebase.perf.util.Constants;
37+
import com.google.firebase.perf.util.FirstDrawDoneListener;
3338
import com.google.firebase.perf.util.Timer;
3439
import com.google.firebase.perf.v1.ApplicationProcessState;
3540
import com.google.firebase.perf.v1.TraceMetric;
@@ -69,6 +74,7 @@ public class AppStartTrace implements ActivityLifecycleCallbacks {
6974
private boolean isRegisteredForLifecycleCallbacks = false;
7075
private final TransportManager transportManager;
7176
private final Clock clock;
77+
private final ConfigResolver configResolver;
7278
private Context appContext;
7379
/**
7480
* The first time onCreate() of any activity is called, the activity is saved as launchActivity.
@@ -89,6 +95,7 @@ public class AppStartTrace implements ActivityLifecycleCallbacks {
8995
private Timer onCreateTime = null;
9096
private Timer onStartTime = null;
9197
private Timer onResumeTime = null;
98+
private Timer firstDrawDone = null;
9299

93100
private PerfSession startSession;
94101
private boolean isStartedFromBackground = false;
@@ -134,12 +141,13 @@ static AppStartTrace getInstance(TransportManager transportManager, Clock clock)
134141
new AppStartTrace(
135142
transportManager,
136143
clock,
144+
ConfigResolver.getInstance(),
137145
new ThreadPoolExecutor(
138146
CORE_POOL_SIZE,
139147
MAX_POOL_SIZE,
140148
/* keepAliveTime= */ MAX_LATENCY_BEFORE_UI_INIT + 10,
141149
TimeUnit.SECONDS,
142-
new LinkedBlockingQueue<>(1)));
150+
new LinkedBlockingQueue<>()));
143151
}
144152
}
145153
}
@@ -149,9 +157,11 @@ static AppStartTrace getInstance(TransportManager transportManager, Clock clock)
149157
AppStartTrace(
150158
@NonNull TransportManager transportManager,
151159
@NonNull Clock clock,
160+
@NonNull ConfigResolver configResolver,
152161
@NonNull ExecutorService executorService) {
153162
this.transportManager = transportManager;
154163
this.clock = clock;
164+
this.configResolver = configResolver;
155165
this.executorService = executorService;
156166
}
157167

@@ -178,6 +188,33 @@ public synchronized void unregisterActivityLifecycleCallbacks() {
178188
isRegisteredForLifecycleCallbacks = false;
179189
}
180190

191+
/**
192+
* Gets the timetamp that marks the beginning of app start, currently defined as the beginning of
193+
* BIND_APPLICATION. Fallback to class-load time of {@link FirebasePerfProvider} when API < 24.
194+
*
195+
* @return {@link Timer} at the beginning of app start by Fireperf definition.
196+
*/
197+
private static Timer getStartTimer() {
198+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
199+
return Timer.ofElapsedRealtime(Process.getStartElapsedRealtime());
200+
}
201+
return FirebasePerfProvider.getAppStartTime();
202+
}
203+
204+
private void recordFirstDrawDone() {
205+
if (firstDrawDone != null) {
206+
return;
207+
}
208+
this.firstDrawDone = clock.getTime();
209+
executorService.execute(
210+
() -> this.logColdStart(getStartTimer(), this.firstDrawDone, this.startSession));
211+
212+
if (isRegisteredForLifecycleCallbacks) {
213+
// After AppStart trace is queued to be logged, we can unregister this callback.
214+
unregisterActivityLifecycleCallbacks();
215+
}
216+
}
217+
181218
@Override
182219
public synchronized void onActivityCreated(Activity activity, Bundle savedInstanceState) {
183220
if (isStartedFromBackground || onCreateTime != null // An activity already called onCreate()
@@ -206,9 +243,18 @@ public synchronized void onActivityStarted(Activity activity) {
206243

207244
@Override
208245
public synchronized void onActivityResumed(Activity activity) {
209-
if (isStartedFromBackground
210-
|| onResumeTime != null // An activity already called onResume()
211-
|| isTooLateToInitUI) {
246+
if (isStartedFromBackground || isTooLateToInitUI) {
247+
return;
248+
}
249+
250+
// Shadow-launch experiment of new app start time
251+
final boolean isExperimentTTIDEnabled = configResolver.getIsExperimentTTIDEnabled();
252+
if (isExperimentTTIDEnabled) {
253+
View rootView = activity.findViewById(android.R.id.content);
254+
FirstDrawDoneListener.registerForNextDraw(rootView, this::recordFirstDrawDone);
255+
}
256+
257+
if (onResumeTime != null) { // An activity already called onResume()
212258
return;
213259
}
214260

@@ -228,12 +274,30 @@ public synchronized void onActivityResumed(Activity activity) {
228274
// Log the app start trace in a non-main thread.
229275
executorService.execute(this::logAppStartTrace);
230276

231-
if (isRegisteredForLifecycleCallbacks) {
277+
if (!isExperimentTTIDEnabled && isRegisteredForLifecycleCallbacks) {
232278
// After AppStart trace is logged, we can unregister this callback.
233279
unregisterActivityLifecycleCallbacks();
234280
}
235281
}
236282

283+
private void logColdStart(Timer start, Timer end, PerfSession session) {
284+
TraceMetric.Builder metric =
285+
TraceMetric.newBuilder()
286+
.setName("_experiment_app_start_ttid")
287+
.setClientStartTimeUs(start.getMicros())
288+
.setDurationUs(start.getDurationMicros(end));
289+
290+
TraceMetric.Builder subtrace =
291+
TraceMetric.newBuilder()
292+
.setName("_experiment_classLoadTime")
293+
.setClientStartTimeUs(FirebasePerfProvider.getAppStartTime().getMicros())
294+
.setDurationUs(FirebasePerfProvider.getAppStartTime().getDurationMicros(end));
295+
296+
metric.addSubtraces(subtrace).addPerfSessions(this.startSession.build());
297+
298+
transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND);
299+
}
300+
237301
private void logAppStartTrace() {
238302
TraceMetric.Builder metric =
239303
TraceMetric.newBuilder()

0 commit comments

Comments
 (0)