diff --git a/firebase-perf/CHANGELOG.md b/firebase-perf/CHANGELOG.md index b4db8bb6bc4..314f0ad55a8 100644 --- a/firebase-perf/CHANGELOG.md +++ b/firebase-perf/CHANGELOG.md @@ -1,5 +1,8 @@ # Unreleased * [fixed] Fixed an ANR on app launch. [#4831] +* [changed] Updated `firebase-sessions` dependency to v3.0.0 +* [fixed] Fixed the issues around unifying the sessions in `firebase-sessions` + and`firebase-performance`. # 22.0.0 * [changed] **Breaking Change**: Updated minSdkVersion to API level 23 or higher. @@ -442,4 +445,3 @@ updates. # 16.1.0 * [fixed] Fixed a `SecurityException` crash on certain devices that do not have Google Play Services on them. - diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index 54a8a232c71..5eaa059fcff 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -70,6 +70,7 @@ android { buildConfigField("String", "TRANSPORT_LOG_SRC", "String.valueOf(\"FIREPERF\")") buildConfigField("Boolean", "ENFORCE_DEFAULT_LOG_SRC", "Boolean.valueOf(false)") buildConfigField("String", "FIREPERF_VERSION_NAME", "String.valueOf(\"" + property("version") + "\")") + buildConfigField("Boolean", "ENFORCE_LEGACY_SESSIONS", "Boolean.valueOf(false)") if (project.hasProperty("fireperfBuildForAutopush")) { // This allows the SDK to be built for "Autopush" env when the mentioned flag @@ -77,6 +78,7 @@ android { // SDK or the Test App). buildConfigField("String", "TRANSPORT_LOG_SRC", "String.valueOf(\"FIREPERF_AUTOPUSH\")") buildConfigField("Boolean", "ENFORCE_DEFAULT_LOG_SRC", "Boolean.valueOf(true)") + buildConfigField("Boolean", "ENFORCE_LEGACY_SESSIONS", "Boolean.valueOf(true)") } minSdkVersion project.minSdkVersion @@ -121,7 +123,7 @@ dependencies { api("com.google.firebase:firebase-installations:18.0.0") { exclude group: 'com.google.firebase', module: 'firebase-common-ktx' } - api("com.google.firebase:firebase-sessions:2.0.7") { + api("com.google.firebase:firebase-sessions:3.0.0") { exclude group: 'com.google.firebase', module: 'firebase-common' exclude group: 'com.google.firebase', module: 'firebase-common-ktx' exclude group: 'com.google.firebase', module: 'firebase-components' diff --git a/firebase-perf/gradle.properties b/firebase-perf/gradle.properties index 19ed4bca754..8b4e646fe06 100644 --- a/firebase-perf/gradle.properties +++ b/firebase-perf/gradle.properties @@ -15,7 +15,7 @@ # # -version=22.0.1 +version=22.1.0 latestReleasedVersion=22.0.0 android.enableUnitTestBinaryResources=true diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java index 5b89deaad82..01a081ca9b7 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfEarly.java @@ -21,7 +21,8 @@ import com.google.firebase.perf.application.AppStateMonitor; import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.metrics.AppStartTrace; -import com.google.firebase.perf.session.SessionManager; +import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber; +import com.google.firebase.sessions.api.FirebaseSessionsDependencies; import java.util.concurrent.Executor; /** @@ -41,6 +42,10 @@ public FirebasePerfEarly( ConfigResolver configResolver = ConfigResolver.getInstance(); configResolver.setApplicationContext(context); + // Register FirebasePerformance as a subscriber ASAP - which will start collecting gauges if the + // FirebaseSession is verbose. + FirebaseSessionsDependencies.register(new FirebasePerformanceSessionSubscriber(configResolver)); + AppStateMonitor appStateMonitor = AppStateMonitor.getInstance(); appStateMonitor.registerActivityLifecycleCallbacks(context); appStateMonitor.registerForAppColdStart(new FirebasePerformanceInitializer()); @@ -50,13 +55,5 @@ public FirebasePerfEarly( appStartTrace.registerActivityLifecycleCallbacks(context); uiExecutor.execute(new AppStartTrace.StartFromBackgroundRunnable(appStartTrace)); } - - // TODO: Bring back Firebase Sessions dependency to watch for updates to sessions. - - // In the case of cold start, we create a session and start collecting gauges as early as - // possible. - // There is code in SessionManager that prevents us from resetting the session twice in case - // of app cold start. - SessionManager.getInstance().initializeGaugeCollection(); } } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java index c01f035af1f..daffc2de81a 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerfRegistrar.java @@ -30,6 +30,8 @@ import com.google.firebase.perf.injection.modules.FirebasePerformanceModule; import com.google.firebase.platforminfo.LibraryVersionComponent; import com.google.firebase.remoteconfig.RemoteConfigComponent; +import com.google.firebase.sessions.api.FirebaseSessionsDependencies; +import com.google.firebase.sessions.api.SessionSubscriber; import java.util.Arrays; import java.util.List; import java.util.concurrent.Executor; @@ -47,6 +49,11 @@ public class FirebasePerfRegistrar implements ComponentRegistrar { private static final String LIBRARY_NAME = "fire-perf"; private static final String EARLY_LIBRARY_NAME = "fire-perf-early"; + static { + // Add Firebase Performance as a dependency of Sessions when this class is loaded into memory. + FirebaseSessionsDependencies.addDependency(SessionSubscriber.Name.PERFORMANCE); + } + @Override @Keep public List> getComponents() { diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java index 40468566225..da2ed21d947 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/FirebasePerformance.java @@ -34,6 +34,7 @@ import com.google.firebase.perf.config.RemoteConfigManager; import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.logging.ConsoleUrlGenerator; +import com.google.firebase.perf.logging.FirebaseSessionsEnforcementCheck; import com.google.firebase.perf.metrics.HttpMetric; import com.google.firebase.perf.metrics.Trace; import com.google.firebase.perf.session.SessionManager; @@ -136,11 +137,6 @@ public static FirebasePerformance getInstance() { // to false if it's been force disabled or it is set to null if neither. @Nullable private Boolean mPerformanceCollectionForceEnabledState = null; - private final FirebaseApp firebaseApp; - private final Provider firebaseRemoteConfigProvider; - private final FirebaseInstallationsApi firebaseInstallationsApi; - private final Provider transportFactoryProvider; - /** * Constructs the FirebasePerformance class and allows injecting dependencies. * @@ -166,23 +162,18 @@ public static FirebasePerformance getInstance() { ConfigResolver configResolver, SessionManager sessionManager) { - this.firebaseApp = firebaseApp; - this.firebaseRemoteConfigProvider = firebaseRemoteConfigProvider; - this.firebaseInstallationsApi = firebaseInstallationsApi; - this.transportFactoryProvider = transportFactoryProvider; - if (firebaseApp == null) { this.mPerformanceCollectionForceEnabledState = false; this.configResolver = configResolver; this.mMetadataBundle = new ImmutableBundle(new Bundle()); return; } + FirebaseSessionsEnforcementCheck.setEnforcement(BuildConfig.ENFORCE_LEGACY_SESSIONS); TransportManager.getInstance() .initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider); Context appContext = firebaseApp.getApplicationContext(); - // TODO(b/110178816): Explore moving off of main thread. mMetadataBundle = extractMetadata(appContext); remoteConfigManager.setFirebaseRemoteConfigProvider(firebaseRemoteConfigProvider); @@ -192,6 +183,7 @@ public static FirebasePerformance getInstance() { sessionManager.setApplicationContext(appContext); mPerformanceCollectionForceEnabledState = configResolver.getIsPerformanceCollectionEnabled(); + if (logger.isLogcatEnabled() && isPerformanceCollectionEnabled()) { logger.info( String.format( @@ -282,7 +274,7 @@ public synchronized void setPerformanceCollectionEnabled(@Nullable Boolean enabl return; } - if (configResolver.getIsPerformanceCollectionDeactivated()) { + if (Boolean.TRUE.equals(configResolver.getIsPerformanceCollectionDeactivated())) { logger.info("Firebase Performance is permanently disabled"); return; } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/logging/FirebaseSessionsEnforcementCheck.kt b/firebase-perf/src/main/java/com/google/firebase/perf/logging/FirebaseSessionsEnforcementCheck.kt new file mode 100644 index 00000000000..089611564e8 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/logging/FirebaseSessionsEnforcementCheck.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.perf.logging + +import com.google.firebase.perf.session.PerfSession +import com.google.firebase.perf.session.isLegacy +import com.google.firebase.perf.v1.PerfSession as ProtoPerfSession + +class FirebaseSessionsEnforcementCheck { + companion object { + /** When enabled, failed preconditions will cause assertion errors for debugging. */ + @JvmStatic var enforcement: Boolean = false + private var logger: AndroidLogger = AndroidLogger.getInstance() + + @JvmStatic + fun checkSession(sessions: List, failureMessage: String) { + sessions.forEach { checkSession(it.sessionId, failureMessage) } + } + + @JvmStatic + fun checkSession(session: PerfSession, failureMessage: String) { + checkSession(session.sessionId(), failureMessage) + } + + @JvmStatic + fun checkSession(sessionId: String, failureMessage: String) { + if (sessionId.isLegacy()) { + logger.debug("legacy session ${sessionId}: $failureMessage") + assert(!enforcement) { failureMessage } + } + } + } +} 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 7574f989d92..bec5f8df82f 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,6 +14,8 @@ package com.google.firebase.perf.metrics; +import static com.google.firebase.perf.util.AppProcessesProvider.getAppProcesses; + import android.annotation.SuppressLint; import android.app.Activity; import android.app.ActivityManager; @@ -509,17 +511,10 @@ public static boolean isAnyAppProcessInForeground(Context appContext) { if (activityManager == null) { return true; } - List appProcesses = - activityManager.getRunningAppProcesses(); - if (appProcesses != null) { - String appProcessName = appContext.getPackageName(); - String allowedAppProcessNamePrefix = appProcessName + ":"; + List appProcesses = getAppProcesses(appContext); + if (!appProcesses.isEmpty()) { for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { - if (appProcess.importance != ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { - continue; - } - if (appProcess.processName.equals(appProcessName) - || appProcess.processName.startsWith(allowedAppProcessNamePrefix)) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) { boolean isAppInForeground = true; // For the case when the app is in foreground and the device transitions to sleep mode, diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilder.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilder.java index 1e04744d1b2..6afbb2a4de9 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilder.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilder.java @@ -224,7 +224,7 @@ public NetworkRequestMetricBuilder setCustomAttributes(Map attri * point depending upon the current {@link PerfSession} verbosity. * * @see GaugeManager#collectGaugeMetricOnce(Timer) - * @see PerfSession#isGaugeAndEventCollectionEnabled() + * @see PerfSession#isVerbose() */ public NetworkRequestMetricBuilder setRequestStartTimeMicros(long time) { SessionManager sessionManager = SessionManager.getInstance(); @@ -234,7 +234,7 @@ public NetworkRequestMetricBuilder setRequestStartTimeMicros(long time) { builder.setClientStartTimeUs(time); updateSession(perfSession); - if (perfSession.isGaugeAndEventCollectionEnabled()) { + if (perfSession.isVerbose()) { gaugeManager.collectGaugeMetricOnce(perfSession.getTimer()); } @@ -265,12 +265,12 @@ public long getTimeToResponseInitiatedMicros() { * point depending upon the current {@link PerfSession} Verbosity. * * @see GaugeManager#collectGaugeMetricOnce(Timer) - * @see PerfSession#isGaugeAndEventCollectionEnabled() + * @see PerfSession#isVerbose() */ public NetworkRequestMetricBuilder setTimeToResponseCompletedMicros(long time) { builder.setTimeToResponseCompletedUs(time); - if (SessionManager.getInstance().perfSession().isGaugeAndEventCollectionEnabled()) { + if (SessionManager.getInstance().perfSession().isVerbose()) { gaugeManager.collectGaugeMetricOnce(SessionManager.getInstance().perfSession().getTimer()); } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java index 91e5f44b4a0..6e9cc6fa47a 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/metrics/Trace.java @@ -233,7 +233,7 @@ public void start() { updateSession(perfSession); - if (perfSession.isGaugeAndEventCollectionEnabled()) { + if (perfSession.isVerbose()) { gaugeManager.collectGaugeMetricOnce(perfSession.getTimer()); } } @@ -259,7 +259,7 @@ public void stop() { if (!name.isEmpty()) { transportManager.log(new TraceMetricBuilder(this).build(), getAppState()); - if (SessionManager.getInstance().perfSession().isGaugeAndEventCollectionEnabled()) { + if (SessionManager.getInstance().perfSession().isVerbose()) { gaugeManager.collectGaugeMetricOnce( SessionManager.getInstance().perfSession().getTimer()); } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt new file mode 100644 index 00000000000..0f93f45586e --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.perf.session + +import com.google.firebase.perf.config.ConfigResolver +import com.google.firebase.perf.logging.FirebaseSessionsEnforcementCheck +import com.google.firebase.sessions.api.SessionSubscriber + +class FirebasePerformanceSessionSubscriber(val configResolver: ConfigResolver) : SessionSubscriber { + + override val sessionSubscriberName: SessionSubscriber.Name = SessionSubscriber.Name.PERFORMANCE + + override val isDataCollectionEnabled: Boolean + get() = configResolver.isPerformanceCollectionEnabled ?: false + + override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { + val currentPerfSession = SessionManager.getInstance().perfSession() + // TODO(b/394127311): Add logic to deal with app start gauges if necessary. + FirebaseSessionsEnforcementCheck.checkSession(currentPerfSession, "onSessionChanged") + + val updatedSession = PerfSession.createWithId(sessionDetails.sessionId) + SessionManager.getInstance().updatePerfSession(updatedSession) + } +} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebaseSessionsHelper.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebaseSessionsHelper.kt new file mode 100644 index 00000000000..7ab9bbf6fee --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebaseSessionsHelper.kt @@ -0,0 +1,40 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.perf.session + +import com.google.firebase.perf.util.Constants +import java.util.UUID + +/** Identifies whether the [PerfSession] is legacy or not. */ +fun PerfSession.isLegacy(): Boolean { + return this.sessionId().isLegacy() +} + +/** Identifies whether the string is from a legacy [PerfSession]. */ +fun String.isLegacy(): Boolean { + return this.startsWith(Constants.UNDEFINED_AQS_ID_PREFIX) +} + +/** Creates a valid session ID for [PerfSession] that can be predictably identified as legacy. */ +fun createLegacySessionId(): String { + val uuid = UUID.randomUUID().toString().replace("-", "") + return uuid.replaceRange( + 0, + Constants.UNDEFINED_AQS_ID_PREFIX.length, + Constants.UNDEFINED_AQS_ID_PREFIX + ) +} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java index 160a4507560..94c2ad74a0d 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/PerfSession.java @@ -28,21 +28,19 @@ /** Details of a session including a unique Id and related information. */ public class PerfSession implements Parcelable { - - private final String sessionId; private final Timer creationTime; - + private final String sessionId; private boolean isGaugeAndEventCollectionEnabled = false; /* * Creates a PerfSession object and decides what metrics to collect. */ - public static PerfSession createWithId(@NonNull String sessionId) { - String prunedSessionId = sessionId.replace("-", ""); - PerfSession session = new PerfSession(prunedSessionId, new Clock()); - session.setGaugeAndEventCollectionEnabled(shouldCollectGaugesAndEvents()); - - return session; + public static PerfSession createWithId(@Nullable String aqsSessionId) { + String sessionId = aqsSessionId; + if (sessionId == null) { + sessionId = FirebaseSessionsHelperKt.createLegacySessionId(); + } + return new PerfSession(sessionId, new Clock()); } /** Creates a PerfSession with the provided {@code sessionId} and {@code clock}. */ @@ -50,6 +48,7 @@ public static PerfSession createWithId(@NonNull String sessionId) { public PerfSession(String sessionId, Clock clock) { this.sessionId = sessionId; creationTime = clock.getTime(); + isGaugeAndEventCollectionEnabled = shouldCollectGaugesAndEvents(); } private PerfSession(@NonNull Parcel in) { @@ -59,7 +58,8 @@ private PerfSession(@NonNull Parcel in) { creationTime = in.readParcelable(Timer.class.getClassLoader()); } - /** Returns the sessionId of the object. */ + /** Returns the sessionId for the given session. */ + @NonNull public String sessionId() { return sessionId; } @@ -71,37 +71,11 @@ public Timer getTimer() { return creationTime; } - /* - * Enables/Disables the gauge and event collection for the system. - */ - public void setGaugeAndEventCollectionEnabled(boolean enabled) { - isGaugeAndEventCollectionEnabled = enabled; - } - - /* - * Returns if gauge and event collection is enabled for the system. - */ - public boolean isGaugeAndEventCollectionEnabled() { - return isGaugeAndEventCollectionEnabled; - } - /** Returns if the current session is verbose or not. */ public boolean isVerbose() { return isGaugeAndEventCollectionEnabled; } - /** Checks if the current {@link com.google.firebase.perf.v1.PerfSession} is verbose or not. */ - @VisibleForTesting - static boolean isVerbose(@NonNull com.google.firebase.perf.v1.PerfSession perfSession) { - for (SessionVerbosity sessionVerbosity : perfSession.getSessionVerbosityList()) { - if (sessionVerbosity == SessionVerbosity.GAUGES_AND_SYSTEM_EVENTS) { - return true; - } - } - - return false; - } - /** * Checks if it has been more than {@link ConfigResolver#getSessionsMaxDurationMinutes()} time * since the creation time of the current session. @@ -163,11 +137,20 @@ public static com.google.firebase.perf.v1.PerfSession[] buildAndSort( } /** If true, Session Gauge collection is enabled. */ - public static boolean shouldCollectGaugesAndEvents() { + @VisibleForTesting + public boolean shouldCollectGaugesAndEvents() { ConfigResolver configResolver = ConfigResolver.getInstance(); - return configResolver.isPerformanceMonitoringEnabled() - && Math.random() < configResolver.getSessionsSamplingRate(); + && (Math.abs(this.sessionId.hashCode() % 100) + < configResolver.getSessionsSamplingRate() * 100); + } + + /* + * Enables/Disables whether the session is verbose or not. + */ + @VisibleForTesting + public void setGaugeAndEventCollectionEnabled(boolean enabled) { + isGaugeAndEventCollectionEnabled = enabled; } /** @@ -208,4 +191,16 @@ public PerfSession[] newArray(int size) { return new PerfSession[size]; } }; + + /** Checks if the current {@link com.google.firebase.perf.v1.PerfSession} is verbose or not. */ + @VisibleForTesting + static boolean isVerbose(@NonNull com.google.firebase.perf.v1.PerfSession perfSession) { + for (SessionVerbosity sessionVerbosity : perfSession.getSessionVerbosityList()) { + if (sessionVerbosity == SessionVerbosity.GAUGES_AND_SYSTEM_EVENTS) { + return true; + } + } + + return false; + } } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java index 79d034b9b0b..a5f20ec6aa4 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManager.java @@ -18,34 +18,26 @@ import android.content.Context; import androidx.annotation.Keep; import androidx.annotation.VisibleForTesting; -import com.google.firebase.perf.application.AppStateMonitor; -import com.google.firebase.perf.application.AppStateUpdateHandler; +import com.google.firebase.perf.logging.FirebaseSessionsEnforcementCheck; import com.google.firebase.perf.session.gauges.GaugeManager; -import com.google.firebase.perf.v1.ApplicationProcessState; import com.google.firebase.perf.v1.GaugeMetadata; import com.google.firebase.perf.v1.GaugeMetric; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.Iterator; +import java.util.Objects; import java.util.Set; -import java.util.UUID; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; /** Session manager to generate sessionIDs and broadcast to the application. */ @Keep // Needed because of b/117526359. -public class SessionManager extends AppStateUpdateHandler { - +public class SessionManager { @SuppressLint("StaticFieldLeak") private static final SessionManager instance = new SessionManager(); private final GaugeManager gaugeManager; - private final AppStateMonitor appStateMonitor; private final Set> clients = new HashSet<>(); private PerfSession perfSession; - private Future syncInitFuture; /** Returns the singleton instance of SessionManager. */ public static SessionManager getInstance() { @@ -54,24 +46,22 @@ public static SessionManager getInstance() { /** Returns the currently active PerfSession. */ public final PerfSession perfSession() { + FirebaseSessionsEnforcementCheck.checkSession(perfSession, "PerfSession.perfSession()"); + return perfSession; } private SessionManager() { - // Generate a new sessionID for every cold start. - this( - GaugeManager.getInstance(), - PerfSession.createWithId(UUID.randomUUID().toString()), - AppStateMonitor.getInstance()); + // Creates a legacy session by default. This is a safety net to allow initializing + // SessionManager - but the current implementation replaces it immediately. + this(GaugeManager.getInstance(), PerfSession.createWithId(null)); + FirebaseSessionsEnforcementCheck.checkSession(perfSession, "SessionManager()"); } @VisibleForTesting - public SessionManager( - GaugeManager gaugeManager, PerfSession perfSession, AppStateMonitor appStateMonitor) { + public SessionManager(GaugeManager gaugeManager, PerfSession perfSession) { this.gaugeManager = gaugeManager; this.perfSession = perfSession; - this.appStateMonitor = appStateMonitor; - registerForAppState(); } /** @@ -79,49 +69,7 @@ public SessionManager( * (currently that is before onResume finishes) to ensure gauge collection starts on time. */ public void setApplicationContext(final Context appContext) { - // Get PerfSession in main thread first, because it is possible that app changes fg/bg state - // which creates a new perfSession, before the following is executed in background thread - final PerfSession appStartSession = perfSession; - // TODO(b/258263016): Migrate to go/firebase-android-executors - @SuppressLint("ThreadPoolCreation") - ExecutorService executorService = Executors.newSingleThreadExecutor(); - syncInitFuture = - executorService.submit( - () -> { - gaugeManager.initializeGaugeMetadataManager(appContext); - if (appStartSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.logGaugeMetadata( - appStartSession.sessionId(), ApplicationProcessState.FOREGROUND); - } - }); - } - - @Override - public void onUpdateAppState(ApplicationProcessState newAppState) { - super.onUpdateAppState(newAppState); - - if (appStateMonitor.isColdStart()) { - // We want the Session to remain unchanged if this is a cold start of the app since we already - // update the PerfSession in FirebasePerfProvider#onAttachInfo(). - return; - } - - if (newAppState == ApplicationProcessState.FOREGROUND) { - // A new foregrounding of app will force a new sessionID generation. - PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString()); - updatePerfSession(session); - } else { - // If the session is running for too long, generate a new session and collect gauges as - // necessary. - if (perfSession.isSessionRunningTooLong()) { - PerfSession session = PerfSession.createWithId(UUID.randomUUID().toString()); - updatePerfSession(session); - } else { - // For any other state change of the application, modify gauge collection state as - // necessary. - startOrStopCollectingGauges(newAppState); - } - } + gaugeManager.initializeGaugeMetadataManager(appContext); } /** @@ -130,6 +78,9 @@ public void onUpdateAppState(ApplicationProcessState newAppState) { * @see PerfSession#isSessionRunningTooLong() */ public void stopGaugeCollectionIfSessionRunningTooLong() { + FirebaseSessionsEnforcementCheck.checkSession( + perfSession, "SessionManager.stopGaugeCollectionIfSessionRunningTooLong"); + if (perfSession.isSessionRunningTooLong()) { gaugeManager.stopCollectingGauges(); } @@ -145,12 +96,18 @@ public void stopGaugeCollectionIfSessionRunningTooLong() { */ public void updatePerfSession(PerfSession perfSession) { // Do not update the perf session if it is the exact same sessionId. - if (perfSession.sessionId() == this.perfSession.sessionId()) { + if (Objects.equals(perfSession.sessionId(), this.perfSession.sessionId())) { return; } this.perfSession = perfSession; + // Start or stop the gauge data collection ASAP. + startOrStopCollectingGauges(); + + // Log gauge metadata. + logGaugeMetadataIfCollectionEnabled(); + synchronized (clients) { for (Iterator> i = clients.iterator(); i.hasNext(); ) { SessionAwareObject callback = i.next().get(); @@ -163,12 +120,6 @@ public void updatePerfSession(PerfSession perfSession) { } } } - - // Log the gauge metadata event if data collection is enabled. - logGaugeMetadataIfCollectionEnabled(appStateMonitor.getAppState()); - - // Start of stop the gauge data collection. - startOrStopCollectingGauges(appStateMonitor.getAppState()); } /** @@ -177,9 +128,9 @@ public void updatePerfSession(PerfSession perfSession) { * PerfSession} was already initialized a moment ago by getInstance(). Unlike updatePerfSession, * this does not reset the perfSession. */ + @Deprecated // TODO(b/394127311): Delete this. AQS early initialization updates the session ASAP. public void initializeGaugeCollection() { - logGaugeMetadataIfCollectionEnabled(ApplicationProcessState.FOREGROUND); - startOrStopCollectingGauges(ApplicationProcessState.FOREGROUND); + startOrStopCollectingGauges(); } /** @@ -206,15 +157,19 @@ public void unregisterForSessionUpdates(WeakReference client } } - private void logGaugeMetadataIfCollectionEnabled(ApplicationProcessState appState) { - if (perfSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.logGaugeMetadata(perfSession.sessionId(), appState); + private void logGaugeMetadataIfCollectionEnabled() { + FirebaseSessionsEnforcementCheck.checkSession( + perfSession, "logGaugeMetadataIfCollectionEnabled"); + if (perfSession.isVerbose()) { + gaugeManager.logGaugeMetadata(perfSession.sessionId()); } } - private void startOrStopCollectingGauges(ApplicationProcessState appState) { - if (perfSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.startCollectingGauges(perfSession, appState); + private void startOrStopCollectingGauges() { + FirebaseSessionsEnforcementCheck.checkSession(perfSession, "startOrStopCollectingGauges"); + + if (perfSession.isVerbose()) { + gaugeManager.startCollectingGauges(perfSession); } else { gaugeManager.stopCollectingGauges(); } @@ -224,9 +179,4 @@ private void startOrStopCollectingGauges(ApplicationProcessState appState) { public void setPerfSession(PerfSession perfSession) { this.perfSession = perfSession; } - - @VisibleForTesting - public Future getSyncInitFuture() { - return this.syncInitFuture; - } } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java index e33d363c0aa..528de2e62ae 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/CpuGaugeCollector.java @@ -17,8 +17,6 @@ import static android.system.Os.sysconf; import android.annotation.SuppressLint; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.system.OsConstants; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -78,7 +76,7 @@ public class CpuGaugeCollector { private final String procFileName; private final long clockTicksPerSecond; - @Nullable private ScheduledFuture cpuMetricCollectorJob = null; + @Nullable private ScheduledFuture cpuMetricCollectorJob = null; private long cpuMetricCollectionRateMs = UNSET_CPU_METRIC_COLLECTION_RATE; // TODO(b/258263016): Migrate to go/firebase-android-executors @@ -163,11 +161,12 @@ private synchronized void scheduleCpuMetricCollectionWithRate( this.cpuMetricCollectionRateMs = cpuMetricCollectionRate; try { cpuMetricCollectorJob = - cpuMetricCollectorExecutor.scheduleAtFixedRate( + cpuMetricCollectorExecutor.scheduleWithFixedDelay( () -> { CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime); if (currCpuReading != null) { cpuMetricReadings.add(currCpuReading); + GaugeCounter.incrementCounter(); } }, /* initialDelay */ 0, @@ -181,12 +180,13 @@ private synchronized void scheduleCpuMetricCollectionWithRate( private synchronized void scheduleCpuMetricCollectionOnce(Timer referenceTime) { try { @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + ScheduledFuture unusedFuture = cpuMetricCollectorExecutor.schedule( () -> { CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime); if (currCpuReading != null) { cpuMetricReadings.add(currCpuReading); + GaugeCounter.incrementCounter(); } }, /* initialDelay */ 0, @@ -227,12 +227,7 @@ private CpuMetricReading syncCollectCpuMetric(Timer referenceTime) { } private long getClockTicksPerSecond() { - if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) { - return sysconf(OsConstants._SC_CLK_TCK); - } else { - // TODO(b/110779408): Figure out how to collect this info for Android API 20 and below. - return INVALID_SC_PER_CPU_CLOCK_TICK; - } + return sysconf(OsConstants._SC_CLK_TCK); } private long convertClockTicksToMicroseconds(long clockTicks) { diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeCounter.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeCounter.kt new file mode 100644 index 00000000000..bb91258d73d --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeCounter.kt @@ -0,0 +1,57 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.perf.session.gauges + +import androidx.annotation.VisibleForTesting +import java.util.concurrent.atomic.AtomicInteger + +/** + * [GaugeCounter] is a thread-safe counter for gauge metrics. If the metrics count reaches or + * exceeds [MAX_METRIC_COUNT], it attempts to log the metrics to Firelog. + */ +object GaugeCounter { + private const val MAX_METRIC_COUNT = 50 + // For debugging explore re-introducing logging. + private val counter = AtomicInteger(0) + + @set:VisibleForTesting(otherwise = VisibleForTesting.NONE) + @set:JvmStatic + var gaugeManager: GaugeManager = GaugeManager.getInstance() + + @JvmStatic + fun incrementCounter() { + val metricsCount = counter.incrementAndGet() + + if (metricsCount >= MAX_METRIC_COUNT) { + // TODO(b/394127311): There can be rare conditions where there's an attempt to log metrics + // even when it's currently logging them. While this is a no-op, it might be worth + // exploring optimizing it further to prevent additional calls to [GaugeManager]. + gaugeManager.logGaugeMetrics() + } + } + + @JvmStatic + fun decrementCounter() { + counter.decrementAndGet() + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + @JvmStatic + fun resetCounter() { + counter.set(0) + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) @JvmStatic fun count(): Int = counter.get() +} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java index 7f6182a9c15..372c961257a 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.firebase.components.Lazy; +import com.google.firebase.perf.application.AppStateUpdateHandler; import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.session.PerfSession; @@ -31,7 +32,6 @@ import com.google.firebase.perf.v1.GaugeMetadata; import com.google.firebase.perf.v1.GaugeMetric; import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -41,7 +41,7 @@ * information and logging it to the Transport. */ @Keep // Needed because of b/117526359. -public class GaugeManager { +public class GaugeManager extends AppStateUpdateHandler { private static final AndroidLogger logger = AndroidLogger.getInstance(); private static final GaugeManager instance = new GaugeManager(); @@ -49,7 +49,6 @@ public class GaugeManager { // This is a guesstimate of the max amount of time to wait before any pending metrics' collection // might take. private static final long TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS = 20; - private static final long APPROX_NUMBER_OF_DATA_POINTS_PER_GAUGE_METRIC = 20; private static final long INVALID_GAUGE_COLLECTION_FREQUENCY = -1; private final Lazy gaugeManagerExecutor; @@ -59,8 +58,8 @@ public class GaugeManager { private final TransportManager transportManager; @Nullable private GaugeMetadataManager gaugeMetadataManager; - @Nullable private ScheduledFuture gaugeManagerDataCollectionJob = null; - @Nullable private String sessionId = null; + @Nullable private ScheduledFuture gaugeManagerDataCollectionJob = null; + @Nullable private PerfSession session = null; private ApplicationProcessState applicationProcessState = ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN; @@ -72,8 +71,8 @@ private GaugeManager() { TransportManager.getInstance(), ConfigResolver.getInstance(), null, - new Lazy<>(() -> new CpuGaugeCollector()), - new Lazy<>(() -> new MemoryGaugeCollector())); + new Lazy<>(CpuGaugeCollector::new), + new Lazy<>(MemoryGaugeCollector::new)); } @VisibleForTesting @@ -81,7 +80,7 @@ private GaugeManager() { Lazy gaugeManagerExecutor, TransportManager transportManager, ConfigResolver configResolver, - GaugeMetadataManager gaugeMetadataManager, + @Nullable GaugeMetadataManager gaugeMetadataManager, Lazy cpuGaugeCollector, Lazy memoryGaugeCollector) { @@ -91,6 +90,7 @@ private GaugeManager() { this.gaugeMetadataManager = gaugeMetadataManager; this.cpuGaugeCollector = cpuGaugeCollector; this.memoryGaugeCollector = memoryGaugeCollector; + registerForAppState(); } /** Initializes GaugeMetadataManager which requires application context. */ @@ -98,64 +98,69 @@ public void initializeGaugeMetadataManager(Context appContext) { this.gaugeMetadataManager = new GaugeMetadataManager(appContext); } + @Override + public void onUpdateAppState(ApplicationProcessState applicationProcessState) { + // If it isn't a verbose session (or unset) update the app state and return. + if (session == null || !session.isVerbose()) { + this.applicationProcessState = applicationProcessState; + return; + } + + // Log existing gauges to the current app state. + logGaugeMetrics(); + + // Update App State. + this.applicationProcessState = applicationProcessState; + + // Start collecting gauges for the new app state. + startCollectingGauges(this.applicationProcessState, session.getTimer()); + } + /** Returns the singleton instance of this class. */ public static synchronized GaugeManager getInstance() { return instance; } /** - * Starts the collection of available gauges for the given {@code sessionId} and {@code - * applicationProcessState}. The collected Gauge Metrics will be flushed at regular intervals. + * Starts the collection of available gauges for the given {@link PerfSession}. + * The collected Gauge Metrics will be flushed by {@link GaugeCounter} * *

GaugeManager can only collect gauges for one session at a time, and if this method is called * again with the same or new sessionId while it's already collecting gauges, all future gauges - * will then be associated with the same or new sessionId and applicationProcessState. + * will then be associated with the same or new sessionId. * * @param session The {@link PerfSession} to which the collected gauges will be associated with. - * @param applicationProcessState The {@link ApplicationProcessState} the collected GaugeMetrics - * will be associated with. * @note: This method is NOT thread safe - {@link this.startCollectingGauges()} and {@link - * this.stopCollectingGauges()} should always be called from the same thread. + * this.stopCollectingGauges()} should always be called from the same thread. */ - public void startCollectingGauges( - PerfSession session, ApplicationProcessState applicationProcessState) { - if (this.sessionId != null) { + public void startCollectingGauges(PerfSession session) { + if (this.session != null) { stopCollectingGauges(); } - long collectionFrequency = startCollectingGauges(applicationProcessState, session.getTimer()); + // TODO(b/394127311): Explore always setting the app state as FOREGROUND. + ApplicationProcessState gaugeCollectionApplicationProcessState = applicationProcessState; + if (gaugeCollectionApplicationProcessState + == ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN) { + logger.warn("Start collecting gauges with APPLICATION_PROCESS_STATE_UNKNOWN"); + // Since the application process state is unknown, collect gauges at the foreground frequency. + gaugeCollectionApplicationProcessState = ApplicationProcessState.FOREGROUND; + } + + long collectionFrequency = + startCollectingGauges(gaugeCollectionApplicationProcessState, session.getTimer()); if (collectionFrequency == INVALID_GAUGE_COLLECTION_FREQUENCY) { logger.warn("Invalid gauge collection frequency. Unable to start collecting Gauges."); return; } - this.sessionId = session.sessionId(); - this.applicationProcessState = applicationProcessState; - - // This is needed, otherwise the Runnable might use a stale value. - final String sessionIdForScheduledTask = sessionId; - final ApplicationProcessState applicationProcessStateForScheduledTask = applicationProcessState; - - try { - gaugeManagerDataCollectionJob = - gaugeManagerExecutor - .get() - .scheduleAtFixedRate( - () -> { - syncFlush(sessionIdForScheduledTask, applicationProcessStateForScheduledTask); - }, - /* initialDelay= */ collectionFrequency - * APPROX_NUMBER_OF_DATA_POINTS_PER_GAUGE_METRIC, - /* period= */ collectionFrequency * APPROX_NUMBER_OF_DATA_POINTS_PER_GAUGE_METRIC, - TimeUnit.MILLISECONDS); - - } catch (RejectedExecutionException e) { - logger.warn("Unable to start collecting Gauges: " + e.getMessage()); - } + this.session = session; } /** - * Starts the collection of available Gauges for the given {@code appState}. + * Starts the collection of available Gauges for the given {@code appState}. If it's being + * collected for a different app state, it stops that prior to starting it for the given + * {@code appState}. * * @param appState The app state to which the collected gauges are associated. * @param referenceTime The time off which the system time is calculated when collecting gauges. @@ -189,35 +194,51 @@ private long startCollectingGauges(ApplicationProcessState appState, Timer refer * this.stopCollectingGauges()} should always be called from the same thread. */ public void stopCollectingGauges() { - if (this.sessionId == null) { + if (session == null) { return; } - // This is needed, otherwise the Runnable might use a stale value. - final String sessionIdForScheduledTask = sessionId; - final ApplicationProcessState applicationProcessStateForScheduledTask = applicationProcessState; - cpuGaugeCollector.get().stopCollecting(); memoryGaugeCollector.get().stopCollecting(); - if (gaugeManagerDataCollectionJob != null) { - gaugeManagerDataCollectionJob.cancel(false); + logGaugeMetrics(); + this.session = null; + } + + /** + * Logs the existing GaugeMetrics to Firelog, associates it with the current {@link PerfSession} + * and {@link ApplicationProcessState}. + * + * @return true if a new data collection job is started, false otherwise. + */ + protected boolean logGaugeMetrics() { + if (session == null) { + logger.debug("Attempted to log Gauge Metrics when session was null."); + return false; + } + + // If there's an existing gaugeManagerDataCollectionJob, this is a no-op. + if (gaugeManagerDataCollectionJob != null && !gaugeManagerDataCollectionJob.isDone()) { + logger.debug("Attempted to start an additional gauge logging operation."); + return false; } - // Flush any data that was collected for this session one last time. - @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + logExistingGaugeMetrics(session.sessionId(), applicationProcessState); + return true; + } + + private void logExistingGaugeMetrics( + String sessionId, ApplicationProcessState applicationProcessState) { + // Flush any data that was collected and attach it to the given session and app state. + gaugeManagerDataCollectionJob = gaugeManagerExecutor .get() .schedule( () -> { - syncFlush(sessionIdForScheduledTask, applicationProcessStateForScheduledTask); + syncFlush(sessionId, applicationProcessState); }, TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS, TimeUnit.MILLISECONDS); - - this.sessionId = null; - this.applicationProcessState = ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN; } /** @@ -225,7 +246,7 @@ public void stopCollectingGauges() { * proto and logs it to transport. * * @param sessionId The sessionId to which the collected GaugeMetrics should be associated with. - * @param appState The app state for which these gauges are collected. + * @param appState The app state for which these gauges are attributed to. */ private void syncFlush(String sessionId, ApplicationProcessState appState) { GaugeMetric.Builder gaugeMetricBuilder = GaugeMetric.newBuilder(); @@ -233,12 +254,14 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { // Adding CPU metric readings. while (!cpuGaugeCollector.get().cpuMetricReadings.isEmpty()) { gaugeMetricBuilder.addCpuMetricReadings(cpuGaugeCollector.get().cpuMetricReadings.poll()); + GaugeCounter.decrementCounter(); } // Adding Memory metric readings. while (!memoryGaugeCollector.get().memoryMetricReadings.isEmpty()) { gaugeMetricBuilder.addAndroidMemoryReadings( memoryGaugeCollector.get().memoryMetricReadings.poll()); + GaugeCounter.decrementCounter(); } // Adding Session ID info. @@ -250,19 +273,18 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { /** * Log the Gauge Metadata information to the transport. * - * @param sessionId The {@link PerfSession#sessionId()} to which the collected Gauge Metrics - * should be associated with. - * @param appState The {@link ApplicationProcessState} for which these gauges are collected. + * @param sessionId The {@link PerfSession#sessionId()} ()} to which the collected Gauge Metrics + * should be associated with. * @return true if GaugeMetadata was logged, false otherwise. */ - public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { + public boolean logGaugeMetadata(String sessionId) { if (gaugeMetadataManager != null) { GaugeMetric gaugeMetric = GaugeMetric.newBuilder() .setSessionId(sessionId) .setGaugeMetadata(getGaugeMetadata()) .build(); - transportManager.log(gaugeMetric, appState); + transportManager.log(gaugeMetric); return true; } return false; @@ -402,4 +424,9 @@ private long getMemoryGaugeCollectionFrequencyMs( return memoryGaugeCollectionFrequency; } } + + @VisibleForTesting + void setApplicationProcessState(ApplicationProcessState applicationProcessState) { + this.applicationProcessState = applicationProcessState; + } } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java index 6b4466dfc35..ed38dd8f38d 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeMetadataManager.java @@ -17,18 +17,11 @@ import android.app.ActivityManager; import android.app.ActivityManager.MemoryInfo; import android.content.Context; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import androidx.annotation.VisibleForTesting; import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.util.StorageUnit; import com.google.firebase.perf.util.Utils; import com.google.firebase.perf.v1.GaugeMetadata; -import java.io.BufferedReader; -import java.io.FileReader; -import java.io.IOException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * The {@code GaugeMetadataManager} class is responsible for collecting {@link GaugeMetadata} @@ -41,7 +34,6 @@ class GaugeMetadataManager { private final Runtime runtime; private final ActivityManager activityManager; private final MemoryInfo memoryInfo; - private final Context appContext; GaugeMetadataManager(Context appContext) { this(Runtime.getRuntime(), appContext); @@ -50,7 +42,6 @@ class GaugeMetadataManager { @VisibleForTesting GaugeMetadataManager(Runtime runtime, Context appContext) { this.runtime = runtime; - this.appContext = appContext; this.activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); memoryInfo = new ActivityManager.MemoryInfo(); activityManager.getMemoryInfo(memoryInfo); @@ -75,29 +66,6 @@ public int getMaxEncouragedAppJavaHeapMemoryKb() { /** Returns the total memory (in kilobytes) accessible by the kernel (called the RAM size). */ public int getDeviceRamSizeKb() { - if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { - return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem)); - } - - return readTotalRAM(/* procFileName= */ "/proc/meminfo"); - } - - /** Returns the total ram size of the device (in kilobytes) by reading the "proc/meminfo" file. */ - @VisibleForTesting - int readTotalRAM(String procFileName) { - try (BufferedReader br = new BufferedReader(new FileReader(procFileName))) { - for (String s = br.readLine(); s != null; s = br.readLine()) { - if (s.startsWith("MemTotal")) { - Matcher m = Pattern.compile("\\d+").matcher(s); - return m.find() ? Integer.parseInt(m.group()) : 0; - } - } - } catch (IOException ioe) { - logger.warn("Unable to read '" + procFileName + "' file: " + ioe.getMessage()); - } catch (NumberFormatException nfe) { - logger.warn("Unable to parse '" + procFileName + "' file: " + nfe.getMessage()); - } - - return 0; + return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem)); } } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java index eeaf4eb7c80..878353a2c5b 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollector.java @@ -50,7 +50,7 @@ public class MemoryGaugeCollector { public final ConcurrentLinkedQueue memoryMetricReadings; private final Runtime runtime; - @Nullable private ScheduledFuture memoryMetricCollectorJob = null; + @Nullable private ScheduledFuture memoryMetricCollectorJob = null; private long memoryMetricCollectionRateMs = UNSET_MEMORY_METRIC_COLLECTION_RATE; // TODO(b/258263016): Migrate to go/firebase-android-executors @@ -124,11 +124,12 @@ private synchronized void scheduleMemoryMetricCollectionWithRate( try { memoryMetricCollectorJob = - memoryMetricCollectorExecutor.scheduleAtFixedRate( + memoryMetricCollectorExecutor.scheduleWithFixedDelay( () -> { AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime); if (memoryReading != null) { memoryMetricReadings.add(memoryReading); + GaugeCounter.incrementCounter(); } }, /* initialDelay */ 0, @@ -142,12 +143,13 @@ private synchronized void scheduleMemoryMetricCollectionWithRate( private synchronized void scheduleMemoryMetricCollectionOnce(Timer referenceTime) { try { @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + ScheduledFuture unusedFuture = memoryMetricCollectorExecutor.schedule( () -> { AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime); if (memoryReading != null) { memoryMetricReadings.add(memoryReading); + GaugeCounter.incrementCounter(); } }, /* initialDelay */ 0, diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java index 9600b099a6d..d1eb5ae059c 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/transport/TransportManager.java @@ -14,6 +14,7 @@ package com.google.firebase.perf.transport; +import static com.google.firebase.perf.util.AppProcessesProvider.getProcessName; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; @@ -38,6 +39,7 @@ import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.logging.ConsoleUrlGenerator; +import com.google.firebase.perf.logging.FirebaseSessionsEnforcementCheck; import com.google.firebase.perf.metrics.validator.PerfMetricValidator; import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.util.Constants; @@ -230,6 +232,7 @@ private void finishInitialization() { applicationInfoBuilder = ApplicationInfo.newBuilder(); applicationInfoBuilder .setGoogleAppId(firebaseApp.getOptions().getApplicationId()) + .setProcessName(getProcessName(appContext)) .setAndroidAppInfo( AndroidApplicationInfo.newBuilder() .setPackageName(packageName) @@ -297,6 +300,8 @@ public void log(final TraceMetric traceMetric) { * {@link #isAllowedToDispatch(PerfMetric)}). */ public void log(final TraceMetric traceMetric, final ApplicationProcessState appState) { + FirebaseSessionsEnforcementCheck.checkSession( + traceMetric.getPerfSessionsList(), "log TraceMetric"); executorService.execute( () -> syncLog(PerfMetric.newBuilder().setTraceMetric(traceMetric), appState)); } @@ -325,6 +330,8 @@ public void log(final NetworkRequestMetric networkRequestMetric) { */ public void log( final NetworkRequestMetric networkRequestMetric, final ApplicationProcessState appState) { + FirebaseSessionsEnforcementCheck.checkSession( + networkRequestMetric.getPerfSessionsList(), "log NetworkRequestMetric"); executorService.execute( () -> syncLog( @@ -354,6 +361,7 @@ public void log(final GaugeMetric gaugeMetric) { * {@link #isAllowedToDispatch(PerfMetric)}). */ public void log(final GaugeMetric gaugeMetric, final ApplicationProcessState appState) { + FirebaseSessionsEnforcementCheck.checkSession(gaugeMetric.getSessionId(), "log GaugeMetric"); executorService.execute( () -> syncLog(PerfMetric.newBuilder().setGaugeMetric(gaugeMetric), appState)); } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/util/AppProcessesProvider.kt b/firebase-perf/src/main/java/com/google/firebase/perf/util/AppProcessesProvider.kt new file mode 100644 index 00000000000..c4d820f31e3 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/util/AppProcessesProvider.kt @@ -0,0 +1,73 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.firebase.perf.util + +import android.app.ActivityManager +import android.app.Application +import android.content.Context +import android.os.Build +import android.os.Process +import com.google.android.gms.common.util.ProcessUtils + +/** + * A singleton that contains helper functions to get relevant process details. TODO(b/418041083): + * Explore using a common utility. See [com.google.firebase.sessions.ProcessDetailsProvider]. + */ +object AppProcessesProvider { + /** Gets the details for all of this app's running processes. */ + @JvmStatic + fun getAppProcesses(context: Context): List { + val appUid = context.applicationInfo.uid + val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as? ActivityManager + val runningAppProcesses = activityManager?.runningAppProcesses ?: listOf() + + return runningAppProcesses.filterNotNull().filter { + // Only collect process info for this app's processes. + it.uid == appUid + } + } + + /** + * Gets this app's current process name. + * + * If the current process details are not found for whatever reason, returns an empty string. + */ + @JvmStatic + fun getProcessName(context: Context): String { + val pid = Process.myPid() + return getAppProcesses(context).find { it.pid == pid }?.processName ?: getProcessName() + } + + /** Gets the app's current process name. If it could not be found, return the default. */ + private fun getProcessName(default: String = ""): String { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { + return Process.myProcessName() + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Application.getProcessName()?.let { + return it + } + } + + // GMS core has different ways to get the process name on old api levels. + ProcessUtils.getMyProcessName()?.let { + return it + } + + // Returns default if nothing works. + return default + } +} diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/util/Constants.java b/firebase-perf/src/main/java/com/google/firebase/perf/util/Constants.java index f2704b903ce..42a126f014e 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/util/Constants.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/util/Constants.java @@ -22,6 +22,10 @@ public class Constants { public static final String PREFS_NAME = "FirebasePerfSharedPrefs"; public static final String ENABLE_DISABLE = "isEnabled"; + // A non-hex character guarantees it isn't an AQS generated UUID. + // https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.uuid/-uuid/ + public static final String UNDEFINED_AQS_ID_PREFIX = "z"; + public static final double MIN_SAMPLING_RATE = 0.0; public static final double MAX_SAMPLING_RATE = 1.0; diff --git a/firebase-perf/src/main/proto/firebase/perf/v1/perf_metric.proto b/firebase-perf/src/main/proto/firebase/perf/v1/perf_metric.proto index 8da196c0281..e10a1191671 100644 --- a/firebase-perf/src/main/proto/firebase/perf/v1/perf_metric.proto +++ b/firebase-perf/src/main/proto/firebase/perf/v1/perf_metric.proto @@ -292,7 +292,7 @@ message GaugeMetadata { // Additional metadata about an application and its state (including state of // the device at runtime) that is not provided by firebase data transport. // -// Next tag: 8 +// Next tag: 9 message ApplicationInfo { // Identifier for the application that has been registered with firebase. // Contains pantheon project number, platform and the hash of the (package @@ -316,6 +316,9 @@ message ApplicationInfo { // A map of global-level custom attribute names to values. map custom_attributes = 6; + + // The name of process that initiate the event. Currently only populated for Android apps. + optional string process_name = 8; } // Additional metadata about an android application that is not provided by diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java index b31696d963b..f1f258e518c 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/FirebasePerformanceTestBase.java @@ -25,9 +25,11 @@ import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.session.PerfSession; import com.google.firebase.perf.session.SessionManager; +import com.google.firebase.perf.session.gauges.GaugeCounter; import com.google.firebase.perf.util.ImmutableBundle; import org.junit.After; import org.junit.Before; +import org.junit.BeforeClass; import org.robolectric.shadows.ShadowPackageManager; public class FirebasePerformanceTestBase { @@ -54,6 +56,12 @@ public class FirebasePerformanceTestBase { protected Context appContext; + @BeforeClass + public static void setUpBeforeClass() { + // TODO(b/394127311): Explore removing this. + GaugeCounter.resetCounter(); + } + @Before public void setUpFirebaseApp() { appContext = ApplicationProvider.getApplicationContext(); diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java index 0b7d4bbfc17..f30ee5d73a0 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/application/AppStateMonitorTest.java @@ -39,6 +39,8 @@ import com.google.firebase.perf.config.DeviceCacheManager; import com.google.firebase.perf.metrics.NetworkRequestMetricBuilder; import com.google.firebase.perf.metrics.Trace; +import com.google.firebase.perf.session.PerfSession; +import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Clock; @@ -80,6 +82,7 @@ public class AppStateMonitorTest extends FirebasePerformanceTestBase { @Before public void setUp() { currentTime = 0; + SessionManager.getInstance().updatePerfSession(PerfSession.createWithId("sessionId")); initMocks(this); doAnswer((Answer) invocationOnMock -> new Timer(currentTime)).when(clock).getTime(); diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java index 61b3823741d..4a90184936c 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/NetworkRequestMetricBuilderTest.java @@ -242,7 +242,7 @@ public void testSessionIdNotAddedIfPerfSessionIsNull() { int numberOfSessionIds = metricBuilder.getSessions().size(); - new SessionManager(mock(GaugeManager.class), null, mock(AppStateMonitor.class)); + new SessionManager(mock(GaugeManager.class), null); assertThat(metricBuilder.getSessions()).hasSize(numberOfSessionIds); } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java index 0be443031f2..ca86a1a86b6 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/TraceTest.java @@ -1032,7 +1032,7 @@ public void testSessionIdNotAddedIfPerfSessionIsNull() { int numberOfSessionIds = trace.getSessions().size(); - new SessionManager(mock(GaugeManager.class), null, mock(AppStateMonitor.class)); + new SessionManager(mock(GaugeManager.class), null); assertThat(trace.getSessions()).hasSize(numberOfSessionIds); diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/FirebaseSessionsTestHelper.kt b/firebase-perf/src/test/java/com/google/firebase/perf/session/FirebaseSessionsTestHelper.kt new file mode 100644 index 00000000000..a617af94a58 --- /dev/null +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/FirebaseSessionsTestHelper.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2025 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.perf.session + +import com.google.firebase.perf.util.Clock + +fun createTestSession(suffix: Int): PerfSession { + // TODO(b/394127311): Add a method to verify legacy behavior. + // only hex characters and so it's AQS. + return PerfSession(testSessionId(suffix), Clock()) +} + +fun testSessionId(suffix: Int): String = "abc$suffix" diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java index 43257987b0f..f7c8d400483 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/PerfSessionTest.java @@ -15,6 +15,8 @@ package com.google.firebase.perf.session; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.createTestSession; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.testSessionId; import static com.google.firebase.perf.util.Constants.PREFS_NAME; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -62,12 +64,24 @@ public void setUp() { @Test public void instanceCreation() { - PerfSession session = new PerfSession("sessionId", mockClock); + PerfSession session = PerfSession.createWithId("sessionId"); assertThat(session).isNotNull(); session.setGaugeAndEventCollectionEnabled(true); - Assert.assertTrue(session.isGaugeAndEventCollectionEnabled()); + assertThat(session.isVerbose()).isTrue(); session.setGaugeAndEventCollectionEnabled(false); - Assert.assertFalse(session.isGaugeAndEventCollectionEnabled()); + assertThat(session.isVerbose()).isFalse(); + assertThat(FirebaseSessionsHelperKt.isLegacy(session)).isFalse(); + } + + @Test + public void legacyInstanceCreation() { + PerfSession perfSession = PerfSession.createWithId(null); + assertThat(perfSession).isNotNull(); + perfSession.setGaugeAndEventCollectionEnabled(true); + assertThat(perfSession.isVerbose()).isTrue(); + perfSession.setGaugeAndEventCollectionEnabled(false); + assertThat(perfSession.isVerbose()).isFalse(); + assertThat(FirebaseSessionsHelperKt.isLegacy(perfSession)).isTrue(); } @Test @@ -76,19 +90,20 @@ public void shouldCollectGaugesAndEvents_perfMonDisabledAtRuntime_sessionNotVerb Bundle bundle = new Bundle(); bundle.putFloat("sessions_sampling_percentage", 100); configResolver.setMetadataBundle(new ImmutableBundle(bundle)); + PerfSession testSession = PerfSession.createWithId("aqsSessionId"); // By default, session is verbose if developer has set 100% of session verbosity. - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isTrue(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isTrue(); // Case #1: developer has disabled Performance Monitoring during runtime. configResolver.setIsPerformanceCollectionEnabled(false); - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isFalse(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isFalse(); // Case #2: developer has enabled Performance Monitoring during runtime. configResolver.setIsPerformanceCollectionEnabled(true); - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isTrue(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isTrue(); } @Test @@ -99,20 +114,21 @@ public void shouldCollectGaugesAndEvents_perfMonDisabledAtBuildtime_verbosityDep bundle.putFloat("sessions_sampling_percentage", 100); bundle.putBoolean("firebase_performance_collection_enabled", false); configResolver.setMetadataBundle(new ImmutableBundle(bundle)); + PerfSession testSession = PerfSession.createWithId("aqsSessionId"); // By default, session is not verbose if developer disabled performance monitoring at build // time. - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isFalse(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isFalse(); // Case #1: developer has enabled Performance Monitoring during runtime. configResolver.setIsPerformanceCollectionEnabled(true); - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isTrue(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isTrue(); // Case #2: developer has disabled Performance Monitoring during runtime. configResolver.setIsPerformanceCollectionEnabled(false); - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isFalse(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isFalse(); } @Test @@ -122,24 +138,25 @@ public void shouldCollectGaugesAndEvents_perfMonDeactivated_sessionNotVerbose() bundle.putFloat("sessions_sampling_percentage", 100); bundle.putBoolean("firebase_performance_collection_deactivated", true); configResolver.setMetadataBundle(new ImmutableBundle(bundle)); + PerfSession testSession = PerfSession.createWithId("aqsSessionId"); // Session will never be verbose if developer deactivated performance monitoring at build time. - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isFalse(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isFalse(); // Case #1: developer has enabled Performance Monitoring during runtime. configResolver.setIsPerformanceCollectionEnabled(true); - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isFalse(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isFalse(); // Case #2: developer has disabled Performance Monitoring during runtime. configResolver.setIsPerformanceCollectionEnabled(false); - assertThat(PerfSession.shouldCollectGaugesAndEvents()).isFalse(); + assertThat(testSession.shouldCollectGaugesAndEvents()).isFalse(); } @Test public void testPerfSessionConversion() { - PerfSession session1 = new PerfSession("sessionId", mockClock); + PerfSession session1 = createTestSession(1); session1.setGaugeAndEventCollectionEnabled(true); com.google.firebase.perf.v1.PerfSession perfSession = session1.build(); @@ -150,7 +167,7 @@ public void testPerfSessionConversion() { @Test public void testPerfSessionConversionWithoutVerbosity() { - PerfSession session1 = new PerfSession("sessionId", mockClock); + PerfSession session1 = createTestSession(1); com.google.firebase.perf.v1.PerfSession perfSession = session1.build(); Assert.assertEquals(session1.sessionId(), perfSession.getSessionId()); @@ -160,22 +177,22 @@ public void testPerfSessionConversionWithoutVerbosity() { @Test public void testPerfSessionsCreateDisabledGaugeCollectionWhenVerboseSessionForceDisabled() { forceNonVerboseSession(); - PerfSession testPerfSession = PerfSession.createWithId("sessionId"); - assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isFalse(); + PerfSession testPerfSession = createTestSession(1); + assertThat(testPerfSession.isVerbose()).isFalse(); } @Test public void testPerfSessionsCreateDisabledGaugeCollectionWhenSessionsFeatureDisabled() { forceSessionsFeatureDisabled(); - PerfSession testPerfSession = PerfSession.createWithId("sessionId"); - assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isFalse(); + PerfSession testPerfSession = createTestSession(1); + assertThat(testPerfSession.isVerbose()).isFalse(); } @Test public void testPerfSessionsCreateEnablesGaugeCollectionWhenVerboseSessionForceEnabled() { forceVerboseSession(); - PerfSession testPerfSession = PerfSession.createWithId("sessionId"); - assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isTrue(); + PerfSession testPerfSession = PerfSession.createWithId(testSessionId(1)); + assertThat(testPerfSession.isVerbose()).isTrue(); } @Test @@ -185,16 +202,16 @@ public void testBuildAndSortMovesTheVerboseSessionToTop() { // Next, create 3 non-verbose sessions List sessions = new ArrayList<>(); - sessions.add(PerfSession.createWithId("sessionId1")); - sessions.add(PerfSession.createWithId("sessionId2")); - sessions.add(PerfSession.createWithId("sessionId3")); + sessions.add(PerfSession.createWithId(testSessionId(1))); + sessions.add(PerfSession.createWithId(testSessionId(2))); + sessions.add(PerfSession.createWithId(testSessionId(3))); // Force all the sessions from now onwards to be verbose forceVerboseSession(); // Next, create 2 verbose sessions - sessions.add(PerfSession.createWithId("sessionId4")); - sessions.add(PerfSession.createWithId("sessionId5")); + sessions.add(PerfSession.createWithId(testSessionId(4))); + sessions.add(PerfSession.createWithId(testSessionId(5))); // Verify that the first session in the list of sessions was not verbose assertThat(sessions.get(0).isVerbose()).isFalse(); @@ -216,7 +233,7 @@ public void testIsExpiredReturnsFalseWhenCurrentSessionLengthIsLessThanMaxSessio - TimeUnit.MINUTES.toMicros(1)); // Default Max Session Length is 4 hours when(mockClock.getTime()).thenReturn(mockTimer); - PerfSession session = new PerfSession("sessionId", mockClock); + PerfSession session = new PerfSession(testSessionId(1), mockClock); assertThat(session.isSessionRunningTooLong()).isFalse(); } @@ -227,7 +244,7 @@ public void testIsExpiredReturnsFalseWhenCurrentSessionLengthIsEqualToMaxSession .thenReturn(TimeUnit.HOURS.toMicros(4)); // Default Max Session Length is 4 hours when(mockClock.getTime()).thenReturn(mockTimer); - PerfSession session = new PerfSession("sessionId", mockClock); + PerfSession session = new PerfSession(testSessionId(1), mockClock); assertThat(session.isSessionRunningTooLong()).isFalse(); } @@ -238,7 +255,7 @@ public void testIsExpiredReturnsTrueWhenCurrentSessionLengthIsGreaterThanMaxSess .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours when(mockClock.getTime()).thenReturn(mockTimer); - PerfSession session = new PerfSession("sessionId", mockClock); + PerfSession session = new PerfSession(testSessionId(1), mockClock); assertThat(session.isSessionRunningTooLong()).isTrue(); } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java index f3e3795f3f8..ab8cce7aab5 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java @@ -15,10 +15,9 @@ package com.google.firebase.perf.session; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.createTestSession; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.testSessionId; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -40,7 +39,6 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.AdditionalMatchers; import org.mockito.ArgumentMatchers; import org.mockito.InOrder; import org.mockito.Mock; @@ -61,7 +59,7 @@ public class SessionManagerTest extends FirebasePerformanceTestBase { @Before public void setUp() { initMocks(this); - when(mockPerfSession.sessionId()).thenReturn("sessionId"); + when(mockPerfSession.sessionId()).thenReturn(testSessionId(5)); when(mockAppStateMonitor.isColdStart()).thenReturn(false); AppStateMonitor.getInstance().setIsColdStart(false); } @@ -70,139 +68,17 @@ public void setUp() { public void testInstanceCreation() { assertThat(SessionManager.getInstance()).isNotNull(); assertThat(SessionManager.getInstance()).isEqualTo(SessionManager.getInstance()); - assertThat(SessionManager.getInstance().perfSession().sessionId()).isNotNull(); } @Test - public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsInitialized() + public void setApplicationContext_initializeGaugeMetadataManager() throws ExecutionException, InterruptedException { - when(mockPerfSession.isGaugeAndEventCollectionEnabled()).thenReturn(true); + when(mockPerfSession.isVerbose()).thenReturn(true); InOrder inOrder = Mockito.inOrder(mockGaugeManager); - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession); testSessionManager.setApplicationContext(mockApplicationContext); - testSessionManager.getSyncInitFuture().get(); inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any()); - inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any()); - } - - @Test - public void testOnUpdateAppStateDoesNothingDuringAppStart() { - String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); - - assertThat(oldSessionId).isNotNull(); - assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - - AppStateMonitor.getInstance().setIsColdStart(true); - - SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); - assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - } - - @Test - public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() { - String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); - - assertThat(oldSessionId).isNotNull(); - assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - - SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); - assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId()); - } - - @Test - public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() { - String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); - - assertThat(oldSessionId).isNotNull(); - assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - - SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND); - assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - } - - @Test - public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSessionExpires() { - when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true); - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - String oldSessionId = testSessionManager.perfSession().sessionId(); - - assertThat(oldSessionId).isNotNull(); - assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId()); - - testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); - assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId()); - } - - @Test - public void - testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsVerbose() { - forceVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); - - verify(mockGaugeManager) - .logGaugeMetadata( - anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); - } - - @Test - public void - testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnForegroundStateIfSessionIsNonVerbose() { - forceNonVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); - - verify(mockGaugeManager, never()) - .logGaugeMetadata( - anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); - } - - @Test - public void - testOnUpdateAppStateDoesntMakeGaugeManagerLogGaugeMetadataOnBackgroundStateEvenIfSessionIsVerbose() { - forceVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); - - verify(mockGaugeManager, never()) - .logGaugeMetadata( - anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); - } - - @Test - public void - testOnUpdateAppStateMakesGaugeManagerLogGaugeMetadataOnBackgroundAppStateIfSessionIsVerboseAndTimedOut() { - when(mockPerfSession.isSessionRunningTooLong()).thenReturn(true); - forceVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); - - verify(mockGaugeManager) - .logGaugeMetadata( - anyString(), nullable(com.google.firebase.perf.v1.ApplicationProcessState.class)); - } - - @Test - public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionIsVerbose() { - forceVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); - - verify(mockGaugeManager) - .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any()); } // LogGaugeData on new perf session when Verbose @@ -210,62 +86,32 @@ public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionI // Mark Session as expired after time limit. @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() { + public void testUpdatePerfSessionMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() { forceNonVerboseSession(); - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId")); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession); + testSessionManager.updatePerfSession(createTestSession(1)); verify(mockGaugeManager).stopCollectingGauges(); } @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() { + public void testUpdatePerfSessionMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() { forceSessionsFeatureDisabled(); - SessionManager testSessionManager = - new SessionManager( - mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, createTestSession(1)); + testSessionManager.updatePerfSession(createTestSession(2)); verify(mockGaugeManager).stopCollectingGauges(); } - @Test - public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() { - when(mockPerfSession.isSessionRunningTooLong()).thenReturn(false); - - // Start with a non verbose session - forceNonVerboseSession(); - SessionManager testSessionManager = - new SessionManager( - mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor); - - verify(mockGaugeManager, times(0)) - .logGaugeMetadata( - eq("testSessionId1"), - eq(com.google.firebase.perf.v1.ApplicationProcessState.FOREGROUND)); - - // Forcing a verbose session will enable Gauge collection - forceVerboseSession(); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); - verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId2"), any()); - - // Force a non-verbose session and verify if we are not logging metadata - forceVerboseSession(); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3")); - verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any()); - } - @Test public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() { Timer mockTimer = mock(Timer.class); when(mockClock.getTime()).thenReturn(mockTimer); - PerfSession session = new PerfSession("sessionId", mockClock); - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, session, mockAppStateMonitor); + PerfSession session = new PerfSession(testSessionId(1), mockClock); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, session); assertThat(session.isSessionRunningTooLong()).isFalse(); @@ -273,37 +119,37 @@ public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() { .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours assertThat(session.isSessionRunningTooLong()).isTrue(); - assertThat(testSessionManager.perfSession().sessionId()).isEqualTo("sessionId"); + assertThat(testSessionManager.perfSession().sessionId()).isEqualTo(testSessionId(1)); } @Test - public void testPerfSessionExpiredMakesGaugeManagerStopsCollectingGaugesIfSessionIsVerbose() { - forceVerboseSession(); + public void testUpdatePerfSessionStartsCollectingGaugesIfSessionIsVerbose() { Timer mockTimer = mock(Timer.class); when(mockClock.getTime()).thenReturn(mockTimer); + when(mockAppStateMonitor.getAppState()).thenReturn(ApplicationProcessState.FOREGROUND); - PerfSession session = new PerfSession("sessionId", mockClock); - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, session, mockAppStateMonitor); + PerfSession previousSession = createTestSession(1); + previousSession.setGaugeAndEventCollectionEnabled(false); - assertThat(session.isSessionRunningTooLong()).isFalse(); + PerfSession newSession = createTestSession(2); + newSession.setGaugeAndEventCollectionEnabled(true); - when(mockTimer.getDurationMicros()) - .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours + SessionManager testSessionManager = new SessionManager(mockGaugeManager, previousSession); + testSessionManager.updatePerfSession(newSession); + testSessionManager.setApplicationContext(mockApplicationContext); - assertThat(session.isSessionRunningTooLong()).isTrue(); - verify(mockGaugeManager, times(0)).logGaugeMetadata(any(), any()); + verify(mockGaugeManager, times(1)).initializeGaugeMetadataManager(mockApplicationContext); + verify(mockGaugeManager, times(1)).startCollectingGauges(newSession); } @Test public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession); FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(createTestSession(1)); verify(spySessionAwareObjectOne, never()) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -313,8 +159,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { @Test public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() { - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession); FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); @@ -322,8 +167,8 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() { testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne)); testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo)); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + testSessionManager.updatePerfSession(createTestSession(1)); + testSessionManager.updatePerfSession(createTestSession(2)); verify(spySessionAwareObjectOne, times(2)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -333,8 +178,7 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() { @Test public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession); FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); @@ -347,11 +191,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(createTestSession(1)); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + testSessionManager.updatePerfSession(createTestSession(2)); verify(spySessionAwareObjectOne, times(1)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/CpuGaugeCollectorTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/CpuGaugeCollectorTest.java index 570e72e4a76..8d155196e3f 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/CpuGaugeCollectorTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/CpuGaugeCollectorTest.java @@ -60,9 +60,11 @@ public void tearDown() { @Test public void testStartCollectingAddsCpuMetricReadingsToTheConcurrentLinkedQueue() throws Exception { + int priorGaugeCount = GaugeCounter.count(); testGaugeCollector.startCollecting(100, new Timer()); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); assertThat(testGaugeCollector.cpuMetricReadings).hasSize(1); + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCount + 1); } @Test @@ -246,11 +248,17 @@ public void testCollectCpuMetricDoesntStartCollectingWithInvalidCpuMetricCollect @Test public void testCollectOnce_addOnlyOneCpuMetricReadingToQueue() { + int priorGaugeCount = GaugeCounter.count(); assertThat(testGaugeCollector.cpuMetricReadings).isEmpty(); testGaugeCollector.collectOnce(new Timer()); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + + // Simulate running an additional task. + fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + assertThat(testGaugeCollector.cpuMetricReadings).hasSize(1); + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCount + 1); } @Test diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java index 5090d66c8b9..c111f5fb7a2 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeManagerTest.java @@ -15,6 +15,8 @@ package com.google.firebase.perf.session.gauges; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.createTestSession; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.testSessionId; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; @@ -25,14 +27,15 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.robolectric.Shadows.shadowOf; +import android.os.Looper; import androidx.test.core.app.ApplicationProvider; import com.google.firebase.components.Lazy; import com.google.firebase.perf.FirebasePerformanceTestBase; import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.session.PerfSession; import com.google.firebase.perf.transport.TransportManager; -import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.AndroidMemoryReading; import com.google.firebase.perf.v1.ApplicationProcessState; @@ -40,7 +43,9 @@ import com.google.firebase.perf.v1.GaugeMetadata; import com.google.firebase.perf.v1.GaugeMetric; import com.google.testing.timing.FakeScheduledExecutorService; +import java.util.Random; import java.util.concurrent.TimeUnit; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -55,12 +60,14 @@ public final class GaugeManagerTest extends FirebasePerformanceTestBase { // This is a guesstimate of the max amount of time to wait before any pending metrics' collection // might take. private static final long TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS = 20; - private static final long APPROX_NUMBER_OF_DATA_POINTS_PER_GAUGE_METRIC = 20; private static final long DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_BG_MS = 100; private static final long DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_FG_MS = 50; private static final long DEFAULT_MEMORY_GAUGE_COLLECTION_FREQUENCY_BG_MS = 120; private static final long DEFAULT_MEMORY_GAUGE_COLLECTION_FREQUENCY_FG_MS = 60; + // See [com.google.firebase.perf.session.gauges.GaugeCounter]. + private static final long MAX_GAUGE_COUNTER_LIMIT = 50; + private GaugeManager testGaugeManager = null; private FakeScheduledExecutorService fakeScheduledExecutorService = null; private TransportManager mockTransportManager = null; @@ -122,24 +129,32 @@ public void setUp() { new Lazy<>(() -> fakeMemoryGaugeCollector)); } + @After + public void tearDown() { + shadowOf(Looper.getMainLooper()).idle(); + } + @Test - public void testStartCollectingGaugesStartsCollectingMetricsInBackgroundState() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.BACKGROUND); + public void testStartCollectingGaugesStartsCollectingMetricsDefault() { + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState( + ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN); + testGaugeManager.startCollectingGauges(fakeSession); verify(fakeCpuGaugeCollector) .startCollecting( - eq(DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_BG_MS), + eq(DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_FG_MS), ArgumentMatchers.nullable(Timer.class)); verify(fakeMemoryGaugeCollector) .startCollecting( - eq(DEFAULT_MEMORY_GAUGE_COLLECTION_FREQUENCY_BG_MS), + eq(DEFAULT_MEMORY_GAUGE_COLLECTION_FREQUENCY_FG_MS), ArgumentMatchers.nullable(Timer.class)); } @Test public void testStartCollectingGaugesStartsCollectingMetricsInForegroundState() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); verify(fakeCpuGaugeCollector) .startCollecting( eq(DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_FG_MS), @@ -150,25 +165,14 @@ public void testStartCollectingGaugesStartsCollectingMetricsInForegroundState() ArgumentMatchers.nullable(Timer.class)); } - @Test - public void - testStartCollectingGaugesDoesNotStartCollectingMetricsWithUnknownApplicationProcessState() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges( - fakeSession, ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN); - verify(fakeCpuGaugeCollector, never()) - .startCollecting(ArgumentMatchers.anyLong(), ArgumentMatchers.nullable(Timer.class)); - verify(fakeMemoryGaugeCollector, never()) - .startCollecting(ArgumentMatchers.anyLong(), ArgumentMatchers.nullable(Timer.class)); - } - @Test public void stopCollectingCPUMetric_invalidCPUCaptureFrequency_OtherMetricsWithValidFrequencyInBackground() { // PASS 1: Test with 0 doReturn(0L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyBackgroundMs(); - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.BACKGROUND); + PerfSession fakeSession1 = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.BACKGROUND); + testGaugeManager.startCollectingGauges(fakeSession1); // Verify that Cpu metric collection is not started verify(fakeCpuGaugeCollector, never()) @@ -180,8 +184,8 @@ public void testStartCollectingGaugesStartsCollectingMetricsInForegroundState() // PASS 2: Test with -ve value doReturn(-25L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyBackgroundMs(); - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.BACKGROUND); + PerfSession fakeSession2 = createTestSession(1); + testGaugeManager.startCollectingGauges(fakeSession2); // Verify that Cpu metric collection is not started verify(fakeCpuGaugeCollector, never()) @@ -197,8 +201,9 @@ public void testStartCollectingGaugesStartsCollectingMetricsInForegroundState() startCollectingGaugesOnBackground_invalidMemoryCaptureMs_onlyDisableMemoryCollection() { // PASS 1: Test with 0 doReturn(0L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyBackgroundMs(); - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.BACKGROUND); + PerfSession fakeSession1 = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.BACKGROUND); + testGaugeManager.startCollectingGauges(fakeSession1); // Verify that Memory metric collection is not started verify(fakeMemoryGaugeCollector, never()) @@ -210,8 +215,8 @@ public void testStartCollectingGaugesStartsCollectingMetricsInForegroundState() // PASS 2: Test with -ve value doReturn(-25L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyBackgroundMs(); - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.BACKGROUND); + PerfSession fakeSession2 = createTestSession(2); + testGaugeManager.startCollectingGauges(fakeSession2); // Verify that Memory metric collection is not started verify(fakeMemoryGaugeCollector, never()) @@ -226,8 +231,8 @@ public void testStartCollectingGaugesStartsCollectingMetricsInForegroundState() public void stopCollectingCPUMetric_invalidCPUCaptureFrequency_OtherMetricsWithValidFrequency() { // PASS 1: Test with 0 doReturn(0L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession1 = createTestSession(1); + testGaugeManager.startCollectingGauges(fakeSession1); // Verify that Cpu metric collection is not started verify(fakeCpuGaugeCollector, never()) @@ -239,8 +244,8 @@ public void stopCollectingCPUMetric_invalidCPUCaptureFrequency_OtherMetricsWithV // PASS 2: Test with -ve value doReturn(-25L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession2 = createTestSession(2); + testGaugeManager.startCollectingGauges(fakeSession2); // Verify that Cpu metric collection is not started verify(fakeCpuGaugeCollector, never()) @@ -256,8 +261,9 @@ public void stopCollectingCPUMetric_invalidCPUCaptureFrequency_OtherMetricsWithV startCollectingGaugesOnForeground_invalidMemoryCaptureMs_onlyDisableMemoryCollection() { // PASS 1: Test with 0 doReturn(0L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession1 = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession1); // Verify that Memory metric collection is not started verify(fakeMemoryGaugeCollector, never()) @@ -269,8 +275,8 @@ public void stopCollectingCPUMetric_invalidCPUCaptureFrequency_OtherMetricsWithV // PASS 2: Test with -ve value doReturn(-25L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession2 = createTestSession(2); + testGaugeManager.startCollectingGauges(fakeSession2); // Verify that Memory metric collection is not started verify(fakeMemoryGaugeCollector, never()) @@ -282,45 +288,29 @@ public void stopCollectingCPUMetric_invalidCPUCaptureFrequency_OtherMetricsWithV } @Test - public void testStartCollectingGaugesDoesNotStartAJobToConsumeMetricsWithUnknownAppState() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges( - fakeSession, ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN); + // TODO(b/394127311): Explore parametrized tests. + public void testStartCollectingGaugesDoesNotStartLogging_default() { + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState( + ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN); + testGaugeManager.startCollectingGauges(fakeSession); assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); } @Test - public void stopCollectingCPUMetrics_invalidCPUCaptureFrequency_appInForegrounf() { - // PASS 1: Test with 0 - doReturn(0L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); - - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.FOREGROUND); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); - - // PASS 2: Test with -ve value - doReturn(-25L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); - - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.FOREGROUND); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + public void testStartCollectingGaugesDoesNotStartLogging_appInForeground() { + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); } @Test - public void stopCollectingGauges_invalidMemoryCollectionFrequency_appInForeground() { - // PASS 1: Test with 0 - doReturn(0L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); - - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.FOREGROUND); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); - - // PASS 2: Test with -ve value - doReturn(-25L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); - - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.FOREGROUND); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + public void testStartCollectingGaugesDoesNotStartLogging_appInBackground() { + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.BACKGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); } @Test @@ -329,310 +319,276 @@ public void stopCollectingGauges_invalidGaugeCollectionFrequency_appInForeground doReturn(0L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); doReturn(0L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession1 = createTestSession(1); + testGaugeManager.startCollectingGauges(fakeSession1); assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); // PASS 2: Test with -ve value doReturn(-25L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); doReturn(-25L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); - PerfSession fakeSession2 = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.FOREGROUND); + PerfSession fakeSession2 = createTestSession(2); + testGaugeManager.startCollectingGauges(fakeSession2); assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); } @Test - public void startCollectingGauges_validGaugeCollectionFrequency_appInForeground() { - doReturn(25L).when(mockConfigResolver).getSessionsCpuCaptureFrequencyForegroundMs(); - doReturn(15L).when(mockConfigResolver).getSessionsMemoryCaptureFrequencyForegroundMs(); + public void testGaugeCounterStartsAJobToConsumeTheGeneratedMetrics() { + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + GaugeCounter.setGaugeManager(testGaugeManager); - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.FOREGROUND); + // There's no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); - assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) - .isEqualTo(15L * APPROX_NUMBER_OF_DATA_POINTS_PER_GAUGE_METRIC); - } + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT - 10); - @Test - public void testStartCollectingGaugesStartsAJobToConsumeTheGeneratedMetrics() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.BACKGROUND); + // There's still no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); + + generateMetricsAndIncrementCounter(10); assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) - .isEqualTo( - getMinimumBackgroundCollectionFrequency() - * APPROX_NUMBER_OF_DATA_POINTS_PER_GAUGE_METRIC); - - CpuMetricReading fakeCpuMetricReading1 = createFakeCpuMetricReading(200, 100); - CpuMetricReading fakeCpuMetricReading2 = createFakeCpuMetricReading(300, 200); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading1); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading2); - - AndroidMemoryReading fakeMemoryMetricReading1 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 123456); - AndroidMemoryReading fakeMemoryMetricReading2 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 23454678); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading1); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading2); + .isEqualTo(TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + + // Generate additional metrics, but doesn't start logging them as it hasn't met the threshold. + generateMetricsAndIncrementCounter(5); + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); + GaugeMetric recordedGaugeMetric = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); + getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric, fakeCpuMetricReading1, fakeCpuMetricReading2); + // It flushes all the original metrics in the ConcurrentLinkedQueues, but not the new ones + // added after the task completed. + int recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(MAX_GAUGE_COUNTER_LIMIT); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric, fakeMemoryMetricReading1, fakeMemoryMetricReading2); + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); } @Test - public void testStopCollectingGaugesStopsCollectingAllGaugeMetrics() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); + public void testGaugeCounterIsDecrementedWhenLogged() { + int priorGaugeCounter = GaugeCounter.count(); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.BACKGROUND); - verify(fakeCpuGaugeCollector) - .startCollecting(eq(DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_BG_MS), ArgumentMatchers.any()); + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + GaugeCounter.setGaugeManager(testGaugeManager); - testGaugeManager.stopCollectingGauges(); + // There's no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); - verify(fakeCpuGaugeCollector).stopCollecting(); - verify(fakeMemoryGaugeCollector).stopCollecting(); + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT - 10); + + // There's still no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); + + generateMetricsAndIncrementCounter(10); + + assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) + .isEqualTo(TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS); + + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCounter + MAX_GAUGE_COUNTER_LIMIT); + fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCounter); } @Test - public void testStopCollectingGaugesCreatesOneLastJobToConsumeAnyPendingMetrics() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.BACKGROUND); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + public void testDuplicateGaugeLoggingIsAvoided() { + int priorGaugeCounter = GaugeCounter.count(); + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + GaugeCounter.setGaugeManager(testGaugeManager); + + // There's no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); - testGaugeManager.stopCollectingGauges(); - assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT - 20); - CpuMetricReading fakeCpuMetricReading = createFakeCpuMetricReading(200, 100); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading); + // There's still no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); - AndroidMemoryReading fakeMemoryMetricReading = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 23454678); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading); + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT); + assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) .isEqualTo(TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); + GaugeMetric recordedGaugeMetric = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric, fakeCpuMetricReading); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric, fakeMemoryMetricReading); + getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND); + + // It flushes all the metrics in the ConcurrentLinkedQueues that were added. + int recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(2 * MAX_GAUGE_COUNTER_LIMIT - 20); + + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCounter); } @Test - public void testGaugeManagerClearsTheQueueEachRun() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); + public void testUpdateAppStateHandlesMultipleAppStates() { + PerfSession fakeSession = createTestSession(1); + fakeSession.setGaugeAndEventCollectionEnabled(true); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + GaugeCounter.setGaugeManager(testGaugeManager); - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.BACKGROUND); + // Generate metrics that don't exceed the GaugeCounter.MAX_COUNT. + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT - 10); - fakeCpuGaugeCollector.cpuMetricReadings.add(createFakeCpuMetricReading(200, 100)); - fakeCpuGaugeCollector.cpuMetricReadings.add(createFakeCpuMetricReading(300, 400)); - fakeMemoryGaugeCollector.memoryMetricReadings.add( - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 1234)); + // There's no job to log the gauges. + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); + + testGaugeManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); - assertThat(fakeCpuGaugeCollector.cpuMetricReadings).isNotEmpty(); - assertThat(fakeMemoryGaugeCollector.memoryMetricReadings).isNotEmpty(); + assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) + .isEqualTo(TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - assertThat(fakeCpuGaugeCollector.cpuMetricReadings).isEmpty(); - assertThat(fakeMemoryGaugeCollector.memoryMetricReadings).isEmpty(); + shadowOf(Looper.getMainLooper()).idle(); + + // Generate additional metrics in the new app state. + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT + 1); + + GaugeMetric recordedGaugeMetric = + getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND); - fakeCpuGaugeCollector.cpuMetricReadings.add(createFakeCpuMetricReading(200, 100)); - fakeMemoryGaugeCollector.memoryMetricReadings.add( - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 1234)); - fakeMemoryGaugeCollector.memoryMetricReadings.add( - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 2345)); + // It flushes all metrics in the ConcurrentLinkedQueues. + int recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(MAX_GAUGE_COUNTER_LIMIT - 10); - assertThat(fakeCpuGaugeCollector.cpuMetricReadings).isNotEmpty(); - assertThat(fakeMemoryGaugeCollector.memoryMetricReadings).isNotEmpty(); + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); + // Simulate gauges collected in the new app state. fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - assertThat(fakeCpuGaugeCollector.cpuMetricReadings).isEmpty(); - assertThat(fakeMemoryGaugeCollector.memoryMetricReadings).isEmpty(); + shadowOf(Looper.getMainLooper()).idle(); + + recordedGaugeMetric = getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND); + + // Verify the metrics in the new app state. + recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(MAX_GAUGE_COUNTER_LIMIT + 1); + + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); } @Test - public void testStartingGaugeManagerWithNewSessionIdButSameAppState() { - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); + public void testGaugeManagerHandlesMultipleSessionIds() { + PerfSession fakeSession = createTestSession(1); + fakeSession.setGaugeAndEventCollectionEnabled(true); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.BACKGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + GaugeCounter.setGaugeManager(testGaugeManager); - // Start collecting Gauges. - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.BACKGROUND); - CpuMetricReading fakeCpuMetricReading1 = createFakeCpuMetricReading(200, 100); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading1); - AndroidMemoryReading fakeMemoryMetricReading1 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 1234); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading1); + // Generate metrics that don't exceed the GaugeCounter.MAX_COUNT. + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT - 10); + + PerfSession updatedPerfSession = createTestSession(2); + updatedPerfSession.setGaugeAndEventCollectionEnabled(true); + + // A new session and updated app state. + testGaugeManager.startCollectingGauges(updatedPerfSession); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + + assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) + .isEqualTo(TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric1 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric1, fakeCpuMetricReading1); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric1, fakeMemoryMetricReading1); - - // One Cpu and Memory metric was added when the gauge was collecting for the previous sessionId. - CpuMetricReading fakeCpuMetricReading2 = createFakeCpuMetricReading(400, 500); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading2); - AndroidMemoryReading fakeMemoryMetricReading2 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 2345); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading2); - - PerfSession fakeSession2 = new PerfSession("sessionId2", new Clock()); - - // Start collecting gauges for new session, but same app state. - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.BACKGROUND); - - // The next sweep conducted by GaugeManager still associates metrics to old sessionId and state. - fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric2 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric2, fakeCpuMetricReading2); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric2, fakeMemoryMetricReading2); - - // Collect some more Cpu and Memory metrics and verify that they're associated with new - // sessionId and state. - CpuMetricReading fakeCpuMetricReading3 = createFakeCpuMetricReading(500, 600); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading3); - AndroidMemoryReading fakeMemoryMetricReading3 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 3456); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading3); + shadowOf(Looper.getMainLooper()).idle(); + + // Generate metrics for the new session. + generateMetricsAndIncrementCounter(MAX_GAUGE_COUNTER_LIMIT + 1); + + GaugeMetric recordedGaugeMetric = + getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND); + + // It flushes all metrics in the ConcurrentLinkedQueues. + int recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(MAX_GAUGE_COUNTER_LIMIT - 10); + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); + + // Simulate gauges collected in the new app state. fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric3 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId2", recordedGaugeMetric3, fakeCpuMetricReading3); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId2", recordedGaugeMetric3, fakeMemoryMetricReading3); + shadowOf(Looper.getMainLooper()).idle(); + + recordedGaugeMetric = getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND); + + // Verify the metrics in the new app state. + recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(MAX_GAUGE_COUNTER_LIMIT + 1); + + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(2)); } @Test - public void testStartGaugeManagerWithSameSessionIdButDifferentAppState() { - PerfSession fakeSession = new PerfSession("sessionId", new Clock()); + public void testStopCollectingGaugesStopsCollectingAllGaugeMetrics() { + PerfSession fakeSession = createTestSession(1); - // Start collecting Gauges. - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.BACKGROUND); - CpuMetricReading fakeCpuMetricReading1 = createFakeCpuMetricReading(200, 100); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading1); - AndroidMemoryReading fakeMemoryMetricReading1 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 1234); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading1); + testGaugeManager.startCollectingGauges(fakeSession); + verify(fakeCpuGaugeCollector) + .startCollecting(eq(DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_FG_MS), ArgumentMatchers.any()); + verify(fakeMemoryGaugeCollector) + .startCollecting( + eq(DEFAULT_MEMORY_GAUGE_COLLECTION_FREQUENCY_FG_MS), ArgumentMatchers.any()); - fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric1 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric1, fakeCpuMetricReading1); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric1, fakeMemoryMetricReading1); - - // One Cpu and Memory metric was added when the gauge was collecting for the previous sessionId. - CpuMetricReading fakeCpuMetricReading2 = createFakeCpuMetricReading(400, 500); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading2); - AndroidMemoryReading fakeMemoryMetricReading2 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 2345); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading2); - - // Start collecting gauges for same session, but new app state - testGaugeManager.startCollectingGauges(fakeSession, ApplicationProcessState.FOREGROUND); - - // The next sweep conducted by GaugeManager still associates metrics to old sessionId and state. - fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric2 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric2, fakeCpuMetricReading2); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric2, fakeMemoryMetricReading2); - - // Collect some more Cpu and Memory metrics and verify that they're associated with new - // sessionId and state. - CpuMetricReading fakeCpuMetricReading3 = createFakeCpuMetricReading(500, 600); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading3); - AndroidMemoryReading fakeMemoryMetricReading3 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 3456); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading3); + testGaugeManager.stopCollectingGauges(); - fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric3 = - getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric3, fakeCpuMetricReading3); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric3, fakeMemoryMetricReading3); + verify(fakeCpuGaugeCollector).stopCollecting(); + verify(fakeMemoryGaugeCollector).stopCollecting(); } @Test - public void testStartGaugeManagerWithNewSessionIdAndNewAppState() { - PerfSession fakeSession1 = new PerfSession("sessionId", new Clock()); + public void testStopCollectingGaugesCreatesOneLastJobToConsumeAnyPendingMetrics() { + PerfSession fakeSession = createTestSession(1); + testGaugeManager.setApplicationProcessState(ApplicationProcessState.FOREGROUND); + testGaugeManager.startCollectingGauges(fakeSession); + assertThat(fakeScheduledExecutorService.isEmpty()).isTrue(); - // Start collecting Gauges. - testGaugeManager.startCollectingGauges(fakeSession1, ApplicationProcessState.BACKGROUND); - CpuMetricReading fakeCpuMetricReading1 = createFakeCpuMetricReading(200, 100); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading1); - AndroidMemoryReading fakeMemoryMetricReading1 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 1234); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading1); + generateMetricsAndIncrementCounter(2); - fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric1 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric1, fakeCpuMetricReading1); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric1, fakeMemoryMetricReading1); - - // One Cpu and Memory metric was added when the gauge was collecting for the previous sessionId. - CpuMetricReading fakeCpuMetricReading2 = createFakeCpuMetricReading(400, 500); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading2); - AndroidMemoryReading fakeMemoryMetricReading2 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 2345); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading2); - - PerfSession fakeSession2 = new PerfSession("sessionId2", new Clock()); - - // Start collecting gauges for new session and new app state - testGaugeManager.startCollectingGauges(fakeSession2, ApplicationProcessState.FOREGROUND); - - // The next sweep conducted by GaugeManager still associates metrics to old sessionId and state. - fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric2 = - getLastRecordedGaugeMetric(ApplicationProcessState.BACKGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric2, fakeCpuMetricReading2); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId", recordedGaugeMetric2, fakeMemoryMetricReading2); - - // Collect some more Cpu and Memory metrics and verify that they're associated with new - // sessionId and state. - CpuMetricReading fakeCpuMetricReading3 = createFakeCpuMetricReading(500, 600); - fakeCpuGaugeCollector.cpuMetricReadings.add(fakeCpuMetricReading3); - AndroidMemoryReading fakeMemoryMetricReading3 = - createFakeAndroidMetricReading(/* currentUsedAppJavaHeapMemoryKb= */ 3456); - fakeMemoryGaugeCollector.memoryMetricReadings.add(fakeMemoryMetricReading3); + testGaugeManager.stopCollectingGauges(); + assertThat(fakeScheduledExecutorService.isEmpty()).isFalse(); + + assertThat(fakeScheduledExecutorService.getDelayToNextTask(TimeUnit.MILLISECONDS)) + .isEqualTo(TIME_TO_WAIT_BEFORE_FLUSHING_GAUGES_QUEUE_MS); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); - GaugeMetric recordedGaugeMetric3 = - getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1); - assertThatCpuGaugeMetricWasSentToTransport( - "sessionId2", recordedGaugeMetric3, fakeCpuMetricReading3); - assertThatMemoryGaugeMetricWasSentToTransport( - "sessionId2", recordedGaugeMetric3, fakeMemoryMetricReading3); + + GaugeMetric recordedGaugeMetric = + getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND); + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); + int recordedGaugeMetricsCount = + recordedGaugeMetric.getAndroidMemoryReadingsCount() + + recordedGaugeMetric.getCpuMetricReadingsCount(); + assertThat(recordedGaugeMetricsCount).isEqualTo(2); + + // TODO(b/394127311): Investigate why this isn't 0 on local runs. + // assertThat(GaugeCounter.INSTANCE.count()).isEqualTo(0); } @Test @@ -641,13 +597,13 @@ public void testLogGaugeMetadataSendDataToTransport() { when(fakeGaugeMetadataManager.getMaxAppJavaHeapMemoryKb()).thenReturn(1000); when(fakeGaugeMetadataManager.getMaxEncouragedAppJavaHeapMemoryKb()).thenReturn(800); - testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND); + testGaugeManager.logGaugeMetadata(testSessionId(1)); GaugeMetric recordedGaugeMetric = - getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1); + getLastRecordedGaugeMetric(ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN); GaugeMetadata recordedGaugeMetadata = recordedGaugeMetric.getGaugeMetadata(); - assertThat(recordedGaugeMetric.getSessionId()).isEqualTo("sessionId"); + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); assertThat(recordedGaugeMetadata.getDeviceRamSizeKb()) .isEqualTo(fakeGaugeMetadataManager.getDeviceRamSizeKb()); @@ -658,8 +614,7 @@ public void testLogGaugeMetadataSendDataToTransport() { } @Test - public void testLogGaugeMetadataDoesntLogWhenGaugeMetadataManagerNotAvailable() { - + public void testLogGaugeMetadataDoesNotLogWhenGaugeMetadataManagerNotAvailable() { testGaugeManager = new GaugeManager( new Lazy<>(() -> fakeScheduledExecutorService), @@ -669,8 +624,7 @@ public void testLogGaugeMetadataDoesntLogWhenGaugeMetadataManagerNotAvailable() new Lazy<>(() -> fakeCpuGaugeCollector), new Lazy<>(() -> fakeMemoryGaugeCollector)); - assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND)) - .isFalse(); + assertThat(testGaugeManager.logGaugeMetadata(testSessionId(1))).isFalse(); } @Test @@ -685,18 +639,17 @@ public void testLogGaugeMetadataLogsAfterApplicationContextIsSet() { new Lazy<>(() -> fakeCpuGaugeCollector), new Lazy<>(() -> fakeMemoryGaugeCollector)); - assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND)) - .isFalse(); + assertThat(testGaugeManager.logGaugeMetadata(testSessionId(1))).isFalse(); testGaugeManager.initializeGaugeMetadataManager(ApplicationProvider.getApplicationContext()); - assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND)) - .isTrue(); + assertThat(testGaugeManager.logGaugeMetadata(testSessionId(1))).isTrue(); GaugeMetric recordedGaugeMetric = - getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1); + getLastRecordedGaugeMetric(ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN); GaugeMetadata recordedGaugeMetadata = recordedGaugeMetric.getGaugeMetadata(); - assertThat(recordedGaugeMetric.getSessionId()).isEqualTo("sessionId"); + assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(testSessionId(1)); + assertThat(recordedGaugeMetadata).isNotEqualTo(GaugeMetadata.getDefaultInstance()); } @Test @@ -716,6 +669,22 @@ private long getMinimumBackgroundCollectionFrequency() { return DEFAULT_CPU_GAUGE_COLLECTION_FREQUENCY_BG_MS; } + // Simulates the behavior of Cpu and Memory Gauge collector. + private void generateMetricsAndIncrementCounter(long count) { + // TODO(b/394127311): Explore actually collecting metrics using the fake Cpu and Memory + // metric collectors. + Random random = new Random(); + for (int i = 0; i < count; ++i) { + if (random.nextInt(2) == 0) { + fakeCpuGaugeCollector.cpuMetricReadings.add(createFakeCpuMetricReading(100, 200)); + GaugeCounter.incrementCounter(); + } else { + fakeMemoryGaugeCollector.memoryMetricReadings.add(createFakeAndroidMetricReading(100)); + GaugeCounter.incrementCounter(); + } + } + } + private CpuMetricReading createFakeCpuMetricReading(long userTimeUs, long systemTimeUs) { CpuMetricReading.Builder fakeMetricReadingBuilder = CpuMetricReading.newBuilder(); fakeMetricReadingBuilder.setClientTimeUs(System.currentTimeMillis()); @@ -735,35 +704,26 @@ private AndroidMemoryReading createFakeAndroidMetricReading(int currentUsedAppJa * Gets the last recorded GaugeMetric, and verifies that they were logged for the right {@link * ApplicationProcessState}. * - * @param applicationProcessState The expected {@link ApplicationProcessState} that it was logged - * to. - * @param timesLogged Number of {@link GaugeMetric} that were expected to be logged to Transport. + * @param expectedApplicationProcessState The expected {@link ApplicationProcessState} that it was logged + * to. * @return The last logged {@link GaugeMetric}. */ private GaugeMetric getLastRecordedGaugeMetric( - ApplicationProcessState applicationProcessState, int timesLogged) { + ApplicationProcessState expectedApplicationProcessState) { ArgumentCaptor argMetric = ArgumentCaptor.forClass(GaugeMetric.class); - verify(mockTransportManager, times(timesLogged)) - .log(argMetric.capture(), eq(applicationProcessState)); + + // TODO(b/394127311): Revisit transportManager.log method which is only being called in unit + // tests. + if (expectedApplicationProcessState + == ApplicationProcessState.APPLICATION_PROCESS_STATE_UNKNOWN) { + verify(mockTransportManager, times(1)).log(argMetric.capture()); + } else { + verify(mockTransportManager, times(1)) + .log(argMetric.capture(), eq(expectedApplicationProcessState)); + } reset(mockTransportManager); // Required after resetting the mock. By default we assume that Transport is initialized. when(mockTransportManager.isInitialized()).thenReturn(true); return argMetric.getValue(); } - - private void assertThatCpuGaugeMetricWasSentToTransport( - String sessionId, GaugeMetric recordedGaugeMetric, CpuMetricReading... cpuMetricReadings) { - assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(sessionId); - assertThat(recordedGaugeMetric.getCpuMetricReadingsList()) - .containsAtLeastElementsIn(cpuMetricReadings); - } - - private void assertThatMemoryGaugeMetricWasSentToTransport( - String sessionId, - GaugeMetric recordedGaugeMetric, - AndroidMemoryReading... androidMetricReadings) { - assertThat(recordedGaugeMetric.getSessionId()).isEqualTo(sessionId); - assertThat(recordedGaugeMetric.getAndroidMemoryReadingsList()) - .containsAtLeastElementsIn(androidMetricReadings); - } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java index 292747121dd..1592f77e5ad 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/GaugeMetadataManagerTest.java @@ -15,26 +15,20 @@ package com.google.firebase.perf.session.gauges; import static com.google.common.truth.Truth.assertThat; -import static java.nio.charset.StandardCharsets.UTF_8; import static org.mockito.MockitoAnnotations.initMocks; import static org.robolectric.Shadows.shadowOf; import android.app.ActivityManager; import android.content.Context; -import android.os.Environment; import androidx.test.core.app.ApplicationProvider; import com.google.firebase.perf.FirebasePerformanceTestBase; import com.google.firebase.perf.util.StorageUnit; -import java.io.File; import java.io.IOException; -import java.io.Writer; -import java.nio.file.Files; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.robolectric.RobolectricTestRunner; -import org.robolectric.shadows.ShadowEnvironment; /** Unit tests for {@link com.google.firebase.perf.session.gauges.GaugeMetadataManager} */ @RunWith(RobolectricTestRunner.class) @@ -49,12 +43,11 @@ public class GaugeMetadataManagerTest extends FirebasePerformanceTestBase { @Mock private Runtime runtime; private ActivityManager activityManager; - private Context appContext; @Before public void setUp() { initMocks(this); - appContext = ApplicationProvider.getApplicationContext(); + Context appContext = ApplicationProvider.getApplicationContext(); activityManager = (ActivityManager) appContext.getSystemService(Context.ACTIVITY_SERVICE); mockMemory(); @@ -90,62 +83,5 @@ public void testGetDeviceRamSize_returnsExpectedValue() throws IOException { int ramSize = testGaugeMetadataManager.getDeviceRamSizeKb(); assertThat(ramSize).isEqualTo(StorageUnit.BYTES.toKilobytes(DEVICE_RAM_SIZE_BYTES)); - assertThat(ramSize).isEqualTo(testGaugeMetadataManager.readTotalRAM(createFakeMemInfoFile())); } - - /** @return The file path of this fake file which can be used to read the file. */ - private String createFakeMemInfoFile() throws IOException { - // Due to file permission issues on forge, it's easiest to just write this file to the emulated - // robolectric external storage. - ShadowEnvironment.setExternalStorageState(Environment.MEDIA_MOUNTED); - - File file = new File(Environment.getExternalStorageDirectory(), "FakeProcMemInfoFile"); - Writer fileWriter; - - fileWriter = Files.newBufferedWriter(file.toPath(), UTF_8); - fileWriter.write(MEM_INFO_CONTENTS); - fileWriter.close(); - - return file.getAbsolutePath(); - } - - private static final String MEM_INFO_CONTENTS = - "MemTotal: " - + DEVICE_RAM_SIZE_KB - + " kB\n" - + "MemFree: 542404 kB\n" - + "MemAvailable: 1392324 kB\n" - + "Buffers: 64292 kB\n" - + "Cached: 826180 kB\n" - + "SwapCached: 4196 kB\n" - + "Active: 934768 kB\n" - + "Inactive: 743812 kB\n" - + "Active(anon): 582132 kB\n" - + "Inactive(anon): 241500 kB\n" - + "Active(file): 352636 kB\n" - + "Inactive(file): 502312 kB\n" - + "Unevictable: 5148 kB\n" - + "Mlocked: 256 kB\n" - + "SwapTotal: 524284 kB\n" - + "SwapFree: 484800 kB\n" - + "Dirty: 4 kB\n" - + "Writeback: 0 kB\n" - + "AnonPages: 789404 kB\n" - + "Mapped: 241928 kB\n" - + "Shmem: 30632 kB\n" - + "Slab: 122320 kB\n" - + "SReclaimable: 42552 kB\n" - + "SUnreclaim: 79768 kB\n" - + "KernelStack: 22816 kB\n" - + "PageTables: 35344 kB\n" - + "NFS_Unstable: 0 kB\n" - + "Bounce: 0 kB\n" - + "WritebackTmp: 0 kB\n" - + "CommitLimit: 2042280 kB\n" - + "Committed_AS: 76623352 kB\n" - + "VmallocTotal: 251658176 kB\n" - + "VmallocUsed: 232060 kB\n" - + "VmallocChunk: 251347444 kB\n" - + "NvMapMemFree: 48640 kB\n" - + "NvMapMemUsed: 471460 kB\n"; } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollectorTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollectorTest.java index a99be17bee3..0c4769e207c 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollectorTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/session/gauges/MemoryGaugeCollectorTest.java @@ -59,9 +59,11 @@ private void mockMemory() { @Test public void testStartCollecting_addsMemoryMetricReadingsToQueue() { + int priorGaugeCount = GaugeCounter.count(); testGaugeCollector.startCollecting(/* memoryMetricCollectionRateMs= */ 100, new Timer()); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); assertThat(testGaugeCollector.memoryMetricReadings).hasSize(1); + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCount + 1); } @Test @@ -148,11 +150,17 @@ public void testCollectedMemoryMetric_containsApproximatelyCorrectTimestamp() { @Test public void testCollectOnce_addOnlyOneMemoryMetricReadingToQueue() { + int priorGaugeCount = GaugeCounter.count(); assertThat(testGaugeCollector.memoryMetricReadings).isEmpty(); testGaugeCollector.collectOnce(new Timer()); fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + + // Simulate running an additional task. + fakeScheduledExecutorService.simulateSleepExecutingAtMostOneTask(); + assertThat(testGaugeCollector.memoryMetricReadings).hasSize(1); + assertThat(GaugeCounter.count()).isEqualTo(priorGaugeCount + 1); } @Test diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/transport/TransportManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/transport/TransportManagerTest.java index 5376265aa0e..07b50c4377d 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/transport/TransportManagerTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/transport/TransportManagerTest.java @@ -15,6 +15,8 @@ package com.google.firebase.perf.transport; import static com.google.common.truth.Truth.assertThat; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.createTestSession; +import static com.google.firebase.perf.session.FirebaseSessionsTestHelperKt.testSessionId; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; @@ -28,6 +30,7 @@ import android.content.Context; import android.content.pm.PackageInfo; +import android.os.Process; import androidx.test.core.app.ApplicationProvider; import com.google.android.datatransport.TransportFactory; import com.google.android.gms.tasks.Tasks; @@ -40,7 +43,6 @@ import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.shadows.ShadowPreconditions; -import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Constants; import com.google.firebase.perf.util.Constants.CounterNames; import com.google.firebase.perf.v1.AndroidMemoryReading; @@ -1168,8 +1170,7 @@ public void logGaugeMetric_globalCustomAttributesAreNotAdded() { public void logTraceMetric_sessionEnabled_doesNotStripOffSessionId() { TraceMetric.Builder validTrace = createValidTraceMetric().toBuilder(); List perfSessions = new ArrayList<>(); - perfSessions.add( - new com.google.firebase.perf.session.PerfSession("fakeSessionId", new Clock()).build()); + perfSessions.add(createTestSession(1).build()); validTrace.addAllPerfSessions(perfSessions); testTransportManager.log(validTrace.build()); @@ -1178,7 +1179,7 @@ public void logTraceMetric_sessionEnabled_doesNotStripOffSessionId() { PerfMetric loggedPerfMetric = getLastLoggedEvent(times(1)); assertThat(loggedPerfMetric.getTraceMetric().getPerfSessionsCount()).isEqualTo(1); assertThat(loggedPerfMetric.getTraceMetric().getPerfSessions(0).getSessionId()) - .isEqualTo("fakeSessionId"); + .isEqualTo(testSessionId(1)); } @Test @@ -1186,8 +1187,7 @@ public void logNetworkMetric_sessionEnabled_doesNotStripOffSessionId() { NetworkRequestMetric.Builder validNetworkRequest = createValidNetworkRequestMetric().toBuilder(); List perfSessions = new ArrayList<>(); - perfSessions.add( - new com.google.firebase.perf.session.PerfSession("fakeSessionId", new Clock()).build()); + perfSessions.add(createTestSession(1).build()); validNetworkRequest.clearPerfSessions().addAllPerfSessions(perfSessions); testTransportManager.log(validNetworkRequest.build()); @@ -1196,7 +1196,7 @@ public void logNetworkMetric_sessionEnabled_doesNotStripOffSessionId() { PerfMetric loggedPerfMetric = getLastLoggedEvent(times(1)); assertThat(loggedPerfMetric.getNetworkRequestMetric().getPerfSessionsCount()).isEqualTo(1); assertThat(loggedPerfMetric.getNetworkRequestMetric().getPerfSessions(0).getSessionId()) - .isEqualTo("fakeSessionId"); + .isEqualTo(testSessionId(1)); } @Test @@ -1400,6 +1400,11 @@ private void initializeTransport(boolean shouldInitialize) { if (shouldInitialize) { // Set the version name since Firebase sessions needs it. Context context = ApplicationProvider.getApplicationContext(); + + // For unit test, app context does not application info related to uid, so we have to force + // set it through process info. + context.getApplicationInfo().uid = Process.myUid(); + ShadowPackageManager shadowPackageManager = shadowOf(context.getPackageManager()); PackageInfo packageInfo = @@ -1469,6 +1474,8 @@ private static void validateApplicationInfo( .isEqualTo(FAKE_FIREBASE_APPLICATION_ID); assertThat(loggedPerfMetric.getApplicationInfo().getApplicationProcessState()) .isEqualTo(applicationProcessState); + assertThat(loggedPerfMetric.getApplicationInfo().getProcessName()) + .isEqualTo("com.google.firebase.perf.test"); assertThat(loggedPerfMetric.getApplicationInfo().hasAndroidAppInfo()).isTrue(); } diff --git a/firebase-sessions/test-app/src/main/AndroidManifest.xml b/firebase-sessions/test-app/src/main/AndroidManifest.xml index 8d2011ca18e..c42785262cc 100644 --- a/firebase-sessions/test-app/src/main/AndroidManifest.xml +++ b/firebase-sessions/test-app/src/main/AndroidManifest.xml @@ -28,7 +28,6 @@ android:exported="true" android:label="@string/app_name" android:name=".MainActivity" - android:process=":main" android:theme="@style/Theme.Widget_test_app.NoActionBar"> diff --git a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/TestApplication.kt b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/TestApplication.kt index f8b8dec2cc7..5146dbc8511 100644 --- a/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/TestApplication.kt +++ b/firebase-sessions/test-app/src/main/kotlin/com/google/firebase/testing/sessions/TestApplication.kt @@ -37,7 +37,12 @@ class TestApplication : MultiDexApplication() { override fun onCreate() { super.onCreate() Log.i(TAG, "TestApplication created on process: $myProcessName") - FirebaseApp.initializeApp(this) + + // Initialize firebase for all processes except the default process + // The default process will get initialized automatically by FirebaseInitProvider + if (myProcessName != packageName) { + FirebaseApp.initializeApp(this) + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { registerReceiver(