diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index 73d587afa0d..6001ce6c97a 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -18,6 +18,7 @@ plugins { id 'firebase-library' id 'com.google.protobuf' + id 'kotlin-android' } firebaseLibrary { @@ -90,6 +91,7 @@ android { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion" // Firebase Deps implementation project(':firebase-common') implementation project(':firebase-components') diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java index 3aa363e1338..3ae405501c6 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/AppStartTrace.java @@ -14,20 +14,31 @@ package com.google.firebase.perf.metrics; +import android.annotation.TargetApi; import android.app.Activity; import android.app.Application; import android.app.Application.ActivityLifecycleCallbacks; import android.content.Context; +import android.os.Build; import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.os.SystemClock; +import android.view.View; +import android.view.ViewTreeObserver; + import androidx.annotation.Keep; import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import com.google.android.gms.common.util.VisibleForTesting; import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.provider.FirebasePerfProvider; import com.google.firebase.perf.session.PerfSession; import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.transport.TransportManager; +import com.google.firebase.perf.ttid.TTIDMeasure; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Constants; import com.google.firebase.perf.util.Timer; @@ -40,6 +51,7 @@ import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; /** * A class to capture the Android AppStart Trace information. The first time activity goes through @@ -69,6 +81,7 @@ public class AppStartTrace implements ActivityLifecycleCallbacks { private boolean isRegisteredForLifecycleCallbacks = false; private final TransportManager transportManager; private final Clock clock; + private final Handler sMainThreadHandler = new Handler(Looper.getMainLooper()); private Context appContext; /** * The first time onCreate() of any activity is called, the activity is saved as launchActivity. @@ -89,7 +102,9 @@ public class AppStartTrace implements ActivityLifecycleCallbacks { private Timer onCreateTime = null; private Timer onStartTime = null; private Timer onResumeTime = null; - + private Timer onFirstDrawPierreRicau = null; + private Timer onFirstDrawP1 = null; + private long onFirstDrawElapsedRealTime; private PerfSession startSession; private boolean isStartedFromBackground = false; @@ -122,6 +137,22 @@ public static void setLauncherActivityOnResumeTime(String activity) { // no-op, for backward compatibility with old version plugin. } + private void recordFirstOnDrawPierreRicau() { + this.onFirstDrawPierreRicau = clock.getTime(); + executorService.execute(this::logTTIDPierreRicauTrace); + } + + private void recordFirstOnDrawP1() { + // Old time + this.onFirstDrawP1 = clock.getTime(); + executorService.execute(this::logTTIDPRIMESFrontOfQueueTrace); + // Process start, API 24+ only + this.onFirstDrawElapsedRealTime = SystemClock.elapsedRealtime(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + executorService.execute(this::logTTIDProcessStartElapsedRealTimeTrace); + } + } + public static AppStartTrace getInstance() { return instance != null ? instance : getInstance(TransportManager.getInstance(), new Clock()); } @@ -139,7 +170,7 @@ static AppStartTrace getInstance(TransportManager transportManager, Clock clock) MAX_POOL_SIZE, /* keepAliveTime= */ MAX_LATENCY_BEFORE_UI_INIT + 10, TimeUnit.SECONDS, - new LinkedBlockingQueue<>(1))); + new LinkedBlockingQueue<>(5))); } } } @@ -225,6 +256,19 @@ public synchronized void onActivityResumed(Activity activity) { + this.appStartTime.getDurationMicros(onResumeTime) + " microseconds"); + // Pierre-Yves Ricau's method of measuring TTID + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + TTIDMeasure.measureTTID(activity, this::recordFirstOnDrawPierreRicau); + } + + // Google P1 method of measuring TTID + View rootView = activity.findViewById(android.R.id.content); + // views are available after setContentView which should be called in onCreate + if (rootView != null) { + ViewTreeObserver observer = rootView.getViewTreeObserver(); + observer.addOnDrawListener(new FirstOnDrawListener(rootView)); + } + // Log the app start trace in a non-main thread. executorService.execute(this::logAppStartTrace); @@ -234,6 +278,42 @@ public synchronized void onActivityResumed(Activity activity) { } } + private void logTTIDPierreRicauTrace() { + TraceMetric.Builder metric = + TraceMetric.newBuilder() + .setName("TTID_PierreRicau") + .setClientStartTimeUs(getappStartTime().getMicros()) + .setDurationUs(getappStartTime().getDurationMicros(onFirstDrawPierreRicau)); + metric.addPerfSessions(this.startSession.build()); + + transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND); + } + + private void logTTIDPRIMESFrontOfQueueTrace() { + TraceMetric.Builder metric = + TraceMetric.newBuilder() + .setName("TTID_PRIMES_FrontOfQueue") + .setClientStartTimeUs(getappStartTime().getMicros()) + .setDurationUs(getappStartTime().getDurationMicros(onFirstDrawP1)); + metric.addPerfSessions(this.startSession.build()); + + transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND); + } + + private void logTTIDProcessStartElapsedRealTimeTrace() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { + return; + } + TraceMetric.Builder metric = + TraceMetric.newBuilder() + .setName("TTID_ProcessStart_elapsedRealTime") + .setClientStartTimeUs(getappStartTime().getMicros()) + .setDurationUs(TimeUnit.MILLISECONDS.toMicros(onFirstDrawElapsedRealTime - Process.getStartElapsedRealtime())); + metric.addPerfSessions(this.startSession.build()); + + transportManager.log(metric.build(), ApplicationProcessState.FOREGROUND_BACKGROUND); + } + private void logAppStartTrace() { TraceMetric.Builder metric = TraceMetric.newBuilder() @@ -303,6 +383,31 @@ public void run() { } } + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private final class FirstOnDrawListener implements ViewTreeObserver.OnDrawListener { + + private final AtomicReference view; + + private FirstOnDrawListener(View view) { + this.view = new AtomicReference<>(view); + } + + @Override + public void onDraw() { + View theView = view.getAndSet(null); + if (theView == null) { + return; + } + // OnDrawListeners cannot be removed within onDraw, so we remove it with a + // GlobalLayoutListener + theView + .getViewTreeObserver() + .addOnGlobalLayoutListener( + () -> theView.getViewTreeObserver().removeOnDrawListener(this)); + sMainThreadHandler.postAtFrontOfQueue(AppStartTrace.this::recordFirstOnDrawP1); + } + } + @VisibleForTesting @Nullable Activity getLaunchActivity() { diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/ttid/NextDrawListener.kt b/firebase-perf/src/main/java/com/google/firebase/perf/ttid/NextDrawListener.kt new file mode 100644 index 00000000000..80864b98e60 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/ttid/NextDrawListener.kt @@ -0,0 +1,54 @@ +package com.google.firebase.perf.ttid + +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.View +import android.view.ViewTreeObserver +import androidx.annotation.RequiresApi + +class NextDrawListener( + val view: View, + val onDrawCallback: () -> Unit +) : ViewTreeObserver.OnDrawListener { + + val handler = Handler(Looper.getMainLooper()) + var invoked = false + + override fun onDraw() { + if (invoked) return + invoked = true + onDrawCallback() + handler.post { + if (view.viewTreeObserver.isAlive) { + view.viewTreeObserver.removeOnDrawListener(this) + } + } + } + + companion object { + @RequiresApi(Build.VERSION_CODES.KITKAT) + fun View.onNextDraw(onDrawCallback: () -> Unit) { + if (viewTreeObserver.isAlive && isAttachedToWindow) { + addNextDrawListener(onDrawCallback) + } else { + // Wait until attached + addOnAttachStateChangeListener( + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + addNextDrawListener(onDrawCallback) + removeOnAttachStateChangeListener(this) + } + + override fun onViewDetachedFromWindow(v: View) = Unit + }) + } + } + + private fun View.addNextDrawListener(callback: () -> Unit) { + viewTreeObserver.addOnDrawListener( + NextDrawListener(this, callback) + ) + } + } +} \ No newline at end of file diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/ttid/TTIDMeasure.kt b/firebase-perf/src/main/java/com/google/firebase/perf/ttid/TTIDMeasure.kt new file mode 100644 index 00000000000..9a404b743c5 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/ttid/TTIDMeasure.kt @@ -0,0 +1,37 @@ +package com.google.firebase.perf.ttid + +import android.app.Activity +import android.os.SystemClock +import android.os.Handler +import androidx.annotation.RequiresApi +import com.google.firebase.perf.ttid.NextDrawListener.Companion.onNextDraw +import com.google.firebase.perf.ttid.WindowDelegateCallback.Companion.onDecorViewReady + +class TTIDMeasure { + interface FirstDrawCallback { + fun callback(): Unit + } + + companion object { + var firstDraw = false + val handler = Handler() + var firstDrawMs: Long = 0 + + @JvmStatic + @RequiresApi(android.os.Build.VERSION_CODES.KITKAT) + fun measureTTID(activity: Activity, callback: FirstDrawCallback) { + if (firstDraw) return + val window = activity.window + window.onDecorViewReady { + window.decorView.onNextDraw { + if (firstDraw) return@onNextDraw + firstDraw = true + handler.postAtFrontOfQueue { + firstDrawMs = SystemClock.uptimeMillis() + callback.callback() + } + } + } + } + } +} \ No newline at end of file diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/ttid/WindowDelegateCallback.kt b/firebase-perf/src/main/java/com/google/firebase/perf/ttid/WindowDelegateCallback.kt new file mode 100644 index 00000000000..d751a03ef93 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/ttid/WindowDelegateCallback.kt @@ -0,0 +1,46 @@ +package com.google.firebase.perf.ttid + +import android.view.Window + +class WindowDelegateCallback constructor( + private val delegate: Window.Callback +) : Window.Callback by delegate { + + val onContentChangedCallbacks = mutableListOf<() -> Boolean>() + + override fun onContentChanged() { + onContentChangedCallbacks.removeAll { callback -> + !callback() + } + delegate.onContentChanged() + } + + companion object { + fun Window.onDecorViewReady(callback: () -> Unit) { + if (peekDecorView() == null) { + onContentChanged { + callback() + return@onContentChanged false + } + } else { + callback() + } + } + + fun Window.onContentChanged(block: () -> Boolean) { + val callback = wrapCallback() + callback.onContentChangedCallbacks += block + } + + private fun Window.wrapCallback(): WindowDelegateCallback { + val currentCallback = callback + return if (currentCallback is WindowDelegateCallback) { + currentCallback + } else { + val newCallback = WindowDelegateCallback(currentCallback) + callback = newCallback + newCallback + } + } + } +} \ No newline at end of file