From 3e15e86bc299f4f12c62b72702b7862ca2e091ef Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 16:46:10 -0500 Subject: [PATCH 001/146] Upload a prototype of lazy AQS mapping --- .../src/third_party/crashpad | 2 +- firebase-crashlytics-ndk/src/third_party/lss | 2 +- .../src/third_party/mini_chromium | 2 +- .../firebase/perf/FirebasePerfEarly.java | 5 +- .../firebase/perf/FirebasePerfRegistrar.java | 8 +++ .../firebase/perf/FirebasePerformance.java | 6 ++ .../firebase/perf/metrics/AppStartTrace.java | 1 + .../firebase/perf/session/PerfSession.java | 30 ++++++--- .../firebase/perf/session/SessionManager.java | 62 ++++++++++--------- .../firebase/perf/session/SessionManagerKt.kt | 32 ++++++++++ .../perf/FirebasePerformanceTestBase.java | 2 +- .../perf/metrics/AppStartTraceTest.java | 2 +- .../NetworkRequestMetricBuilderTest.java | 4 +- .../firebase/perf/metrics/TraceTest.java | 4 +- .../perf/session/PerfSessionTest.java | 16 ++--- .../perf/session/SessionManagerTest.java | 22 +++---- .../api/FirebaseSessionsDependencies.kt | 13 ---- 17 files changed, 131 insertions(+), 82 deletions(-) create mode 100644 firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad index c902f6b1c9e..b8937c6cb4b 160000 --- a/firebase-crashlytics-ndk/src/third_party/crashpad +++ b/firebase-crashlytics-ndk/src/third_party/crashpad @@ -1 +1 @@ -Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c +Subproject commit b8937c6cb4b38c1ca06b46791c84b31632895f1f diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss index 9719c1e1e67..ed31caa60f2 160000 --- a/firebase-crashlytics-ndk/src/third_party/lss +++ b/firebase-crashlytics-ndk/src/third_party/lss @@ -1 +1 @@ -Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31 +Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0 diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium index 4332ddb6963..c081fd005b0 160000 --- a/firebase-crashlytics-ndk/src/third_party/mini_chromium +++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium @@ -1 +1 @@ -Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430 +Subproject commit c081fd005b09a59a505b09a4b506f8ba45f70859 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..91fcf4a0c49 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 @@ -23,6 +23,8 @@ import com.google.firebase.perf.metrics.AppStartTrace; import com.google.firebase.perf.session.SessionManager; import java.util.concurrent.Executor; +import com.google.firebase.perf.logging.AndroidLogger; +import com.google.firebase.perf.session.SessionManagerKt; /** * The Firebase Performance early initialization. @@ -51,12 +53,11 @@ public FirebasePerfEarly( 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. + AndroidLogger.getInstance().debug("Initializing Gauge Collection"); 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..4f3a1d242ea 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,10 @@ 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.FirebaseSessions; +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 +51,10 @@ public class FirebasePerfRegistrar implements ComponentRegistrar { private static final String LIBRARY_NAME = "fire-perf"; private static final String EARLY_LIBRARY_NAME = "fire-perf-early"; + static { + 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..419f42246ce 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 @@ -37,11 +37,14 @@ import com.google.firebase.perf.metrics.HttpMetric; import com.google.firebase.perf.metrics.Trace; import com.google.firebase.perf.session.SessionManager; +import com.google.firebase.perf.session.SessionManagerKt; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Constants; import com.google.firebase.perf.util.ImmutableBundle; import com.google.firebase.perf.util.Timer; import com.google.firebase.remoteconfig.RemoteConfigComponent; +import com.google.firebase.sessions.api.FirebaseSessionsDependencies; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URL; @@ -199,6 +202,9 @@ public static FirebasePerformance getInstance() { ConsoleUrlGenerator.generateDashboardUrl( firebaseApp.getOptions().getProjectId(), appContext.getPackageName()))); } + + SessionManagerKt sessionSubscriber = new SessionManagerKt(isPerformanceCollectionEnabled()); + FirebaseSessionsDependencies.register(sessionSubscriber); } /** 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..7d76684dc01 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 @@ -370,6 +370,7 @@ public synchronized void onActivityResumed(Activity activity) { appStartActivity = new WeakReference(activity); onResumeTime = clock.getTime(); + // TODO(b/394127311): Defer this to SessionManagerKt this.startSession = SessionManager.getInstance().perfSession(); AndroidLogger.getInstance() .debug( 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..f206e919a30 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 @@ -24,12 +24,14 @@ import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.SessionVerbosity; import java.util.List; +import java.util.Objects; +import java.util.UUID; import java.util.concurrent.TimeUnit; /** Details of a session including a unique Id and related information. */ public class PerfSession implements Parcelable { - private final String sessionId; + private final String internalSessionId; private final Timer creationTime; private boolean isGaugeAndEventCollectionEnabled = false; @@ -37,31 +39,41 @@ public class PerfSession implements Parcelable { /* * Creates a PerfSession object and decides what metrics to collect. */ - public static PerfSession createWithId(@NonNull String sessionId) { - String prunedSessionId = sessionId.replace("-", ""); + public static PerfSession createNewSession() { + String prunedSessionId = UUID.randomUUID().toString().replace("-", ""); PerfSession session = new PerfSession(prunedSessionId, new Clock()); session.setGaugeAndEventCollectionEnabled(shouldCollectGaugesAndEvents()); + // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received, + // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. + // The assumption is that new PerfSessions *should* be limited to either App Start, or through AQS. + SessionManagerKt.Companion.getPerfSessionToAqs().put(prunedSessionId, null); + return session; } /** Creates a PerfSession with the provided {@code sessionId} and {@code clock}. */ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public PerfSession(String sessionId, Clock clock) { - this.sessionId = sessionId; + public PerfSession(String internalSessionId, Clock clock) { + this.internalSessionId = internalSessionId; creationTime = clock.getTime(); } private PerfSession(@NonNull Parcel in) { super(); - sessionId = in.readString(); + internalSessionId = in.readString(); isGaugeAndEventCollectionEnabled = in.readByte() != 0; creationTime = in.readParcelable(Timer.class.getClassLoader()); } /** Returns the sessionId of the object. */ public String sessionId() { - return sessionId; + // TODO(b/394127311): Verify edge cases. + return Objects.requireNonNull(SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId)).getSessionId(); + } + + protected String getInternalSessionId() { + return internalSessionId; } /** @@ -114,7 +126,7 @@ public boolean isSessionRunningTooLong() { /** Creates and returns the proto object for PerfSession object. */ public com.google.firebase.perf.v1.PerfSession build() { com.google.firebase.perf.v1.PerfSession.Builder sessionMetric = - com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId); + com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(internalSessionId); // If gauge collection is enabled, enable gauge collection verbosity. if (isGaugeAndEventCollectionEnabled) { @@ -189,7 +201,7 @@ public int describeContents() { * @param flags Additional flags about how the object should be written. */ public void writeToParcel(@NonNull Parcel out, int flags) { - out.writeString(sessionId); + out.writeString(internalSessionId); out.writeByte((byte) (isGaugeAndEventCollectionEnabled ? 1 : 0)); out.writeParcelable(creationTime, 0); } 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..869ef1ef055 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 @@ -27,8 +27,8 @@ 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; @@ -40,6 +40,8 @@ public class SessionManager extends AppStateUpdateHandler { @SuppressLint("StaticFieldLeak") private static final SessionManager instance = new SessionManager(); + private static final String COLD_START_GAUGE_NAME = "coldstart"; + private final GaugeManager gaugeManager; private final AppStateMonitor appStateMonitor; private final Set> clients = new HashSet<>(); @@ -61,7 +63,7 @@ private SessionManager() { // Generate a new sessionID for every cold start. this( GaugeManager.getInstance(), - PerfSession.createWithId(UUID.randomUUID().toString()), + PerfSession.createNewSession(), AppStateMonitor.getInstance()); } @@ -96,33 +98,33 @@ public void setApplicationContext(final Context appContext) { }); } - @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); - } - } - } +// @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); +// } +// } +// } /** * Checks if the current {@link PerfSession} is expired/timed out. If so, stop collecting gauges. @@ -145,7 +147,7 @@ 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.getInternalSessionId(), this.perfSession.getInternalSessionId())) { return; } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt new file mode 100644 index 00000000000..e2c4f9cab51 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt @@ -0,0 +1,32 @@ +package com.google.firebase.perf.session + +import com.google.firebase.sessions.api.SessionSubscriber +import com.google.firebase.perf.logging.AndroidLogger + + +class SessionManagerKt(val dataCollectionEnabled: Boolean): SessionSubscriber { + override val isDataCollectionEnabled: Boolean + get() = dataCollectionEnabled + + override val sessionSubscriberName: SessionSubscriber.Name + get() = SessionSubscriber.Name.PERFORMANCE + + override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { + AndroidLogger.getInstance().debug("Session Changed: $sessionDetails") + val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId + + // There can be situations where a new [PerfSession] was created, but an AQS wasn't + // available (during cold start). + if (perfSessionToAqs[currentInternalSessionId] == null) { + perfSessionToAqs[currentInternalSessionId] = sessionDetails + } else { + val newSession = PerfSession.createNewSession() + SessionManager.getInstance().updatePerfSession(newSession) + perfSessionToAqs[newSession.internalSessionId] = sessionDetails + } + } + + companion object { + val perfSessionToAqs: MutableMap by lazy { mutableMapOf() } + } +} \ No newline at end of file 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..5bd2c2a4c47 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 @@ -98,6 +98,6 @@ private static void forceVerboseSessionWithSamplingPercentage(long samplingPerce bundle.putFloat("sessions_sampling_percentage", samplingPercentage); ConfigResolver.getInstance().setMetadataBundle(new ImmutableBundle(bundle)); - SessionManager.getInstance().setPerfSession(PerfSession.createWithId("sessionId")); + SessionManager.getInstance().setPerfSession(PerfSession.createNewSession()); } } diff --git a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java index 36ae3d10116..47569cb4a2d 100644 --- a/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java +++ b/firebase-perf/src/test/java/com/google/firebase/perf/metrics/AppStartTraceTest.java @@ -96,7 +96,7 @@ public Timer answer(InvocationOnMock invocationOnMock) throws Throwable { @After public void reset() { - SessionManager.getInstance().updatePerfSession(PerfSession.createWithId("randomSessionId")); + SessionManager.getInstance().updatePerfSession(PerfSession.createNewSession()); } /** Test activity sequentially goes through onCreate()->onStart()->onResume() state change. */ 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..3f6a3f929e2 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 @@ -225,7 +225,7 @@ public void testSessionIdAdditionInNetworkRequestMetric() { assertThat(this.networkMetricBuilder.getSessions()).isEmpty(); int numberOfSessionIds = metricBuilder.getSessions().size(); - PerfSession perfSession = PerfSession.createWithId("testSessionId"); + PerfSession perfSession = PerfSession.createNewSession(); SessionManager.getInstance().updatePerfSession(perfSession); assertThat(metricBuilder.getSessions().size()).isEqualTo(numberOfSessionIds + 1); @@ -328,7 +328,7 @@ public void testUpdateSessionWithValidSessionIsAdded() { networkMetricBuilder.setRequestStartTimeMicros(/* time= */ 2000); assertThat(networkMetricBuilder.getSessions()).hasSize(1); - networkMetricBuilder.updateSession(PerfSession.createWithId("testSessionId")); + networkMetricBuilder.updateSession(PerfSession.createNewSession()); assertThat(networkMetricBuilder.getSessions()).hasSize(2); } 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..020bf433560 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 @@ -1016,7 +1016,7 @@ public void testSessionIdAdditionInTrace() { int numberOfSessionIds = trace.getSessions().size(); - PerfSession perfSession = PerfSession.createWithId("test_session_id"); + PerfSession perfSession = PerfSession.createNewSession(); SessionManager.getInstance().updatePerfSession(perfSession); assertThat(trace.getSessions()).hasSize(numberOfSessionIds + 1); @@ -1071,7 +1071,7 @@ public void testUpdateSessionWithValidSessionIsAdded() { trace.start(); assertThat(trace.getSessions()).hasSize(1); - trace.updateSession(PerfSession.createWithId("test_session_id")); + trace.updateSession(PerfSession.createNewSession()); assertThat(trace.getSessions()).hasSize(2); trace.stop(); 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..9551f1d11ae 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 @@ -160,21 +160,21 @@ public void testPerfSessionConversionWithoutVerbosity() { @Test public void testPerfSessionsCreateDisabledGaugeCollectionWhenVerboseSessionForceDisabled() { forceNonVerboseSession(); - PerfSession testPerfSession = PerfSession.createWithId("sessionId"); + PerfSession testPerfSession = PerfSession.createNewSession(); assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isFalse(); } @Test public void testPerfSessionsCreateDisabledGaugeCollectionWhenSessionsFeatureDisabled() { forceSessionsFeatureDisabled(); - PerfSession testPerfSession = PerfSession.createWithId("sessionId"); + PerfSession testPerfSession = PerfSession.createNewSession(); assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isFalse(); } @Test public void testPerfSessionsCreateEnablesGaugeCollectionWhenVerboseSessionForceEnabled() { forceVerboseSession(); - PerfSession testPerfSession = PerfSession.createWithId("sessionId"); + PerfSession testPerfSession = PerfSession.createNewSession(); assertThat(testPerfSession.isGaugeAndEventCollectionEnabled()).isTrue(); } @@ -185,16 +185,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.createNewSession()); + sessions.add(PerfSession.createNewSession()); + sessions.add(PerfSession.createNewSession()); // 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.createNewSession()); + sessions.add(PerfSession.createNewSession()); // Verify that the first session in the list of sessions was not verbose assertThat(sessions.get(0).isVerbose()).isFalse(); 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..0a8f1a98b40 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 @@ -215,7 +215,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIs SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(mockGaugeManager).stopCollectingGauges(); } @@ -226,8 +226,8 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession SessionManager testSessionManager = new SessionManager( - mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(mockGaugeManager).stopCollectingGauges(); } @@ -240,7 +240,7 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() { forceNonVerboseSession(); SessionManager testSessionManager = new SessionManager( - mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor); + mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); verify(mockGaugeManager, times(0)) .logGaugeMetadata( @@ -249,12 +249,12 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() { // Forcing a verbose session will enable Gauge collection forceVerboseSession(); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); 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")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any()); } @@ -303,7 +303,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, never()) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -322,8 +322,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(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, times(2)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -347,11 +347,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, times(1)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt index 8d3548c8f4b..4b636a155e0 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt @@ -40,19 +40,6 @@ object FirebaseSessionsDependencies { */ @JvmStatic fun addDependency(subscriberName: SessionSubscriber.Name) { - if (subscriberName == SessionSubscriber.Name.PERFORMANCE) { - throw IllegalArgumentException( - """ - Incompatible versions of Firebase Perf and Firebase Sessions. - A safe combination would be: - firebase-sessions:1.1.0 - firebase-crashlytics:18.5.0 - firebase-perf:20.5.0 - For more information contact Firebase Support. - """ - .trimIndent() - ) - } if (dependencies.containsKey(subscriberName)) { Log.d(TAG, "Dependency $subscriberName already added.") return From ad7217a2560ee77ef5b94f2544548cf1b20a3fd0 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 16:52:05 -0500 Subject: [PATCH 002/146] Improve logging --- .../java/com/google/firebase/perf/session/SessionManager.java | 3 +++ .../java/com/google/firebase/perf/session/SessionManagerKt.kt | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) 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 869ef1ef055..4c44ed4aa20 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 @@ -20,6 +20,7 @@ import androidx.annotation.VisibleForTesting; import com.google.firebase.perf.application.AppStateMonitor; import com.google.firebase.perf.application.AppStateUpdateHandler; +import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.v1.ApplicationProcessState; import com.google.firebase.perf.v1.GaugeMetadata; @@ -151,6 +152,8 @@ public void updatePerfSession(PerfSession perfSession) { return; } + AndroidLogger.getInstance().debug("Perf Session Changed: " + perfSession); + this.perfSession = perfSession; synchronized (clients) { diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt index e2c4f9cab51..458bd2ce82f 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt @@ -12,7 +12,7 @@ class SessionManagerKt(val dataCollectionEnabled: Boolean): SessionSubscriber { get() = SessionSubscriber.Name.PERFORMANCE override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { - AndroidLogger.getInstance().debug("Session Changed: $sessionDetails") + AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails") val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId // There can be situations where a new [PerfSession] was created, but an AQS wasn't From aa0da1f6fd0857583eb1f8818d574da7aeea171a Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:09:35 -0500 Subject: [PATCH 003/146] Better logging --- .../firebase/perf/session/PerfSession.java | 8 ++++- .../firebase/perf/session/SessionManager.java | 31 +------------------ 2 files changed, 8 insertions(+), 31 deletions(-) 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 f206e919a30..5f206e2038e 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 @@ -20,9 +20,13 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.firebase.perf.config.ConfigResolver; +import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.SessionVerbosity; +import com.google.firebase.sessions.SessionDetails; +import com.google.firebase.sessions.api.SessionSubscriber; + import java.util.List; import java.util.Objects; import java.util.UUID; @@ -69,7 +73,9 @@ private PerfSession(@NonNull Parcel in) { /** Returns the sessionId of the object. */ public String sessionId() { // TODO(b/394127311): Verify edge cases. - return Objects.requireNonNull(SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId)).getSessionId(); + SessionSubscriber.SessionDetails sessionDetails = SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId); + AndroidLogger.getInstance().debug("AQS for " + this.internalSessionId + " is " + sessionDetails); + return Objects.requireNonNull(sessionDetails).getSessionId(); } protected String getInternalSessionId() { 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 4c44ed4aa20..13dfde95b91 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 @@ -36,7 +36,7 @@ /** 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(); @@ -74,7 +74,6 @@ public SessionManager( this.gaugeManager = gaugeManager; this.perfSession = perfSession; this.appStateMonitor = appStateMonitor; - registerForAppState(); } /** @@ -99,34 +98,6 @@ public void setApplicationContext(final Context appContext) { }); } -// @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); -// } -// } -// } - /** * Checks if the current {@link PerfSession} is expired/timed out. If so, stop collecting gauges. * From 9a91b1b106129666071d4014345d1f68532a11e2 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:17:52 -0500 Subject: [PATCH 004/146] Switch the proto value to the AQS session --- .../main/java/com/google/firebase/perf/session/PerfSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 5f206e2038e..2e13360103c 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 @@ -132,7 +132,7 @@ public boolean isSessionRunningTooLong() { /** Creates and returns the proto object for PerfSession object. */ public com.google.firebase.perf.v1.PerfSession build() { com.google.firebase.perf.v1.PerfSession.Builder sessionMetric = - com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(internalSessionId); + com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId()); // If gauge collection is enabled, enable gauge collection verbosity. if (isGaugeAndEventCollectionEnabled) { From ed9f56c3008bd48c83794cb4a72da3dc72034d8a Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:21:09 -0500 Subject: [PATCH 005/146] Remove TODO in AppStartTrace --- .../java/com/google/firebase/perf/metrics/AppStartTrace.java | 1 - 1 file changed, 1 deletion(-) 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 7d76684dc01..7574f989d92 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 @@ -370,7 +370,6 @@ public synchronized void onActivityResumed(Activity activity) { appStartActivity = new WeakReference(activity); onResumeTime = clock.getTime(); - // TODO(b/394127311): Defer this to SessionManagerKt this.startSession = SessionManager.getInstance().perfSession(); AndroidLogger.getInstance() .debug( From ed765f54c12f15f61325d89834cd31450f6e1317 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:25:27 -0500 Subject: [PATCH 006/146] style --- .../firebase/perf/FirebasePerfEarly.java | 3 +- .../firebase/perf/FirebasePerfRegistrar.java | 2 - .../firebase/perf/FirebasePerformance.java | 1 - .../firebase/perf/session/PerfSession.java | 11 ++--- .../firebase/perf/session/SessionManager.java | 9 ++-- .../firebase/perf/session/SessionManagerKt.kt | 45 ++++++++++--------- .../perf/session/SessionManagerTest.java | 6 +-- 7 files changed, 35 insertions(+), 42 deletions(-) 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 91fcf4a0c49..969031a773f 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 @@ -20,11 +20,10 @@ import com.google.firebase.StartupTime; import com.google.firebase.perf.application.AppStateMonitor; import com.google.firebase.perf.config.ConfigResolver; +import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.metrics.AppStartTrace; import com.google.firebase.perf.session.SessionManager; import java.util.concurrent.Executor; -import com.google.firebase.perf.logging.AndroidLogger; -import com.google.firebase.perf.session.SessionManagerKt; /** * The Firebase Performance early initialization. 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 4f3a1d242ea..0754eddcd51 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,10 +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.FirebaseSessions; 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; 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 419f42246ce..cbfa6613d10 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 @@ -44,7 +44,6 @@ import com.google.firebase.perf.util.Timer; import com.google.firebase.remoteconfig.RemoteConfigComponent; import com.google.firebase.sessions.api.FirebaseSessionsDependencies; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URL; 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 2e13360103c..b36a2a683ed 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 @@ -24,9 +24,7 @@ import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.SessionVerbosity; -import com.google.firebase.sessions.SessionDetails; import com.google.firebase.sessions.api.SessionSubscriber; - import java.util.List; import java.util.Objects; import java.util.UUID; @@ -50,7 +48,8 @@ public static PerfSession createNewSession() { // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received, // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. - // The assumption is that new PerfSessions *should* be limited to either App Start, or through AQS. + // The assumption is that new PerfSessions *should* be limited to either App Start, or through + // AQS. SessionManagerKt.Companion.getPerfSessionToAqs().put(prunedSessionId, null); return session; @@ -73,8 +72,10 @@ private PerfSession(@NonNull Parcel in) { /** Returns the sessionId of the object. */ public String sessionId() { // TODO(b/394127311): Verify edge cases. - SessionSubscriber.SessionDetails sessionDetails = SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId); - AndroidLogger.getInstance().debug("AQS for " + this.internalSessionId + " is " + sessionDetails); + SessionSubscriber.SessionDetails sessionDetails = + SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId); + AndroidLogger.getInstance() + .debug("AQS for " + this.internalSessionId + " is " + sessionDetails); return Objects.requireNonNull(sessionDetails).getSessionId(); } 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 13dfde95b91..973bccbef6f 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 @@ -19,7 +19,6 @@ 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.AndroidLogger; import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.v1.ApplicationProcessState; @@ -62,10 +61,7 @@ public final PerfSession perfSession() { private SessionManager() { // Generate a new sessionID for every cold start. - this( - GaugeManager.getInstance(), - PerfSession.createNewSession(), - AppStateMonitor.getInstance()); + this(GaugeManager.getInstance(), PerfSession.createNewSession(), AppStateMonitor.getInstance()); } @VisibleForTesting @@ -119,7 +115,8 @@ public void stopGaugeCollectionIfSessionRunningTooLong() { */ public void updatePerfSession(PerfSession perfSession) { // Do not update the perf session if it is the exact same sessionId. - if (Objects.equals(perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) { + if (Objects.equals( + perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) { return; } diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt index 458bd2ce82f..cf27cad3175 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt @@ -1,32 +1,33 @@ package com.google.firebase.perf.session -import com.google.firebase.sessions.api.SessionSubscriber import com.google.firebase.perf.logging.AndroidLogger +import com.google.firebase.sessions.api.SessionSubscriber +class SessionManagerKt(val dataCollectionEnabled: Boolean) : SessionSubscriber { + override val isDataCollectionEnabled: Boolean + get() = dataCollectionEnabled -class SessionManagerKt(val dataCollectionEnabled: Boolean): SessionSubscriber { - override val isDataCollectionEnabled: Boolean - get() = dataCollectionEnabled - - override val sessionSubscriberName: SessionSubscriber.Name - get() = SessionSubscriber.Name.PERFORMANCE + override val sessionSubscriberName: SessionSubscriber.Name + get() = SessionSubscriber.Name.PERFORMANCE - override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { - AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails") - val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId + override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { + AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails") + val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId - // There can be situations where a new [PerfSession] was created, but an AQS wasn't - // available (during cold start). - if (perfSessionToAqs[currentInternalSessionId] == null) { - perfSessionToAqs[currentInternalSessionId] = sessionDetails - } else { - val newSession = PerfSession.createNewSession() - SessionManager.getInstance().updatePerfSession(newSession) - perfSessionToAqs[newSession.internalSessionId] = sessionDetails - } + // There can be situations where a new [PerfSession] was created, but an AQS wasn't + // available (during cold start). + if (perfSessionToAqs[currentInternalSessionId] == null) { + perfSessionToAqs[currentInternalSessionId] = sessionDetails + } else { + val newSession = PerfSession.createNewSession() + SessionManager.getInstance().updatePerfSession(newSession) + perfSessionToAqs[newSession.internalSessionId] = sessionDetails } + } - companion object { - val perfSessionToAqs: MutableMap by lazy { mutableMapOf() } + companion object { + val perfSessionToAqs: MutableMap by lazy { + mutableMapOf() } -} \ No newline at end of file + } +} 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 0a8f1a98b40..264028b8139 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 @@ -225,8 +225,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession forceSessionsFeatureDisabled(); SessionManager testSessionManager = - new SessionManager( - mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); + new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(mockGaugeManager).stopCollectingGauges(); @@ -239,8 +238,7 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() { // Start with a non verbose session forceNonVerboseSession(); SessionManager testSessionManager = - new SessionManager( - mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); + new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); verify(mockGaugeManager, times(0)) .logGaugeMetadata( From d355af2935a0022686b5534f5a971112d97a44f9 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:39:03 -0500 Subject: [PATCH 007/146] remove cold start gauge name --- .../java/com/google/firebase/perf/session/SessionManager.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 973bccbef6f..6ff17e859c1 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 @@ -39,9 +39,7 @@ public class SessionManager { @SuppressLint("StaticFieldLeak") private static final SessionManager instance = new SessionManager(); - - private static final String COLD_START_GAUGE_NAME = "coldstart"; - + private final GaugeManager gaugeManager; private final AppStateMonitor appStateMonitor; private final Set> clients = new HashSet<>(); From 4096b0a1149fc1ee5579f0546b92415779bb5122 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:41:24 -0500 Subject: [PATCH 008/146] More changes --- .../java/com/google/firebase/perf/session/PerfSession.java | 3 ++- .../java/com/google/firebase/perf/session/SessionManager.java | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) 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 b36a2a683ed..91b63df9041 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 @@ -79,7 +79,8 @@ public String sessionId() { return Objects.requireNonNull(sessionDetails).getSessionId(); } - protected String getInternalSessionId() { + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) + public String getInternalSessionId() { return internalSessionId; } 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 6ff17e859c1..54784d7a274 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 @@ -39,7 +39,7 @@ 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<>(); From ff75fd91f9aac24a883dfd56a1fdef4b9a0e47dc Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Mon, 3 Feb 2025 17:49:01 -0500 Subject: [PATCH 009/146] Update comments --- .../main/java/com/google/firebase/perf/FirebasePerfEarly.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 969031a773f..45dad3c78f0 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 @@ -54,9 +54,7 @@ public FirebasePerfEarly( // 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. - AndroidLogger.getInstance().debug("Initializing Gauge Collection"); + // The session is mapped to an AQS once AQS is initialized. SessionManager.getInstance().initializeGaugeCollection(); } } From 6bdf12c77f4e5890e083106527c3ce518ce052dd Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 12:06:18 -0500 Subject: [PATCH 010/146] Style --- .../main/java/com/google/firebase/perf/FirebasePerfEarly.java | 1 - 1 file changed, 1 deletion(-) 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 45dad3c78f0..f2652ca1a9a 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 @@ -20,7 +20,6 @@ import com.google.firebase.StartupTime; import com.google.firebase.perf.application.AppStateMonitor; import com.google.firebase.perf.config.ConfigResolver; -import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.metrics.AppStartTrace; import com.google.firebase.perf.session.SessionManager; import java.util.concurrent.Executor; From 4e75c31fcb0aca3d3ab01bc3325a10d022cce3f1 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 13:02:50 -0500 Subject: [PATCH 011/146] Change sessions dependency to HEAD --- firebase-perf/firebase-perf.gradle | 2 +- .../perf/session/SessionManagerTest.java | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index b6028e75b61..3dc2fd5a38d 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -118,7 +118,7 @@ dependencies { api("com.google.firebase:firebase-components:18.0.0") api("com.google.firebase:firebase-config:21.5.0") api("com.google.firebase:firebase-installations:17.2.0") - api("com.google.firebase:firebase-sessions:2.0.7") { + api(project(":firebase-sessions")) { 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/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java b/firebase-perf/src/test/java/com/google/firebase/perf/session/SessionManagerTest.java index 264028b8139..c2a4098f972 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 @@ -33,7 +33,6 @@ import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; -import com.google.firebase.perf.v1.ApplicationProcessState; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -90,13 +89,14 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn @Test public void testOnUpdateAppStateDoesNothingDuringAppStart() { String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); + assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); AppStateMonitor.getInstance().setIsColdStart(true); - SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); + // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); } @@ -107,7 +107,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() { assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); + // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId()); } @@ -118,7 +118,7 @@ public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() { assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND); + // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); } @@ -132,7 +132,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId()); - testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId()); } @@ -143,7 +143,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); verify(mockGaugeManager) .logGaugeMetadata( @@ -157,7 +157,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); verify(mockGaugeManager, never()) .logGaugeMetadata( @@ -171,7 +171,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); verify(mockGaugeManager, never()) .logGaugeMetadata( @@ -186,7 +186,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); verify(mockGaugeManager) .logGaugeMetadata( @@ -199,7 +199,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionI SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); verify(mockGaugeManager) .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any()); From 407df2a8620d18983a24edde285590afb8a39645 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 15:19:22 -0500 Subject: [PATCH 012/146] Switch to a singleton Fireperf session subscriber --- .../firebase/perf/FirebasePerformance.java | 3 +-- .../firebase/perf/session/PerfSession.java | 13 +++--------- .../firebase/perf/session/SessionManagerKt.kt | 20 ++++++++++++++++--- .../perf/FirebasePerformanceTestBase.java | 8 +++++--- 4 files changed, 26 insertions(+), 18 deletions(-) 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 cbfa6613d10..ec63b035d3f 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 @@ -202,8 +202,7 @@ public static FirebasePerformance getInstance() { firebaseApp.getOptions().getProjectId(), appContext.getPackageName()))); } - SessionManagerKt sessionSubscriber = new SessionManagerKt(isPerformanceCollectionEnabled()); - FirebaseSessionsDependencies.register(sessionSubscriber); + FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance()); } /** 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 91b63df9041..75114cab15f 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 @@ -20,13 +20,10 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.firebase.perf.config.ConfigResolver; -import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.SessionVerbosity; -import com.google.firebase.sessions.api.SessionSubscriber; import java.util.List; -import java.util.Objects; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -50,7 +47,7 @@ public static PerfSession createNewSession() { // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. // The assumption is that new PerfSessions *should* be limited to either App Start, or through // AQS. - SessionManagerKt.Companion.getPerfSessionToAqs().put(prunedSessionId, null); + SessionManagerKt.Companion.getInstance().reportPerfSession(prunedSessionId); return session; } @@ -71,12 +68,8 @@ private PerfSession(@NonNull Parcel in) { /** Returns the sessionId of the object. */ public String sessionId() { - // TODO(b/394127311): Verify edge cases. - SessionSubscriber.SessionDetails sessionDetails = - SessionManagerKt.Companion.getPerfSessionToAqs().get(internalSessionId); - AndroidLogger.getInstance() - .debug("AQS for " + this.internalSessionId + " is " + sessionDetails); - return Objects.requireNonNull(sessionDetails).getSessionId(); + return SessionManagerKt.Companion.getInstance() + .getAqsMappedToPerfSession(this.internalSessionId); } @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt index cf27cad3175..9913a923168 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt @@ -1,9 +1,13 @@ package com.google.firebase.perf.session +import com.google.firebase.perf.config.ConfigResolver import com.google.firebase.perf.logging.AndroidLogger import com.google.firebase.sessions.api.SessionSubscriber -class SessionManagerKt(val dataCollectionEnabled: Boolean) : SessionSubscriber { +class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubscriber { + private val perfSessionToAqs: MutableMap = + mutableMapOf() + override val isDataCollectionEnabled: Boolean get() = dataCollectionEnabled @@ -25,9 +29,19 @@ class SessionManagerKt(val dataCollectionEnabled: Boolean) : SessionSubscriber { } } + fun reportPerfSession(perfSessionId: String) { + perfSessionToAqs[perfSessionId] = null + } + + fun getAqsMappedToPerfSession(perfSessionId: String): String { + AndroidLogger.getInstance() + .debug("AQS for perf session $perfSessionId is ${perfSessionToAqs[perfSessionId]?.sessionId}") + return perfSessionToAqs[perfSessionId]?.sessionId ?: perfSessionId + } + companion object { - val perfSessionToAqs: MutableMap by lazy { - mutableMapOf() + val instance: SessionManagerKt by lazy { + SessionManagerKt(ConfigResolver.getInstance().isPerformanceMonitoringEnabled) } } } 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 5bd2c2a4c47..344c9c81721 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 @@ -23,9 +23,9 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; 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.SessionManagerKt; import com.google.firebase.perf.util.ImmutableBundle; +import com.google.firebase.sessions.api.SessionSubscriber; import org.junit.After; import org.junit.Before; import org.robolectric.shadows.ShadowPackageManager; @@ -72,6 +72,7 @@ public void setUpFirebaseApp() { .setProjectId(FAKE_FIREBASE_PROJECT_ID) .build(); FirebaseApp.initializeApp(appContext, options); + FirebasePerformance.getInstance(); } @After @@ -98,6 +99,7 @@ private static void forceVerboseSessionWithSamplingPercentage(long samplingPerce bundle.putFloat("sessions_sampling_percentage", samplingPercentage); ConfigResolver.getInstance().setMetadataBundle(new ImmutableBundle(bundle)); - SessionManager.getInstance().setPerfSession(PerfSession.createNewSession()); + SessionManagerKt.Companion.getInstance() + .onSessionChanged(new SessionSubscriber.SessionDetails("sessionId")); } } From eb341c0fadcf7c4bbbb29a96f1cece7379837daf Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 15:43:16 -0500 Subject: [PATCH 013/146] Revert session manager test --- .../perf/session/SessionManagerTest.java | 44 ++++++++++--------- 1 file changed, 23 insertions(+), 21 deletions(-) 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 c2a4098f972..f3e3795f3f8 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 @@ -33,6 +33,7 @@ import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; +import com.google.firebase.perf.v1.ApplicationProcessState; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; @@ -89,14 +90,13 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn @Test public void testOnUpdateAppStateDoesNothingDuringAppStart() { String oldSessionId = SessionManager.getInstance().perfSession().sessionId(); - assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); AppStateMonitor.getInstance().setIsColdStart(true); - // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); + SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); } @@ -107,7 +107,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnForegroundState() { assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); + SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.FOREGROUND); assertThat(oldSessionId).isNotEqualTo(SessionManager.getInstance().perfSession().sessionId()); } @@ -118,7 +118,7 @@ public void testOnUpdateAppStateDoesntGenerateNewSessionIdOnBackgroundState() { assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); - // SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND); + SessionManager.getInstance().onUpdateAppState(ApplicationProcessState.BACKGROUND); assertThat(oldSessionId).isEqualTo(SessionManager.getInstance().perfSession().sessionId()); } @@ -132,7 +132,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess assertThat(oldSessionId).isNotNull(); assertThat(oldSessionId).isEqualTo(testSessionManager.perfSession().sessionId()); - // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); assertThat(oldSessionId).isNotEqualTo(testSessionManager.perfSession().sessionId()); } @@ -143,7 +143,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); verify(mockGaugeManager) .logGaugeMetadata( @@ -157,7 +157,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); verify(mockGaugeManager, never()) .logGaugeMetadata( @@ -171,7 +171,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); verify(mockGaugeManager, never()) .logGaugeMetadata( @@ -186,7 +186,7 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - // testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); + testSessionManager.onUpdateAppState(ApplicationProcessState.BACKGROUND); verify(mockGaugeManager) .logGaugeMetadata( @@ -199,7 +199,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStartCollectingGaugesIfSessionI SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - // testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); + testSessionManager.onUpdateAppState(ApplicationProcessState.FOREGROUND); verify(mockGaugeManager) .startCollectingGauges(AdditionalMatchers.not(eq(mockPerfSession)), any()); @@ -215,7 +215,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIs SessionManager testSessionManager = new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId")); verify(mockGaugeManager).stopCollectingGauges(); } @@ -225,8 +225,9 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession forceSessionsFeatureDisabled(); SessionManager testSessionManager = - new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + new SessionManager( + mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); verify(mockGaugeManager).stopCollectingGauges(); } @@ -238,7 +239,8 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() { // Start with a non verbose session forceNonVerboseSession(); SessionManager testSessionManager = - new SessionManager(mockGaugeManager, PerfSession.createNewSession(), mockAppStateMonitor); + new SessionManager( + mockGaugeManager, PerfSession.createWithId("testSessionId1"), mockAppStateMonitor); verify(mockGaugeManager, times(0)) .logGaugeMetadata( @@ -247,12 +249,12 @@ public void testGaugeMetadataIsFlushedOnlyWhenNewVerboseSessionIsCreated() { // Forcing a verbose session will enable Gauge collection forceVerboseSession(); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + 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.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId3")); verify(mockGaugeManager, times(1)).logGaugeMetadata(eq("testSessionId3"), any()); } @@ -301,7 +303,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); verify(spySessionAwareObjectOne, never()) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -320,8 +322,8 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() { testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne)); testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo)); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); verify(spySessionAwareObjectOne, times(2)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -345,11 +347,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); verify(spySessionAwareObjectOne, times(1)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); From 042ee43b2de39be1f20165b7b2fb0b1c470e6849 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 17:55:10 -0500 Subject: [PATCH 014/146] Update SessionManagerTest --- .../perf/FirebasePerformanceTestBase.java | 13 +- .../perf/session/SessionManagerTest.java | 239 +----------------- 2 files changed, 21 insertions(+), 231 deletions(-) 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 344c9c81721..9a787161017 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 @@ -26,6 +26,7 @@ import com.google.firebase.perf.session.SessionManagerKt; import com.google.firebase.perf.util.ImmutableBundle; import com.google.firebase.sessions.api.SessionSubscriber; +import java.util.UUID; import org.junit.After; import org.junit.Before; import org.robolectric.shadows.ShadowPackageManager; @@ -52,6 +53,8 @@ public class FirebasePerformanceTestBase { protected static final String FAKE_FIREBASE_DB_URL = "https://fir-perftestapp.firebaseio.com"; protected static final String FAKE_FIREBASE_PROJECT_ID = "fir-perftestapp"; + protected static final String FAKE_AQS_SESSION_PREFIX = "AIzaSyBcE"; + protected Context appContext; @Before @@ -94,12 +97,16 @@ protected static void forceNonVerboseSession() { forceVerboseSessionWithSamplingPercentage(0); } + protected static void triggerAqsSession() { + SessionManagerKt.Companion.getInstance() + .onSessionChanged( + new SessionSubscriber.SessionDetails(FAKE_AQS_SESSION_PREFIX + UUID.randomUUID())); + } + private static void forceVerboseSessionWithSamplingPercentage(long samplingPercentage) { Bundle bundle = new Bundle(); bundle.putFloat("sessions_sampling_percentage", samplingPercentage); ConfigResolver.getInstance().setMetadataBundle(new ImmutableBundle(bundle)); - - SessionManagerKt.Companion.getInstance() - .onSessionChanged(new SessionSubscriber.SessionDetails("sessionId")); + triggerAqsSession(); } } 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..bd8e69a74f4 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 @@ -16,10 +16,6 @@ import static com.google.common.truth.Truth.assertThat; 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; import static org.mockito.Mockito.times; @@ -32,15 +28,11 @@ import com.google.firebase.perf.application.AppStateMonitor; import com.google.firebase.perf.session.gauges.GaugeManager; import com.google.firebase.perf.util.Clock; -import com.google.firebase.perf.util.Timer; -import com.google.firebase.perf.v1.ApplicationProcessState; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; 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; @@ -64,13 +56,16 @@ public void setUp() { when(mockPerfSession.sessionId()).thenReturn("sessionId"); when(mockAppStateMonitor.isColdStart()).thenReturn(false); AppStateMonitor.getInstance().setIsColdStart(false); + // We assume that an AQS session has been created in all tests. + triggerAqsSession(); } @Test public void testInstanceCreation() { assertThat(SessionManager.getInstance()).isNotNull(); assertThat(SessionManager.getInstance()).isEqualTo(SessionManager.getInstance()); - assertThat(SessionManager.getInstance().perfSession().sessionId()).isNotNull(); + assertThat(SessionManager.getInstance().perfSession().sessionId()) + .contains(FAKE_AQS_SESSION_PREFIX); } @Test @@ -87,214 +82,6 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn 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 - // NotLogGaugeData on new perf session when not Verbose - // Mark Session as expired after time limit. - - @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() { - forceNonVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId")); - - verify(mockGaugeManager).stopCollectingGauges(); - } - - @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() { - forceSessionsFeatureDisabled(); - - SessionManager testSessionManager = - new SessionManager( - mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); - - 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); - - assertThat(session.isSessionRunningTooLong()).isFalse(); - - when(mockTimer.getDurationMicros()) - .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours - assertThat(session.isSessionRunningTooLong()).isTrue(); - - assertThat(testSessionManager.perfSession().sessionId()).isEqualTo("sessionId"); - } - - @Test - public void testPerfSessionExpiredMakesGaugeManagerStopsCollectingGaugesIfSessionIsVerbose() { - forceVerboseSession(); - Timer mockTimer = mock(Timer.class); - when(mockClock.getTime()).thenReturn(mockTimer); - - PerfSession session = new PerfSession("sessionId", mockClock); - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, session, mockAppStateMonitor); - - assertThat(session.isSessionRunningTooLong()).isFalse(); - - when(mockTimer.getDurationMicros()) - .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours - - assertThat(session.isSessionRunningTooLong()).isTrue(); - verify(mockGaugeManager, times(0)).logGaugeMetadata(any(), any()); - } - @Test public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { SessionManager testSessionManager = @@ -303,7 +90,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, never()) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -313,17 +100,15 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { @Test public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() { - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - + SessionManager testSessionManager = SessionManager.getInstance(); FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectOne)); testSessionManager.registerForSessionUpdates(new WeakReference<>(spySessionAwareObjectTwo)); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + triggerAqsSession(); + triggerAqsSession(); verify(spySessionAwareObjectOne, times(2)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -333,9 +118,7 @@ public void testPerfSession_sessionAwareObjects_NotifiesIfRegistered() { @Test public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - + SessionManager testSessionManager = SessionManager.getInstance(); FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); @@ -347,11 +130,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + triggerAqsSession(); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + triggerAqsSession(); verify(spySessionAwareObjectOne, times(1)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); From bacc81f322f120898174ac6dc0bdcdcea0df5e7a Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 18:07:34 -0500 Subject: [PATCH 015/146] Update SessionManagerTest --- .../com/google/firebase/perf/session/SessionManagerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 bd8e69a74f4..53ad5ea607f 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 @@ -84,8 +84,7 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn @Test public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); + SessionManager testSessionManager = SessionManager.getInstance(); FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); From f2b30fe4db30142dc602a8491a8a47c78da3c11c Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 4 Feb 2025 18:13:27 -0500 Subject: [PATCH 016/146] Remove calling defaultInstance as it's already called in FirebasePerformanceInitializer --- .../com/google/firebase/perf/FirebasePerformanceTestBase.java | 1 - 1 file changed, 1 deletion(-) 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 9a787161017..471b8c59df7 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 @@ -75,7 +75,6 @@ public void setUpFirebaseApp() { .setProjectId(FAKE_FIREBASE_PROJECT_ID) .build(); FirebaseApp.initializeApp(appContext, options); - FirebasePerformance.getInstance(); } @After From db14ff5d6c42c515e7ecc7a62e53efa5a4751e89 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 09:50:13 -0500 Subject: [PATCH 017/146] More test changes --- .../java/com/google/firebase/perf/session/SessionManagerKt.kt | 4 ++++ .../com/google/firebase/perf/FirebasePerformanceTestBase.java | 2 ++ .../com/google/firebase/perf/session/SessionManagerTest.java | 4 ++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt index 9913a923168..855a890a4d6 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt @@ -39,6 +39,10 @@ class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubs return perfSessionToAqs[perfSessionId]?.sessionId ?: perfSessionId } + fun clearSessionForTest() { + perfSessionToAqs.clear() + } + companion object { val instance: SessionManagerKt by lazy { SessionManagerKt(ConfigResolver.getInstance().isPerformanceMonitoringEnabled) 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 471b8c59df7..4a811be9f32 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 @@ -75,11 +75,13 @@ public void setUpFirebaseApp() { .setProjectId(FAKE_FIREBASE_PROJECT_ID) .build(); FirebaseApp.initializeApp(appContext, options); + triggerAqsSession(); } @After public void tearDownFirebaseApp() { FirebaseApp.clearInstancesForTest(); + SessionManagerKt.Companion.getInstance().clearSessionForTest(); } protected static void forceSessionsFeatureDisabled() { 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 53ad5ea607f..59c2ec21d2a 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 @@ -30,6 +30,8 @@ import com.google.firebase.perf.util.Clock; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutionException; + +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -56,8 +58,6 @@ public void setUp() { when(mockPerfSession.sessionId()).thenReturn("sessionId"); when(mockAppStateMonitor.isColdStart()).thenReturn(false); AppStateMonitor.getInstance().setIsColdStart(false); - // We assume that an AQS session has been created in all tests. - triggerAqsSession(); } @Test From 0b1be56a74eaf01dfde4f2f29b603015b6bd5f90 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 09:50:34 -0500 Subject: [PATCH 018/146] style --- .../com/google/firebase/perf/session/SessionManagerTest.java | 2 -- 1 file changed, 2 deletions(-) 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 59c2ec21d2a..4dc97bda641 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 @@ -30,8 +30,6 @@ import com.google.firebase.perf.util.Clock; import java.lang.ref.WeakReference; import java.util.concurrent.ExecutionException; - -import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; From dfbc5215c6cf1e843f51bd0d1978890e48b0666d Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 13:10:49 -0500 Subject: [PATCH 019/146] Change the location of registering subscriber --- .../java/com/google/firebase/perf/FirebasePerformance.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 ec63b035d3f..48e123ff2d1 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 @@ -180,6 +180,9 @@ public static FirebasePerformance getInstance() { return; } + // Prioritize registering the FirebaseSession dependency to have the session + // `setApplicationContext`. + FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance()); TransportManager.getInstance() .initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider); @@ -201,8 +204,6 @@ public static FirebasePerformance getInstance() { ConsoleUrlGenerator.generateDashboardUrl( firebaseApp.getOptions().getProjectId(), appContext.getPackageName()))); } - - FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance()); } /** From 21d47785cd7ddbbbb31c4d1f72a7ac86801d4f9c Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 14:28:17 -0500 Subject: [PATCH 020/146] Update subscriber --- firebase-crashlytics-ndk/src/third_party/crashpad | 2 +- firebase-crashlytics-ndk/src/third_party/lss | 2 +- firebase-crashlytics-ndk/src/third_party/mini_chromium | 2 +- .../com/google/firebase/perf/FirebasePerformance.java | 5 +++-- ...agerKt.kt => FirebasePerformanceSessionSubscriber.kt} | 9 ++++++--- .../com/google/firebase/perf/session/PerfSession.java | 4 ++-- .../firebase/perf/FirebasePerformanceTestBase.java | 6 +++--- 7 files changed, 17 insertions(+), 13 deletions(-) rename firebase-perf/src/main/java/com/google/firebase/perf/session/{SessionManagerKt.kt => FirebasePerformanceSessionSubscriber.kt} (85%) diff --git a/firebase-crashlytics-ndk/src/third_party/crashpad b/firebase-crashlytics-ndk/src/third_party/crashpad index b8937c6cb4b..c902f6b1c9e 160000 --- a/firebase-crashlytics-ndk/src/third_party/crashpad +++ b/firebase-crashlytics-ndk/src/third_party/crashpad @@ -1 +1 @@ -Subproject commit b8937c6cb4b38c1ca06b46791c84b31632895f1f +Subproject commit c902f6b1c9e43224181969110b83e0053b2ddd3c diff --git a/firebase-crashlytics-ndk/src/third_party/lss b/firebase-crashlytics-ndk/src/third_party/lss index ed31caa60f2..9719c1e1e67 160000 --- a/firebase-crashlytics-ndk/src/third_party/lss +++ b/firebase-crashlytics-ndk/src/third_party/lss @@ -1 +1 @@ -Subproject commit ed31caa60f20a4f6569883b2d752ef7522de51e0 +Subproject commit 9719c1e1e676814c456b55f5f070eabad6709d31 diff --git a/firebase-crashlytics-ndk/src/third_party/mini_chromium b/firebase-crashlytics-ndk/src/third_party/mini_chromium index c081fd005b0..4332ddb6963 160000 --- a/firebase-crashlytics-ndk/src/third_party/mini_chromium +++ b/firebase-crashlytics-ndk/src/third_party/mini_chromium @@ -1 +1 @@ -Subproject commit c081fd005b09a59a505b09a4b506f8ba45f70859 +Subproject commit 4332ddb6963750e1106efdcece6d6e2de6dc6430 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 48e123ff2d1..17165e8e341 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 @@ -36,8 +36,8 @@ import com.google.firebase.perf.logging.ConsoleUrlGenerator; import com.google.firebase.perf.metrics.HttpMetric; import com.google.firebase.perf.metrics.Trace; +import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber; import com.google.firebase.perf.session.SessionManager; -import com.google.firebase.perf.session.SessionManagerKt; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Constants; import com.google.firebase.perf.util.ImmutableBundle; @@ -182,7 +182,8 @@ public static FirebasePerformance getInstance() { // Prioritize registering the FirebaseSession dependency to have the session // `setApplicationContext`. - FirebaseSessionsDependencies.register(SessionManagerKt.Companion.getInstance()); + FirebaseSessionsDependencies.register( + FirebasePerformanceSessionSubscriber.Companion.getInstance()); TransportManager.getInstance() .initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider); diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt similarity index 85% rename from firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt rename to firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt index 855a890a4d6..44de7cc1f36 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/session/SessionManagerKt.kt +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt @@ -4,7 +4,8 @@ import com.google.firebase.perf.config.ConfigResolver import com.google.firebase.perf.logging.AndroidLogger import com.google.firebase.sessions.api.SessionSubscriber -class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubscriber { +class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Boolean) : + SessionSubscriber { private val perfSessionToAqs: MutableMap = mutableMapOf() @@ -44,8 +45,10 @@ class SessionManagerKt(private val dataCollectionEnabled: Boolean) : SessionSubs } companion object { - val instance: SessionManagerKt by lazy { - SessionManagerKt(ConfigResolver.getInstance().isPerformanceMonitoringEnabled) + val instance: FirebasePerformanceSessionSubscriber by lazy { + FirebasePerformanceSessionSubscriber( + ConfigResolver.getInstance().isPerformanceMonitoringEnabled + ) } } } 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 75114cab15f..e23c0d5f0b3 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 @@ -47,7 +47,7 @@ public static PerfSession createNewSession() { // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. // The assumption is that new PerfSessions *should* be limited to either App Start, or through // AQS. - SessionManagerKt.Companion.getInstance().reportPerfSession(prunedSessionId); + FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(prunedSessionId); return session; } @@ -68,7 +68,7 @@ private PerfSession(@NonNull Parcel in) { /** Returns the sessionId of the object. */ public String sessionId() { - return SessionManagerKt.Companion.getInstance() + return FirebasePerformanceSessionSubscriber.Companion.getInstance() .getAqsMappedToPerfSession(this.internalSessionId); } 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 4a811be9f32..1a045b3f1b9 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 @@ -23,7 +23,7 @@ import com.google.firebase.FirebaseApp; import com.google.firebase.FirebaseOptions; import com.google.firebase.perf.config.ConfigResolver; -import com.google.firebase.perf.session.SessionManagerKt; +import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber; import com.google.firebase.perf.util.ImmutableBundle; import com.google.firebase.sessions.api.SessionSubscriber; import java.util.UUID; @@ -81,7 +81,7 @@ public void setUpFirebaseApp() { @After public void tearDownFirebaseApp() { FirebaseApp.clearInstancesForTest(); - SessionManagerKt.Companion.getInstance().clearSessionForTest(); + FirebasePerformanceSessionSubscriber.Companion.getInstance().clearSessionForTest(); } protected static void forceSessionsFeatureDisabled() { @@ -99,7 +99,7 @@ protected static void forceNonVerboseSession() { } protected static void triggerAqsSession() { - SessionManagerKt.Companion.getInstance() + FirebasePerformanceSessionSubscriber.Companion.getInstance() .onSessionChanged( new SessionSubscriber.SessionDetails(FAKE_AQS_SESSION_PREFIX + UUID.randomUUID())); } From 9731146e234528656b42a02052dcce1fe647ae8f Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 14:38:28 -0500 Subject: [PATCH 021/146] Revert internal perf session id --- .../FirebasePerformanceSessionSubscriber.kt | 8 ++++---- .../firebase/perf/session/PerfSession.java | 18 ++++++------------ .../firebase/perf/session/SessionManager.java | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) 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 index 44de7cc1f36..66a6323ade1 100644 --- 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 @@ -17,16 +17,16 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails") - val currentInternalSessionId = SessionManager.getInstance().perfSession().internalSessionId + val perfSessionId = SessionManager.getInstance().perfSession().sessionId() // There can be situations where a new [PerfSession] was created, but an AQS wasn't // available (during cold start). - if (perfSessionToAqs[currentInternalSessionId] == null) { - perfSessionToAqs[currentInternalSessionId] = sessionDetails + if (perfSessionToAqs[perfSessionId] == null) { + perfSessionToAqs[perfSessionId] = sessionDetails } else { val newSession = PerfSession.createNewSession() SessionManager.getInstance().updatePerfSession(newSession) - perfSessionToAqs[newSession.internalSessionId] = sessionDetails + perfSessionToAqs[newSession.sessionId()] = sessionDetails } } 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 e23c0d5f0b3..f18e97e094a 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 @@ -30,7 +30,7 @@ /** Details of a session including a unique Id and related information. */ public class PerfSession implements Parcelable { - private final String internalSessionId; + private final String sessionId; private final Timer creationTime; private boolean isGaugeAndEventCollectionEnabled = false; @@ -54,27 +54,21 @@ public static PerfSession createNewSession() { /** Creates a PerfSession with the provided {@code sessionId} and {@code clock}. */ @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public PerfSession(String internalSessionId, Clock clock) { - this.internalSessionId = internalSessionId; + public PerfSession(String sessionId, Clock clock) { + this.sessionId = sessionId; creationTime = clock.getTime(); } private PerfSession(@NonNull Parcel in) { super(); - internalSessionId = in.readString(); + sessionId = in.readString(); isGaugeAndEventCollectionEnabled = in.readByte() != 0; creationTime = in.readParcelable(Timer.class.getClassLoader()); } /** Returns the sessionId of the object. */ public String sessionId() { - return FirebasePerformanceSessionSubscriber.Companion.getInstance() - .getAqsMappedToPerfSession(this.internalSessionId); - } - - @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) - public String getInternalSessionId() { - return internalSessionId; + return this.sessionId; } /** @@ -202,7 +196,7 @@ public int describeContents() { * @param flags Additional flags about how the object should be written. */ public void writeToParcel(@NonNull Parcel out, int flags) { - out.writeString(internalSessionId); + out.writeString(sessionId); out.writeByte((byte) (isGaugeAndEventCollectionEnabled ? 1 : 0)); out.writeParcelable(creationTime, 0); } 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 54784d7a274..c077b98efb1 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 @@ -114,7 +114,7 @@ public void stopGaugeCollectionIfSessionRunningTooLong() { public void updatePerfSession(PerfSession perfSession) { // Do not update the perf session if it is the exact same sessionId. if (Objects.equals( - perfSession.getInternalSessionId(), this.perfSession.getInternalSessionId())) { + perfSession.sessionId(), this.perfSession.sessionId())) { return; } From b942120f6e136cfe0826f26390cd4abce3a68a9b Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 14:53:36 -0500 Subject: [PATCH 022/146] Attempt to simplify AQS session usage --- .../firebase/perf/session/PerfSession.java | 22 +++++++++++-------- .../firebase/perf/session/SessionManager.java | 3 +-- .../perf/session/gauges/GaugeManager.java | 11 ++++++++-- 3 files changed, 23 insertions(+), 13 deletions(-) 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 f18e97e094a..ac0cd6f9d71 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 @@ -30,6 +30,7 @@ /** Details of a session including a unique Id and related information. */ public class PerfSession implements Parcelable { + private static final String SESSION_ID_PREFIX = "fireperf-session"; private final String sessionId; private final Timer creationTime; @@ -39,16 +40,9 @@ public class PerfSession implements Parcelable { * Creates a PerfSession object and decides what metrics to collect. */ public static PerfSession createNewSession() { - String prunedSessionId = UUID.randomUUID().toString().replace("-", ""); + String prunedSessionId = SESSION_ID_PREFIX + UUID.randomUUID().toString().replace("-", ""); PerfSession session = new PerfSession(prunedSessionId, new Clock()); session.setGaugeAndEventCollectionEnabled(shouldCollectGaugesAndEvents()); - - // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received, - // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. - // The assumption is that new PerfSessions *should* be limited to either App Start, or through - // AQS. - FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(prunedSessionId); - return session; } @@ -57,6 +51,11 @@ public static PerfSession createNewSession() { public PerfSession(String sessionId, Clock clock) { this.sessionId = sessionId; creationTime = clock.getTime(); + // Every time a PerfSession is created, it sets the AQS to null. Once an AQS is received, + // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. + // The assumption is that new PerfSessions *should* be limited to either App Start, or through + // AQS. + FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(sessionId); } private PerfSession(@NonNull Parcel in) { @@ -71,6 +70,11 @@ public String sessionId() { return this.sessionId; } + private String aqsSessionId() { + return FirebasePerformanceSessionSubscriber.Companion.getInstance() + .getAqsMappedToPerfSession(this.sessionId); + } + /** * Returns a timer object that has been seeded with the system time at which the session began. */ @@ -121,7 +125,7 @@ public boolean isSessionRunningTooLong() { /** Creates and returns the proto object for PerfSession object. */ public com.google.firebase.perf.v1.PerfSession build() { com.google.firebase.perf.v1.PerfSession.Builder sessionMetric = - com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId()); + com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(aqsSessionId()); // If gauge collection is enabled, enable gauge collection verbosity. if (isGaugeAndEventCollectionEnabled) { 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 c077b98efb1..a6e95878a31 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 @@ -113,8 +113,7 @@ public void stopGaugeCollectionIfSessionRunningTooLong() { */ public void updatePerfSession(PerfSession perfSession) { // Do not update the perf session if it is the exact same sessionId. - if (Objects.equals( - perfSession.sessionId(), this.perfSession.sessionId())) { + if (Objects.equals(perfSession.sessionId(), this.perfSession.sessionId())) { return; } 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..46d67540f0b 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 @@ -22,6 +22,7 @@ import com.google.firebase.components.Lazy; import com.google.firebase.perf.config.ConfigResolver; import com.google.firebase.perf.logging.AndroidLogger; +import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber; import com.google.firebase.perf.session.PerfSession; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Timer; @@ -242,7 +243,10 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { } // Adding Session ID info. - gaugeMetricBuilder.setSessionId(sessionId); + String aqsSessionId = + FirebasePerformanceSessionSubscriber.Companion.getInstance() + .getAqsMappedToPerfSession(sessionId); + gaugeMetricBuilder.setSessionId(aqsSessionId); transportManager.log(gaugeMetricBuilder.build(), appState); } @@ -256,10 +260,13 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { * @return true if GaugeMetadata was logged, false otherwise. */ public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { + String aqsSessionId = + FirebasePerformanceSessionSubscriber.Companion.getInstance() + .getAqsMappedToPerfSession(sessionId); if (gaugeMetadataManager != null) { GaugeMetric gaugeMetric = GaugeMetric.newBuilder() - .setSessionId(sessionId) + .setSessionId(aqsSessionId) .setGaugeMetadata(getGaugeMetadata()) .build(); transportManager.log(gaugeMetric, appState); From 6899fde4ba688ee100546985248ae1cac4672303 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 15:00:38 -0500 Subject: [PATCH 023/146] Update tests --- .../com/google/firebase/perf/FirebasePerformanceTestBase.java | 2 +- .../com/google/firebase/perf/session/SessionManagerTest.java | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) 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 1a045b3f1b9..f18bce3855b 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 @@ -53,7 +53,7 @@ public class FirebasePerformanceTestBase { protected static final String FAKE_FIREBASE_DB_URL = "https://fir-perftestapp.firebaseio.com"; protected static final String FAKE_FIREBASE_PROJECT_ID = "fir-perftestapp"; - protected static final String FAKE_AQS_SESSION_PREFIX = "AIzaSyBcE"; + protected static final String FAKE_AQS_SESSION_PREFIX = "AQS"; protected Context appContext; 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 4dc97bda641..63edf3167e4 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 @@ -62,8 +62,7 @@ public void setUp() { public void testInstanceCreation() { assertThat(SessionManager.getInstance()).isNotNull(); assertThat(SessionManager.getInstance()).isEqualTo(SessionManager.getInstance()); - assertThat(SessionManager.getInstance().perfSession().sessionId()) - .contains(FAKE_AQS_SESSION_PREFIX); + assertThat(SessionManager.getInstance().perfSession().sessionId()).isNotNull(); } @Test From 5676892c077402104d021c2f89af24d3baa6a30c Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 15:01:20 -0500 Subject: [PATCH 024/146] Update prefix --- .../main/java/com/google/firebase/perf/session/PerfSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ac0cd6f9d71..7d49374bd1c 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 @@ -30,7 +30,7 @@ /** Details of a session including a unique Id and related information. */ public class PerfSession implements Parcelable { - private static final String SESSION_ID_PREFIX = "fireperf-session"; + private static final String SESSION_ID_PREFIX = "FPRS"; private final String sessionId; private final Timer creationTime; From e4b7114d497697ac4db71ef0091d51e34bf3729d Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Wed, 5 Feb 2025 15:01:35 -0500 Subject: [PATCH 025/146] Update prefix --- .../main/java/com/google/firebase/perf/session/PerfSession.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 7d49374bd1c..2d742f3d825 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 @@ -30,7 +30,7 @@ /** Details of a session including a unique Id and related information. */ public class PerfSession implements Parcelable { - private static final String SESSION_ID_PREFIX = "FPRS"; + private static final String SESSION_ID_PREFIX = "FPR"; private final String sessionId; private final Timer creationTime; From 79aac4c91cd119f84283d92f6fec42de932af096 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 12:45:09 -0500 Subject: [PATCH 026/146] Improve logging and adda TODO --- .../perf/session/FirebasePerformanceSessionSubscriber.kt | 3 ++- .../com/google/firebase/perf/session/PerfSession.java | 8 +++++--- .../com/google/firebase/perf/session/SessionManager.java | 3 +-- .../google/firebase/perf/session/gauges/GaugeManager.java | 5 +++++ 4 files changed, 13 insertions(+), 6 deletions(-) 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 index 66a6323ade1..3c8933bc02a 100644 --- 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 @@ -16,8 +16,9 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo get() = SessionSubscriber.Name.PERFORMANCE override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { - AndroidLogger.getInstance().debug("AQS Session Changed: $sessionDetails") val perfSessionId = SessionManager.getInstance().perfSession().sessionId() + AndroidLogger.getInstance() + .debug("CFPRS AQS Session Changed: $sessionDetails, PerfSession: $perfSessionId") // There can be situations where a new [PerfSession] was created, but an AQS wasn't // available (during cold start). 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 2d742f3d825..6a485e11458 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 @@ -20,6 +20,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.google.firebase.perf.config.ConfigResolver; +import com.google.firebase.perf.logging.AndroidLogger; import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.SessionVerbosity; @@ -55,6 +56,7 @@ public PerfSession(String sessionId, Clock clock) { // SessionManagerKt verifies if this is an active session, and sets the AQS session ID. // The assumption is that new PerfSessions *should* be limited to either App Start, or through // AQS. + AndroidLogger.getInstance().debug("CFPRS PerfSession(): " + sessionId); FirebasePerformanceSessionSubscriber.Companion.getInstance().reportPerfSession(sessionId); } @@ -176,9 +178,9 @@ public static com.google.firebase.perf.v1.PerfSession[] buildAndSort( /** If true, Session Gauge collection is enabled. */ public static boolean shouldCollectGaugesAndEvents() { ConfigResolver configResolver = ConfigResolver.getInstance(); - - return configResolver.isPerformanceMonitoringEnabled() - && Math.random() < configResolver.getSessionsSamplingRate(); + return true; + // return configResolver.isPerformanceMonitoringEnabled() + // && Math.random() < configResolver.getSessionsSamplingRate(); } /** 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 a6e95878a31..afc88b15e49 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 @@ -68,6 +68,7 @@ public SessionManager( this.gaugeManager = gaugeManager; this.perfSession = perfSession; this.appStateMonitor = appStateMonitor; + AndroidLogger.getInstance().debug("CFPRS: SessionManager()"); } /** @@ -117,8 +118,6 @@ public void updatePerfSession(PerfSession perfSession) { return; } - AndroidLogger.getInstance().debug("Perf Session Changed: " + perfSession); - this.perfSession = perfSession; synchronized (clients) { 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 46d67540f0b..fc115079d89 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 @@ -247,6 +247,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { FirebasePerformanceSessionSubscriber.Companion.getInstance() .getAqsMappedToPerfSession(sessionId); gaugeMetricBuilder.setSessionId(aqsSessionId); + AndroidLogger.getInstance().debug("CFPR syncFlush: " + sessionId + " AQS: " + aqsSessionId); transportManager.log(gaugeMetricBuilder.build(), appState); } @@ -260,9 +261,13 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { * @return true if GaugeMetadata was logged, false otherwise. */ public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { + // TODO(b/394127311): Based on logs, AQS session ID isn't available any time + // this is called. Adding a TODO to identify potential changes. String aqsSessionId = FirebasePerformanceSessionSubscriber.Companion.getInstance() .getAqsMappedToPerfSession(sessionId); + AndroidLogger.getInstance() + .debug("CFPR logGaugeMetadata: " + sessionId + " AQS: " + aqsSessionId); if (gaugeMetadataManager != null) { GaugeMetric gaugeMetric = GaugeMetric.newBuilder() From 5093a85c0d87aacdc2ec7b10ddc313743729bb94 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 12:59:23 -0500 Subject: [PATCH 027/146] Additional TODO --- .../com/google/firebase/perf/session/gauges/GaugeManager.java | 1 + 1 file changed, 1 insertion(+) 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 fc115079d89..acabf5ee827 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 @@ -274,6 +274,7 @@ public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appSta .setSessionId(aqsSessionId) .setGaugeMetadata(getGaugeMetadata()) .build(); + // TODO(b/394127311): Explore maintaining this metadata until AQS is available. transportManager.log(gaugeMetric, appState); return true; } From 8a8e8edec1d47099a7cc1236ab9d150ace012fd1 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 15:26:30 -0500 Subject: [PATCH 028/146] Remove the logging of GaugeMetadata based on AppStart --- .../firebase/perf/FirebasePerformance.java | 1 - .../firebase/perf/session/SessionManager.java | 17 ----------------- .../perf/session/gauges/CpuGaugeCollector.java | 13 +++---------- .../perf/session/gauges/GaugeManager.java | 8 ++++---- .../session/gauges/GaugeMetadataManager.java | 10 +--------- .../session/gauges/MemoryGaugeCollector.java | 6 +++--- 6 files changed, 11 insertions(+), 44 deletions(-) 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..3cc49896ce0 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 @@ -182,7 +182,6 @@ public static FirebasePerformance getInstance() { .initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider); Context appContext = firebaseApp.getApplicationContext(); - // TODO(b/110178816): Explore moving off of main thread. mMetadataBundle = extractMetadata(appContext); remoteConfigManager.setFirebaseRemoteConfigProvider(firebaseRemoteConfigProvider); 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..29ffb988ba0 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 @@ -79,9 +79,6 @@ 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(); @@ -89,10 +86,6 @@ public void setApplicationContext(final Context appContext) { executorService.submit( () -> { gaugeManager.initializeGaugeMetadataManager(appContext); - if (appStartSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.logGaugeMetadata( - appStartSession.sessionId(), ApplicationProcessState.FOREGROUND); - } }); } @@ -164,9 +157,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()); } @@ -178,7 +168,6 @@ public void updatePerfSession(PerfSession perfSession) { * this does not reset the perfSession. */ public void initializeGaugeCollection() { - logGaugeMetadataIfCollectionEnabled(ApplicationProcessState.FOREGROUND); startOrStopCollectingGauges(ApplicationProcessState.FOREGROUND); } @@ -206,12 +195,6 @@ public void unregisterForSessionUpdates(WeakReference client } } - private void logGaugeMetadataIfCollectionEnabled(ApplicationProcessState appState) { - if (perfSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.logGaugeMetadata(perfSession.sessionId(), appState); - } - } - private void startOrStopCollectingGauges(ApplicationProcessState appState) { if (perfSession.isGaugeAndEventCollectionEnabled()) { gaugeManager.startCollectingGauges(perfSession, appState); 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..ceb636d56b3 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; @@ -163,7 +161,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate( this.cpuMetricCollectionRateMs = cpuMetricCollectionRate; try { cpuMetricCollectorJob = - cpuMetricCollectorExecutor.scheduleAtFixedRate( + cpuMetricCollectorExecutor.scheduleWithFixedDelay( () -> { CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime); if (currCpuReading != null) { @@ -181,7 +179,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate( private synchronized void scheduleCpuMetricCollectionOnce(Timer referenceTime) { try { @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + ScheduledFuture unusedFuture = cpuMetricCollectorExecutor.schedule( () -> { CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime); @@ -227,12 +225,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/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java index 7f6182a9c15..f8b623d5f89 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 @@ -72,8 +72,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 +81,7 @@ private GaugeManager() { Lazy gaugeManagerExecutor, TransportManager transportManager, ConfigResolver configResolver, - GaugeMetadataManager gaugeMetadataManager, + @Nullable GaugeMetadataManager gaugeMetadataManager, Lazy cpuGaugeCollector, Lazy memoryGaugeCollector) { @@ -140,7 +140,7 @@ public void startCollectingGauges( gaugeManagerDataCollectionJob = gaugeManagerExecutor .get() - .scheduleAtFixedRate( + .scheduleWithFixedDelay( () -> { syncFlush(sessionIdForScheduledTask, applicationProcessStateForScheduledTask); }, 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..d7ad10590b6 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,8 +17,6 @@ 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; @@ -41,7 +39,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 +47,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,11 +71,7 @@ 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"); + return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem)); } /** Returns the total ram size of the device (in kilobytes) by reading the "proc/meminfo" file. */ 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..a7b4b40002a 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,7 +124,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate( try { memoryMetricCollectorJob = - memoryMetricCollectorExecutor.scheduleAtFixedRate( + memoryMetricCollectorExecutor.scheduleWithFixedDelay( () -> { AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime); if (memoryReading != null) { @@ -142,7 +142,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate( private synchronized void scheduleMemoryMetricCollectionOnce(Timer referenceTime) { try { @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + ScheduledFuture unusedFuture = memoryMetricCollectorExecutor.schedule( () -> { AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime); From c606723099fb0af281152f038c555bc60b33295c Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 16:00:10 -0500 Subject: [PATCH 029/146] Remove file based memory reading test --- .../session/gauges/GaugeMetadataManager.java | 24 ------- .../gauges/GaugeMetadataManagerTest.java | 66 +------------------ 2 files changed, 1 insertion(+), 89 deletions(-) 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 d7ad10590b6..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 @@ -22,11 +22,6 @@ 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} @@ -73,23 +68,4 @@ public int getMaxEncouragedAppJavaHeapMemoryKb() { public int getDeviceRamSizeKb() { return Utils.saturatedIntCast(StorageUnit.BYTES.toKilobytes(memoryInfo.totalMem)); } - - /** 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; - } } 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"; } From 148b068e0844796fd8be6c4c2b0050bb50030b19 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 16:03:58 -0500 Subject: [PATCH 030/146] Add TODO to re-introduce metadata logging --- .../com/google/firebase/perf/session/gauges/GaugeManager.java | 1 + 1 file changed, 1 insertion(+) 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 f8b623d5f89..30da2f0160f 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 @@ -256,6 +256,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { * @return true if GaugeMetadata was logged, false otherwise. */ public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { + // TODO(b/394127311): Re-introduce logging of metadata for AQS. if (gaugeMetadataManager != null) { GaugeMetric gaugeMetric = GaugeMetric.newBuilder() From 4c637aa9e1a2717b9aa86e3da1a2a9a1bb995601 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 16:36:00 -0500 Subject: [PATCH 031/146] Re-introduce gauge metadata collection --- .../FirebasePerformanceSessionSubscriber.kt | 4 +++ .../firebase/perf/session/SessionManager.java | 11 +----- .../perf/session/gauges/GaugeManager.java | 34 ++++++++----------- .../perf/session/SessionManagerTest.java | 4 +-- .../perf/session/gauges/GaugeManagerTest.java | 31 +---------------- 5 files changed, 22 insertions(+), 62 deletions(-) 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 index 3c8933bc02a..58a34bb5e98 100644 --- 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 @@ -2,6 +2,7 @@ package com.google.firebase.perf.session import com.google.firebase.perf.config.ConfigResolver import com.google.firebase.perf.logging.AndroidLogger +import com.google.firebase.perf.session.gauges.GaugeManager import com.google.firebase.sessions.api.SessionSubscriber class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Boolean) : @@ -29,6 +30,9 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo SessionManager.getInstance().updatePerfSession(newSession) perfSessionToAqs[newSession.sessionId()] = sessionDetails } + + // Always log GaugeMetadata when a session changes. + GaugeManager.getInstance().logGaugeMetadata(sessionDetails.sessionId) } fun reportPerfSession(perfSessionId: String) { 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 b46dff750ae..0d5493b9599 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 @@ -29,8 +29,6 @@ import java.util.Iterator; import java.util.Objects; import java.util.Set; -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. */ @@ -76,14 +74,7 @@ public SessionManager( * (currently that is before onResume finishes) to ensure gauge collection starts on time. */ public void setApplicationContext(final Context appContext) { - // TODO(b/258263016): Migrate to go/firebase-android-executors - @SuppressLint("ThreadPoolCreation") - ExecutorService executorService = Executors.newSingleThreadExecutor(); - syncInitFuture = - executorService.submit( - () -> { - gaugeManager.initializeGaugeMetadataManager(appContext); - }); + gaugeManager.initializeGaugeMetadataManager(appContext, ApplicationProcessState.FOREGROUND); } /** 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 c4b828970d9..65e55e49c11 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 @@ -95,8 +95,10 @@ private GaugeManager() { } /** Initializes GaugeMetadataManager which requires application context. */ - public void initializeGaugeMetadataManager(Context appContext) { + public void initializeGaugeMetadataManager( + Context appContext, ApplicationProcessState applicationProcessState) { this.gaugeMetadataManager = new GaugeMetadataManager(appContext); + this.applicationProcessState = applicationProcessState; } /** Returns the singleton instance of this class. */ @@ -255,28 +257,20 @@ 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 + * @param aqsSessionId The {@link FirebasePerformanceSessionSubscriber#getAqsMappedToPerfSession(String)} to which the collected Gauge Metrics * should be associated with. - * @param appState The {@link ApplicationProcessState} for which these gauges are collected. * @return true if GaugeMetadata was logged, false otherwise. */ - public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { - // TODO(b/394127311): Re-introduce logging of metadata for AQS. - String aqsSessionId = - FirebasePerformanceSessionSubscriber.Companion.getInstance() - .getAqsMappedToPerfSession(sessionId); - AndroidLogger.getInstance() - .debug("CFPR logGaugeMetadata: " + sessionId + " AQS: " + aqsSessionId); - if (gaugeMetadataManager != null) { - GaugeMetric gaugeMetric = - GaugeMetric.newBuilder() - .setSessionId(aqsSessionId) - .setGaugeMetadata(getGaugeMetadata()) - .build(); - transportManager.log(gaugeMetric, appState); - return true; - } - return false; + public void logGaugeMetadata(String aqsSessionId) { + // TODO(b/394127311): This can now throw an NPE. Explore if there's anything that should be + // verified. + AndroidLogger.getInstance().debug("CFPR logGaugeMetadata: " + aqsSessionId); + GaugeMetric gaugeMetric = + GaugeMetric.newBuilder() + .setSessionId(aqsSessionId) + .setGaugeMetadata(getGaugeMetadata()) + .build(); + transportManager.log(gaugeMetric, this.applicationProcessState); } private GaugeMetadata getGaugeMetadata() { 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 63edf3167e4..13173838905 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 @@ -75,8 +75,8 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn testSessionManager.setApplicationContext(mockApplicationContext); testSessionManager.getSyncInitFuture().get(); - inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any()); - inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any()); + inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any(), any()); + inOrder.verify(mockGaugeManager).logGaugeMetadata(any()); } @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..696b90ed466 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 @@ -641,7 +641,7 @@ public void testLogGaugeMetadataSendDataToTransport() { when(fakeGaugeMetadataManager.getMaxAppJavaHeapMemoryKb()).thenReturn(1000); when(fakeGaugeMetadataManager.getMaxEncouragedAppJavaHeapMemoryKb()).thenReturn(800); - testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND); + testGaugeManager.logGaugeMetadata("sessionId"); GaugeMetric recordedGaugeMetric = getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1); @@ -668,35 +668,6 @@ public void testLogGaugeMetadataDoesntLogWhenGaugeMetadataManagerNotAvailable() /* gaugeMetadataManager= */ null, new Lazy<>(() -> fakeCpuGaugeCollector), new Lazy<>(() -> fakeMemoryGaugeCollector)); - - assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND)) - .isFalse(); - } - - @Test - public void testLogGaugeMetadataLogsAfterApplicationContextIsSet() { - - testGaugeManager = - new GaugeManager( - new Lazy<>(() -> fakeScheduledExecutorService), - mockTransportManager, - mockConfigResolver, - /* gaugeMetadataManager= */ null, - new Lazy<>(() -> fakeCpuGaugeCollector), - new Lazy<>(() -> fakeMemoryGaugeCollector)); - - assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND)) - .isFalse(); - - testGaugeManager.initializeGaugeMetadataManager(ApplicationProvider.getApplicationContext()); - assertThat(testGaugeManager.logGaugeMetadata("sessionId", ApplicationProcessState.FOREGROUND)) - .isTrue(); - - GaugeMetric recordedGaugeMetric = - getLastRecordedGaugeMetric(ApplicationProcessState.FOREGROUND, 1); - GaugeMetadata recordedGaugeMetadata = recordedGaugeMetric.getGaugeMetadata(); - - assertThat(recordedGaugeMetric.getSessionId()).isEqualTo("sessionId"); } @Test From 705ceea73a4000713614f337911e45a5abb0d81e Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 16:53:13 -0500 Subject: [PATCH 032/146] Delete gaugeMetadata tests that correctly fail --- .../perf/session/SessionManagerTest.java | 55 ------------------- 1 file changed, 55 deletions(-) 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..d105594f4ce 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 @@ -136,20 +136,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess 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() { @@ -178,21 +164,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess 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(); @@ -232,32 +203,6 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession 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); From c2e355e50fbb0b39d90eeb8e345686f0e718792d Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 17:04:37 -0500 Subject: [PATCH 033/146] Fix unit test --- .../com/google/firebase/perf/session/SessionManagerTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 d105594f4ce..37b9ff7215b 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 @@ -74,7 +74,7 @@ public void testInstanceCreation() { } @Test - public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsInitialized() + public void setApplicationContext_initializeGaugeMetadataManager() throws ExecutionException, InterruptedException { when(mockPerfSession.isGaugeAndEventCollectionEnabled()).thenReturn(true); InOrder inOrder = Mockito.inOrder(mockGaugeManager); @@ -84,7 +84,6 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn testSessionManager.getSyncInitFuture().get(); inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any()); - inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any()); } @Test From d803be6b2a0d78caa42496ee4cf37a9bbedafc36 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Thu, 6 Feb 2025 17:15:36 -0500 Subject: [PATCH 034/146] Remove LinkedHashMap TODO. --- .../perf/session/SessionManagerTest.java | 130 +----------------- 1 file changed, 7 insertions(+), 123 deletions(-) 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 37b9ff7215b..16a2cf307c7 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 @@ -83,123 +83,7 @@ public void setApplicationContext_initializeGaugeMetadataManager() testSessionManager.setApplicationContext(mockApplicationContext); testSessionManager.getSyncInitFuture().get(); - inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(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 - 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 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 - // NotLogGaugeData on new perf session when not Verbose - // Mark Session as expired after time limit. - - @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() { - forceNonVerboseSession(); - - SessionManager testSessionManager = - new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId")); - - verify(mockGaugeManager).stopCollectingGauges(); - } - - @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() { - forceSessionsFeatureDisabled(); - - SessionManager testSessionManager = - new SessionManager( - mockGaugeManager, PerfSession.createWithId("testSessionId"), mockAppStateMonitor); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); - - verify(mockGaugeManager).stopCollectingGauges(); + inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any(), ApplicationProcessState.FOREGROUND); } @Test @@ -236,7 +120,7 @@ public void testPerfSessionExpiredMakesGaugeManagerStopsCollectingGaugesIfSessio .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours assertThat(session.isSessionRunningTooLong()).isTrue(); - verify(mockGaugeManager, times(0)).logGaugeMetadata(any(), any()); + verify(mockGaugeManager, times(0)).logGaugeMetadata(any()); } @Test @@ -247,7 +131,7 @@ public void testPerfSession_sessionAwareObjects_doesntNotifyIfNotRegistered() { FakeSessionAwareObject spySessionAwareObjectOne = spy(new FakeSessionAwareObject()); FakeSessionAwareObject spySessionAwareObjectTwo = spy(new FakeSessionAwareObject()); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, never()) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -266,8 +150,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(PerfSession.createNewSession()); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, times(2)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); @@ -291,11 +175,11 @@ public void testPerfSession_sessionAwareObjects_DoesNotNotifyIfUnregistered() { testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.registerForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId1")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectOne); testSessionManager.unregisterForSessionUpdates(weakSpySessionAwareObjectTwo); - testSessionManager.updatePerfSession(PerfSession.createWithId("testSessionId2")); + testSessionManager.updatePerfSession(PerfSession.createNewSession()); verify(spySessionAwareObjectOne, times(1)) .updateSession(ArgumentMatchers.nullable(PerfSession.class)); From 4dd97ac472e3aeaadff5f3c18455d3e5d06eb398 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Fri, 7 Feb 2025 11:21:35 -0500 Subject: [PATCH 035/146] Additional changes --- .../google/firebase/perf/config/ConfigResolver.java | 6 +++--- .../session/FirebasePerformanceSessionSubscriber.kt | 11 ++++------- .../firebase/perf/session/SessionManagerTest.java | 8 +++----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java b/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java index 1ee9d395e03..7e321515141 100644 --- a/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java +++ b/firebase-perf/src/main/java/com/google/firebase/perf/config/ConfigResolver.java @@ -116,7 +116,7 @@ public void setMetadataBundle(ImmutableBundle bundle) { /** Default API to call for whether performance monitoring is currently silent. */ public boolean isPerformanceMonitoringEnabled() { Boolean isPerformanceCollectionEnabled = getIsPerformanceCollectionEnabled(); - return (isPerformanceCollectionEnabled == null || isPerformanceCollectionEnabled == true) + return (isPerformanceCollectionEnabled == null || isPerformanceCollectionEnabled) && getIsServiceCollectionEnabled(); } @@ -131,7 +131,7 @@ public Boolean getIsPerformanceCollectionEnabled() { // return developer config. // 4. Else, return null. Because Firebase Performance will read highlevel Firebase flag in this // case. - if (getIsPerformanceCollectionDeactivated()) { + if (Boolean.TRUE.equals(getIsPerformanceCollectionDeactivated())) { // 1. If developer has deactivated Firebase Performance in Manifest, return false. return false; } @@ -186,7 +186,7 @@ public void setIsPerformanceCollectionEnabled(Boolean isEnabled) { // 2. Otherwise, save this configuration in device cache. // If collection is deactivated, skip the action to save user configuration. - if (getIsPerformanceCollectionDeactivated()) { + if (Boolean.TRUE.equals(getIsPerformanceCollectionDeactivated())) { return; } 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 index 58a34bb5e98..f9f6e257c73 100644 --- 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 @@ -1,17 +1,16 @@ package com.google.firebase.perf.session -import com.google.firebase.perf.config.ConfigResolver import com.google.firebase.perf.logging.AndroidLogger import com.google.firebase.perf.session.gauges.GaugeManager import com.google.firebase.sessions.api.SessionSubscriber -class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Boolean) : - SessionSubscriber { +class FirebasePerformanceSessionSubscriber() : SessionSubscriber { private val perfSessionToAqs: MutableMap = mutableMapOf() + // TODO(b/394127311): Identify a way to ste this value after ConfigResolver has relevant metadata. override val isDataCollectionEnabled: Boolean - get() = dataCollectionEnabled + get() = true override val sessionSubscriberName: SessionSubscriber.Name get() = SessionSubscriber.Name.PERFORMANCE @@ -51,9 +50,7 @@ class FirebasePerformanceSessionSubscriber(private val dataCollectionEnabled: Bo companion object { val instance: FirebasePerformanceSessionSubscriber by lazy { - FirebasePerformanceSessionSubscriber( - ConfigResolver.getInstance().isPerformanceMonitoringEnabled - ) + FirebasePerformanceSessionSubscriber() } } } 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 16a2cf307c7..645f5482777 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 @@ -16,9 +16,6 @@ import static com.google.common.truth.Truth.assertThat; 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 +37,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; @@ -83,7 +79,9 @@ public void setApplicationContext_initializeGaugeMetadataManager() testSessionManager.setApplicationContext(mockApplicationContext); testSessionManager.getSyncInitFuture().get(); - inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any(), ApplicationProcessState.FOREGROUND); + inOrder + .verify(mockGaugeManager) + .initializeGaugeMetadataManager(any(), ApplicationProcessState.FOREGROUND); } @Test From 49f00082c40c115178ead5fc47c412011c9ca6e0 Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Fri, 7 Feb 2025 14:58:37 -0500 Subject: [PATCH 036/146] Remove the logging of GaugeMetadata to allow using AQS (#6678) Based on the behaviour of AQS w/ Fireperf, an AQS session isn't available when (currently) logging gauge metadata. Changes: - Remove the current logging of gauge metadata - will be re-introduced in a future PR. - Switch Gauge collection from `scheduleAtFixedRate` to `scheduleWithFixedDelay`. As [documented](https://stackoverflow.com/a/78405653), this *should* prevent a potentially large amounts of gauge collection if a process is cached, and then restored during a verbose session - which *should* make it work better w/ AQS. - Remove API restricted behaviour which is no longer relevant. --- .../firebase/perf/FirebasePerformance.java | 1 - .../firebase/perf/session/SessionManager.java | 17 ----- .../session/gauges/CpuGaugeCollector.java | 13 +--- .../perf/session/gauges/GaugeManager.java | 9 +-- .../session/gauges/GaugeMetadataManager.java | 34 +--------- .../session/gauges/MemoryGaugeCollector.java | 6 +- .../perf/session/SessionManagerTest.java | 58 +--------------- .../gauges/GaugeMetadataManagerTest.java | 66 +------------------ 8 files changed, 14 insertions(+), 190 deletions(-) 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..3cc49896ce0 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 @@ -182,7 +182,6 @@ public static FirebasePerformance getInstance() { .initialize(firebaseApp, firebaseInstallationsApi, transportFactoryProvider); Context appContext = firebaseApp.getApplicationContext(); - // TODO(b/110178816): Explore moving off of main thread. mMetadataBundle = extractMetadata(appContext); remoteConfigManager.setFirebaseRemoteConfigProvider(firebaseRemoteConfigProvider); 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..29ffb988ba0 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 @@ -79,9 +79,6 @@ 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(); @@ -89,10 +86,6 @@ public void setApplicationContext(final Context appContext) { executorService.submit( () -> { gaugeManager.initializeGaugeMetadataManager(appContext); - if (appStartSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.logGaugeMetadata( - appStartSession.sessionId(), ApplicationProcessState.FOREGROUND); - } }); } @@ -164,9 +157,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()); } @@ -178,7 +168,6 @@ public void updatePerfSession(PerfSession perfSession) { * this does not reset the perfSession. */ public void initializeGaugeCollection() { - logGaugeMetadataIfCollectionEnabled(ApplicationProcessState.FOREGROUND); startOrStopCollectingGauges(ApplicationProcessState.FOREGROUND); } @@ -206,12 +195,6 @@ public void unregisterForSessionUpdates(WeakReference client } } - private void logGaugeMetadataIfCollectionEnabled(ApplicationProcessState appState) { - if (perfSession.isGaugeAndEventCollectionEnabled()) { - gaugeManager.logGaugeMetadata(perfSession.sessionId(), appState); - } - } - private void startOrStopCollectingGauges(ApplicationProcessState appState) { if (perfSession.isGaugeAndEventCollectionEnabled()) { gaugeManager.startCollectingGauges(perfSession, appState); 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..ceb636d56b3 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; @@ -163,7 +161,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate( this.cpuMetricCollectionRateMs = cpuMetricCollectionRate; try { cpuMetricCollectorJob = - cpuMetricCollectorExecutor.scheduleAtFixedRate( + cpuMetricCollectorExecutor.scheduleWithFixedDelay( () -> { CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime); if (currCpuReading != null) { @@ -181,7 +179,7 @@ private synchronized void scheduleCpuMetricCollectionWithRate( private synchronized void scheduleCpuMetricCollectionOnce(Timer referenceTime) { try { @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + ScheduledFuture unusedFuture = cpuMetricCollectorExecutor.schedule( () -> { CpuMetricReading currCpuReading = syncCollectCpuMetric(referenceTime); @@ -227,12 +225,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/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java index 7f6182a9c15..30da2f0160f 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 @@ -72,8 +72,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 +81,7 @@ private GaugeManager() { Lazy gaugeManagerExecutor, TransportManager transportManager, ConfigResolver configResolver, - GaugeMetadataManager gaugeMetadataManager, + @Nullable GaugeMetadataManager gaugeMetadataManager, Lazy cpuGaugeCollector, Lazy memoryGaugeCollector) { @@ -140,7 +140,7 @@ public void startCollectingGauges( gaugeManagerDataCollectionJob = gaugeManagerExecutor .get() - .scheduleAtFixedRate( + .scheduleWithFixedDelay( () -> { syncFlush(sessionIdForScheduledTask, applicationProcessStateForScheduledTask); }, @@ -256,6 +256,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { * @return true if GaugeMetadata was logged, false otherwise. */ public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { + // TODO(b/394127311): Re-introduce logging of metadata for AQS. if (gaugeMetadataManager != null) { GaugeMetric gaugeMetric = GaugeMetric.newBuilder() 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..a7b4b40002a 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,7 +124,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate( try { memoryMetricCollectorJob = - memoryMetricCollectorExecutor.scheduleAtFixedRate( + memoryMetricCollectorExecutor.scheduleWithFixedDelay( () -> { AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime); if (memoryReading != null) { @@ -142,7 +142,7 @@ private synchronized void scheduleMemoryMetricCollectionWithRate( private synchronized void scheduleMemoryMetricCollectionOnce(Timer referenceTime) { try { @SuppressWarnings("FutureReturnValueIgnored") - ScheduledFuture unusedFuture = + ScheduledFuture unusedFuture = memoryMetricCollectorExecutor.schedule( () -> { AndroidMemoryReading memoryReading = syncCollectMemoryMetric(referenceTime); 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..37b9ff7215b 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 @@ -74,7 +74,7 @@ public void testInstanceCreation() { } @Test - public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsInitialized() + public void setApplicationContext_initializeGaugeMetadataManager() throws ExecutionException, InterruptedException { when(mockPerfSession.isGaugeAndEventCollectionEnabled()).thenReturn(true); InOrder inOrder = Mockito.inOrder(mockGaugeManager); @@ -84,7 +84,6 @@ public void setApplicationContext_logGaugeMetadata_afterGaugeMetadataManagerIsIn testSessionManager.getSyncInitFuture().get(); inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(any()); - inOrder.verify(mockGaugeManager).logGaugeMetadata(any(), any()); } @Test @@ -136,20 +135,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess 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() { @@ -178,21 +163,6 @@ public void testOnUpdateAppStateGeneratesNewSessionIdOnBackgroundStateIfPerfSess 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(); @@ -232,32 +202,6 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSession 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); 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"; } From c5e9b9b6ffb91245ac7d25846813bbcc1428fb3e Mon Sep 17 00:00:00 2001 From: Tejas Deshpande Date: Tue, 11 Feb 2025 13:06:38 -0500 Subject: [PATCH 037/146] Implement a SessionSubscriber for Firebase Performance (#6683) This PR doesn't change the use of session ID to AQS - except in GaugeMetadata. I've added TODOs to identify the missing locations. --- firebase-perf/firebase-perf.gradle | 2 +- .../firebase/perf/FirebasePerfRegistrar.java | 7 + .../firebase/perf/FirebasePerformance.java | 17 +-- .../FirebasePerformanceSessionSubscriber.kt | 46 +++++++ .../firebase/perf/session/PerfSession.java | 18 ++- .../firebase/perf/session/SessionManager.java | 53 +------- .../perf/session/gauges/GaugeManager.java | 10 +- .../perf/transport/TransportManager.java | 1 + .../perf/session/SessionManagerTest.java | 121 +++--------------- .../api/FirebaseSessionsDependencies.kt | 13 -- 10 files changed, 103 insertions(+), 185 deletions(-) create mode 100644 firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt diff --git a/firebase-perf/firebase-perf.gradle b/firebase-perf/firebase-perf.gradle index c0fd6df6056..49c921edeb0 100644 --- a/firebase-perf/firebase-perf.gradle +++ b/firebase-perf/firebase-perf.gradle @@ -118,7 +118,7 @@ dependencies { api("com.google.firebase:firebase-components:18.0.0") api("com.google.firebase:firebase-config:21.5.0") api("com.google.firebase:firebase-installations:17.2.0") - api("com.google.firebase:firebase-sessions:2.0.7") { + api(project(":firebase-sessions")) { 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/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 3cc49896ce0..e4ddfcd600c 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 @@ -36,12 +36,14 @@ import com.google.firebase.perf.logging.ConsoleUrlGenerator; import com.google.firebase.perf.metrics.HttpMetric; import com.google.firebase.perf.metrics.Trace; +import com.google.firebase.perf.session.FirebasePerformanceSessionSubscriber; import com.google.firebase.perf.session.SessionManager; import com.google.firebase.perf.transport.TransportManager; import com.google.firebase.perf.util.Constants; import com.google.firebase.perf.util.ImmutableBundle; import com.google.firebase.perf.util.Timer; import com.google.firebase.remoteconfig.RemoteConfigComponent; +import com.google.firebase.sessions.api.FirebaseSessionsDependencies; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.net.URL; @@ -136,11 +138,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,11 +163,6 @@ 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; @@ -191,6 +183,9 @@ public static FirebasePerformance getInstance() { sessionManager.setApplicationContext(appContext); mPerformanceCollectionForceEnabledState = configResolver.getIsPerformanceCollectionEnabled(); + FirebaseSessionsDependencies.register( + new FirebasePerformanceSessionSubscriber(isPerformanceCollectionEnabled())); + if (logger.isLogcatEnabled() && isPerformanceCollectionEnabled()) { logger.info( String.format( @@ -281,7 +276,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/session/FirebasePerformanceSessionSubscriber.kt b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt new file mode 100644 index 00000000000..b6a3d30c139 --- /dev/null +++ b/firebase-perf/src/main/java/com/google/firebase/perf/session/FirebasePerformanceSessionSubscriber.kt @@ -0,0 +1,46 @@ +/* + * 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.session.gauges.GaugeManager +import com.google.firebase.perf.v1.ApplicationProcessState +import com.google.firebase.sessions.api.SessionSubscriber +import java.util.UUID + +class FirebasePerformanceSessionSubscriber(override val isDataCollectionEnabled: Boolean) : + SessionSubscriber { + + override val sessionSubscriberName: SessionSubscriber.Name = SessionSubscriber.Name.PERFORMANCE + + override fun onSessionChanged(sessionDetails: SessionSubscriber.SessionDetails) { + val currentPerfSession = SessionManager.getInstance().perfSession() + + // A [PerfSession] was created before a session was started. + if (currentPerfSession.aqsSessionId() == null) { + currentPerfSession.setAQSId(sessionDetails) + GaugeManager.getInstance() + .logGaugeMetadata(currentPerfSession.aqsSessionId(), ApplicationProcessState.FOREGROUND) + return + } + + val updatedSession = PerfSession.createWithId(UUID.randomUUID().toString()) + updatedSession.setAQSId(sessionDetails) + SessionManager.getInstance().updatePerfSession(updatedSession) + GaugeManager.getInstance() + .logGaugeMetadata(updatedSession.aqsSessionId(), ApplicationProcessState.FOREGROUND) + } +} 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..075848ab747 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 @@ -23,6 +23,7 @@ import com.google.firebase.perf.util.Clock; import com.google.firebase.perf.util.Timer; import com.google.firebase.perf.v1.SessionVerbosity; +import com.google.firebase.sessions.api.SessionSubscriber; import java.util.List; import java.util.concurrent.TimeUnit; @@ -31,6 +32,7 @@ public class PerfSession implements Parcelable { private final String sessionId; private final Timer creationTime; + @Nullable private String aqsSessionId; private boolean isGaugeAndEventCollectionEnabled = false; @@ -59,11 +61,24 @@ private PerfSession(@NonNull Parcel in) { creationTime = in.readParcelable(Timer.class.getClassLoader()); } - /** Returns the sessionId of the object. */ + /** Returns the sessionId of the session. */ public String sessionId() { return sessionId; } + /** Returns the AQS sessionId for the given session. */ + @Nullable + public String aqsSessionId() { + return aqsSessionId; + } + + /** Sets the AQS sessionId for the given session. */ + public void setAQSId(SessionSubscriber.SessionDetails aqs) { + if (aqsSessionId == null) { + aqsSessionId = aqs.getSessionId(); + } + } + /** * Returns a timer object that has been seeded with the system time at which the session began. */ @@ -113,6 +128,7 @@ public boolean isSessionRunningTooLong() { /** Creates and returns the proto object for PerfSession object. */ public com.google.firebase.perf.v1.PerfSession build() { + // TODO(b/394127311): Switch to using AQS. com.google.firebase.perf.v1.PerfSession.Builder sessionMetric = com.google.firebase.perf.v1.PerfSession.newBuilder().setSessionId(sessionId); 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 29ffb988ba0..cf99c1e52ea 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 @@ -19,7 +19,6 @@ 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.session.gauges.GaugeManager; import com.google.firebase.perf.v1.ApplicationProcessState; import com.google.firebase.perf.v1.GaugeMetadata; @@ -27,15 +26,13 @@ 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(); @@ -45,7 +42,6 @@ public class SessionManager extends AppStateUpdateHandler { private final Set> clients = new HashSet<>(); private PerfSession perfSession; - private Future syncInitFuture; /** Returns the singleton instance of SessionManager. */ public static SessionManager getInstance() { @@ -71,7 +67,6 @@ public SessionManager( this.gaugeManager = gaugeManager; this.perfSession = perfSession; this.appStateMonitor = appStateMonitor; - registerForAppState(); } /** @@ -79,42 +74,7 @@ public SessionManager( * (currently that is before onResume finishes) to ensure gauge collection starts on time. */ public void setApplicationContext(final Context appContext) { - // TODO(b/258263016): Migrate to go/firebase-android-executors - @SuppressLint("ThreadPoolCreation") - ExecutorService executorService = Executors.newSingleThreadExecutor(); - syncInitFuture = - executorService.submit( - () -> { - gaugeManager.initializeGaugeMetadataManager(appContext); - }); - } - - @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); } /** @@ -138,7 +98,7 @@ 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; } @@ -207,9 +167,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/GaugeManager.java b/firebase-perf/src/main/java/com/google/firebase/perf/session/gauges/GaugeManager.java index 30da2f0160f..1c06ceac9dd 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 @@ -136,6 +136,7 @@ public void startCollectingGauges( final String sessionIdForScheduledTask = sessionId; final ApplicationProcessState applicationProcessStateForScheduledTask = applicationProcessState; + // TODO(b/394127311): Switch to using AQS. try { gaugeManagerDataCollectionJob = gaugeManagerExecutor @@ -204,6 +205,7 @@ public void stopCollectingGauges() { gaugeManagerDataCollectionJob.cancel(false); } + // TODO(b/394127311): Switch to using AQS. // Flush any data that was collected for this session one last time. @SuppressWarnings("FutureReturnValueIgnored") ScheduledFuture unusedFuture = @@ -242,6 +244,7 @@ private void syncFlush(String sessionId, ApplicationProcessState appState) { } // Adding Session ID info. + // TODO(b/394127311): Switch to using AQS. gaugeMetricBuilder.setSessionId(sessionId); transportManager.log(gaugeMetricBuilder.build(), appState); @@ -250,17 +253,16 @@ 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 + * @param aqsSessionId The {@link PerfSession#aqsSessionId()} ()} to which the collected Gauge Metrics * should be associated with. * @param appState The {@link ApplicationProcessState} for which these gauges are collected. * @return true if GaugeMetadata was logged, false otherwise. */ - public boolean logGaugeMetadata(String sessionId, ApplicationProcessState appState) { - // TODO(b/394127311): Re-introduce logging of metadata for AQS. + public boolean logGaugeMetadata(String aqsSessionId, ApplicationProcessState appState) { if (gaugeMetadataManager != null) { GaugeMetric gaugeMetric = GaugeMetric.newBuilder() - .setSessionId(sessionId) + .setSessionId(aqsSessionId) .setGaugeMetadata(getGaugeMetadata()) .build(); transportManager.log(gaugeMetric, appState); 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..159af53d3d3 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 @@ -354,6 +354,7 @@ public void log(final GaugeMetric gaugeMetric) { * {@link #isAllowedToDispatch(PerfMetric)}). */ public void log(final GaugeMetric gaugeMetric, final ApplicationProcessState appState) { + // TODO(b/394127311): This *might* potentially be the right place to get AQS. executorService.execute( () -> syncLog(PerfMetric.newBuilder().setGaugeMetric(gaugeMetric), appState)); } 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 37b9ff7215b..954b0ae88d3 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 @@ -16,9 +16,6 @@ import static com.google.common.truth.Truth.assertThat; 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 +37,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; @@ -82,105 +78,15 @@ public void setApplicationContext_initializeGaugeMetadataManager() new SessionManager(mockGaugeManager, mockPerfSession, mockAppStateMonitor); testSessionManager.setApplicationContext(mockApplicationContext); - testSessionManager.getSyncInitFuture().get(); inOrder.verify(mockGaugeManager).initializeGaugeMetadataManager(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 - 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 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 // NotLogGaugeData on new perf session when not Verbose // Mark Session as expired after time limit. @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() { + public void testUpdatePerfSessionMakesGaugeManagerStopCollectingGaugesIfSessionIsNonVerbose() { forceNonVerboseSession(); SessionManager testSessionManager = @@ -191,7 +97,7 @@ public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesIfSessionIs } @Test - public void testOnUpdateAppStateMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() { + public void testUpdatePerfSessionMakesGaugeManagerStopCollectingGaugesWhenSessionsDisabled() { forceSessionsFeatureDisabled(); SessionManager testSessionManager = @@ -221,22 +127,25 @@ public void testSessionIdDoesNotUpdateIfPerfSessionRunsTooLong() { } @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 = new PerfSession("previousSession", mockClock); + previousSession.setGaugeAndEventCollectionEnabled(false); - assertThat(session.isSessionRunningTooLong()).isFalse(); + PerfSession newSession = new PerfSession("newSession", mockClock); + newSession.setGaugeAndEventCollectionEnabled(true); - when(mockTimer.getDurationMicros()) - .thenReturn(TimeUnit.HOURS.toMicros(5)); // Default Max Session Length is 4 hours + SessionManager testSessionManager = + new SessionManager(mockGaugeManager, previousSession, mockAppStateMonitor); + 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, ApplicationProcessState.FOREGROUND); } @Test diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt index 8d3548c8f4b..4b636a155e0 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/api/FirebaseSessionsDependencies.kt @@ -40,19 +40,6 @@ object FirebaseSessionsDependencies { */ @JvmStatic fun addDependency(subscriberName: SessionSubscriber.Name) { - if (subscriberName == SessionSubscriber.Name.PERFORMANCE) { - throw IllegalArgumentException( - """ - Incompatible versions of Firebase Perf and Firebase Sessions. - A safe combination would be: - firebase-sessions:1.1.0 - firebase-crashlytics:18.5.0 - firebase-perf:20.5.0 - For more information contact Firebase Support. - """ - .trimIndent() - ) - } if (dependencies.containsKey(subscriberName)) { Log.d(TAG, "Dependency $subscriberName already added.") return From c71fd96f4adfaf6e099a4ea08654227c9c9e08d6 Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Thu, 20 Mar 2025 10:28:52 -0600 Subject: [PATCH 038/146] Use multi-process DataStore instead of Preferences DataStore (#6781) Use multi-process DataStore instead of Preferences DataStore. This change allows multiple processes to share the same datastore file safely. This reduces settings fetch to one per app run, not one per process. Also updated the TimeProvider to provide an object with explicit time units. This will make time less error prone. Removed all instances of `System.currentTimeMillis()` from tests, making them deterministic. --- firebase-sessions/CHANGELOG.md | 14 +- .../firebase-sessions.gradle.kts | 4 +- .../sessions/FirebaseSessionsComponent.kt | 60 +++--- .../sessions/FirebaseSessionsRegistrar.kt | 4 +- .../sessions/SessionDataStoreConfigs.kt | 40 ---- .../firebase/sessions/SessionDatastore.kt | 65 +++--- .../firebase/sessions/SessionGenerator.kt | 2 +- .../google/firebase/sessions/TimeProvider.kt | 15 +- .../sessions/settings/RemoteSettings.kt | 52 ++--- .../settings/RemoteSettingsFetcher.kt | 4 +- .../sessions/settings/SessionConfigs.kt | 58 ++++++ .../sessions/settings/SettingsCache.kt | 123 ++++------- .../firebase/sessions/SessionDatastoreTest.kt | 59 ++++++ .../firebase/sessions/SessionGeneratorTest.kt | 13 +- .../sessions/settings/RemoteSettingsTest.kt | 196 ++++++++---------- .../sessions/settings/SessionsSettingsTest.kt | 48 ++--- .../sessions/settings/SettingsCacheTest.kt | 169 ++++++++------- .../sessions/testing/FakeSettingsCache.kt | 52 +++++ .../sessions/testing/FakeTimeProvider.kt | 10 +- .../sessions/testing/TestDataStores.kt | 50 +++++ .../sessions/testing/TestSessionEventData.kt | 16 +- gradle/libs.versions.toml | 1 + 22 files changed, 575 insertions(+), 480 deletions(-) delete mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt create mode 100644 firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt create mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt diff --git a/firebase-sessions/CHANGELOG.md b/firebase-sessions/CHANGELOG.md index 8353fbf9029..f73a860ee60 100644 --- a/firebase-sessions/CHANGELOG.md +++ b/firebase-sessions/CHANGELOG.md @@ -1,5 +1,5 @@ # Unreleased - +* [changed] Use multi-process DataStore instead of Preferences DataStore # 2.1.0 * [changed] Add warning for known issue b/328687152 @@ -7,21 +7,9 @@ * [changed] Updated datastore dependency to v1.1.3 to fix [CVE-2024-7254](https://github.com/advisories/GHSA-735f-pc8j-v9w8). - -## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-sessions` library. The Kotlin extensions library has no additional -updates. - # 2.0.9 * [fixed] Make AQS resilient to background init in multi-process apps. - -## Kotlin -The Kotlin extensions library transitively includes the updated -`firebase-sessions` library. The Kotlin extensions library has no additional -updates. - # 2.0.7 * [fixed] Removed extraneous logs that risk leaking internal identifiers. diff --git a/firebase-sessions/firebase-sessions.gradle.kts b/firebase-sessions/firebase-sessions.gradle.kts index b136a281660..23edc952d5e 100644 --- a/firebase-sessions/firebase-sessions.gradle.kts +++ b/firebase-sessions/firebase-sessions.gradle.kts @@ -21,6 +21,7 @@ plugins { id("firebase-vendor") id("kotlin-android") id("kotlin-kapt") + id("kotlinx-serialization") } firebaseLibrary { @@ -76,7 +77,8 @@ dependencies { implementation("com.google.android.datatransport:transport-api:3.2.0") implementation(libs.javax.inject) implementation(libs.androidx.annotation) - implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.datastore) + implementation(libs.kotlinx.serialization.json) vendor(libs.dagger.dagger) { exclude(group = "javax.inject", module = "javax.inject") } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt index 5680c9cc0ec..99de9e4a3fc 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsComponent.kt @@ -19,23 +19,24 @@ package com.google.firebase.sessions import android.content.Context import android.util.Log import androidx.datastore.core.DataStore +import androidx.datastore.core.MultiProcessDataStoreFactory import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.preferencesDataStoreFile +import androidx.datastore.dataStoreFile import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.inject.Provider import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.sessions.ProcessDetailsProvider.getProcessName import com.google.firebase.sessions.settings.CrashlyticsSettingsFetcher import com.google.firebase.sessions.settings.LocalOverrideSettings import com.google.firebase.sessions.settings.RemoteSettings import com.google.firebase.sessions.settings.RemoteSettingsFetcher +import com.google.firebase.sessions.settings.SessionConfigs +import com.google.firebase.sessions.settings.SessionConfigsSerializer import com.google.firebase.sessions.settings.SessionsSettings +import com.google.firebase.sessions.settings.SettingsCache +import com.google.firebase.sessions.settings.SettingsCacheImpl import com.google.firebase.sessions.settings.SettingsProvider import dagger.Binds import dagger.BindsInstance @@ -45,10 +46,7 @@ import dagger.Provides import javax.inject.Qualifier import javax.inject.Singleton import kotlin.coroutines.CoroutineContext - -@Qualifier internal annotation class SessionConfigsDataStore - -@Qualifier internal annotation class SessionDetailsDataStore +import kotlinx.coroutines.CoroutineScope @Qualifier internal annotation class LocalOverrideSettingsProvider @@ -119,6 +117,8 @@ internal interface FirebaseSessionsComponent { @RemoteSettingsProvider fun remoteSettings(impl: RemoteSettings): SettingsProvider + @Binds @Singleton fun settingsCache(impl: SettingsCacheImpl): SettingsCache + companion object { private const val TAG = "FirebaseSessions" @@ -133,31 +133,37 @@ internal interface FirebaseSessionsComponent { @Provides @Singleton - @SessionConfigsDataStore - fun sessionConfigsDataStore(appContext: Context): DataStore = - PreferenceDataStoreFactory.create( + fun sessionConfigsDataStore( + appContext: Context, + @Blocking blockingDispatcher: CoroutineContext, + ): DataStore = + MultiProcessDataStoreFactory.create( + serializer = SessionConfigsSerializer, corruptionHandler = ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in settings DataStore in ${getProcessName()}.", ex) - emptyPreferences() - } - ) { - appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SETTINGS_CONFIG_NAME) - } + Log.w(TAG, "CorruptionException in session configs DataStore", ex) + SessionConfigsSerializer.defaultValue + }, + scope = CoroutineScope(blockingDispatcher), + produceFile = { appContext.dataStoreFile("aqs/sessionConfigsDataStore.data") }, + ) @Provides @Singleton - @SessionDetailsDataStore - fun sessionDetailsDataStore(appContext: Context): DataStore = - PreferenceDataStoreFactory.create( + fun sessionDataStore( + appContext: Context, + @Blocking blockingDispatcher: CoroutineContext, + ): DataStore = + MultiProcessDataStoreFactory.create( + serializer = SessionDataSerializer, corruptionHandler = ReplaceFileCorruptionHandler { ex -> - Log.w(TAG, "CorruptionException in sessions DataStore in ${getProcessName()}.", ex) - emptyPreferences() - } - ) { - appContext.preferencesDataStoreFile(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME) - } + Log.w(TAG, "CorruptionException in session data DataStore", ex) + SessionDataSerializer.defaultValue + }, + scope = CoroutineScope(blockingDispatcher), + produceFile = { appContext.dataStoreFile("aqs/sessionDataStore.data") }, + ) } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt index 5cb8de7a182..76c0c6330f4 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/FirebaseSessionsRegistrar.kt @@ -19,7 +19,7 @@ package com.google.firebase.sessions import android.content.Context import android.util.Log import androidx.annotation.Keep -import androidx.datastore.preferences.preferencesDataStore +import androidx.datastore.core.MultiProcessDataStoreFactory import com.google.android.datatransport.TransportFactory import com.google.firebase.FirebaseApp import com.google.firebase.annotations.concurrent.Background @@ -84,7 +84,7 @@ internal class FirebaseSessionsRegistrar : ComponentRegistrar { init { try { - ::preferencesDataStore.javaClass + MultiProcessDataStoreFactory.javaClass } catch (ex: NoClassDefFoundError) { Log.w( TAG, diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt deleted file mode 100644 index 109e980e666..00000000000 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDataStoreConfigs.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 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.sessions - -import android.util.Base64 -import com.google.firebase.sessions.settings.SessionsSettings - -/** - * Util object for handling DataStore configs in multi-process apps safely. - * - * This can be removed when datastore-preferences:1.1.0 becomes stable. - */ -internal object SessionDataStoreConfigs { - /** Sanitized process name to use in config filenames. */ - private val PROCESS_NAME = - Base64.encodeToString( - ProcessDetailsProvider.getProcessName().encodeToByteArray(), - Base64.NO_WRAP or Base64.URL_SAFE, // URL safe is also filename safe. - ) - - /** Config name for [SessionDatastore] */ - val SESSIONS_CONFIG_NAME = "firebase_session_${PROCESS_NAME}_data" - - /** Config name for [SessionsSettings] */ - val SETTINGS_CONFIG_NAME = "firebase_session_${PROCESS_NAME}_settings" -} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt index 2c4f243f942..b3b72b4d4d7 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionDatastore.kt @@ -17,15 +17,15 @@ package com.google.firebase.sessions import android.util.Log +import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.emptyPreferences -import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.core.Serializer import com.google.firebase.Firebase import com.google.firebase.annotations.concurrent.Background import com.google.firebase.app import java.io.IOException +import java.io.InputStream +import java.io.OutputStream import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import javax.inject.Singleton @@ -33,11 +33,29 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.catch -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json -/** Datastore for sessions information */ -internal data class FirebaseSessionsData(val sessionId: String?) +/** Data for sessions information */ +@Serializable internal data class SessionData(val sessionId: String?) + +/** DataStore json [Serializer] for [SessionData]. */ +internal object SessionDataSerializer : Serializer { + override val defaultValue = SessionData(sessionId = null) + + override suspend fun readFrom(input: InputStream): SessionData = + try { + Json.decodeFromString(input.readBytes().decodeToString()) + } catch (ex: Exception) { + throw CorruptionException("Cannot parse session data", ex) + } + + override suspend fun writeTo(t: SessionData, output: OutputStream) { + @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls + output.write(Json.encodeToString(SessionData.serializer(), t).encodeToByteArray()) + } +} /** Handles reading to and writing from the [DataStore]. */ internal interface SessionDatastore { @@ -61,23 +79,17 @@ internal class SessionDatastoreImpl @Inject constructor( @Background private val backgroundDispatcher: CoroutineContext, - @SessionDetailsDataStore private val dataStore: DataStore, + private val sessionDataStore: DataStore, ) : SessionDatastore { /** Most recent session from datastore is updated asynchronously whenever it changes */ - private val currentSessionFromDatastore = AtomicReference() + private val currentSessionFromDatastore = AtomicReference() - private object FirebaseSessionDataKeys { - val SESSION_ID = stringPreferencesKey("session_id") - } - - private val firebaseSessionDataFlow: Flow = - dataStore.data - .catch { exception -> - Log.e(TAG, "Error reading stored session data.", exception) - emit(emptyPreferences()) - } - .map { preferences -> mapSessionsData(preferences) } + private val firebaseSessionDataFlow: Flow = + sessionDataStore.data.catch { ex -> + Log.e(TAG, "Error reading stored session data.", ex) + emit(SessionDataSerializer.defaultValue) + } init { CoroutineScope(backgroundDispatcher).launch { @@ -88,19 +100,14 @@ constructor( override fun updateSessionId(sessionId: String) { CoroutineScope(backgroundDispatcher).launch { try { - dataStore.edit { preferences -> - preferences[FirebaseSessionDataKeys.SESSION_ID] = sessionId - } - } catch (e: IOException) { - Log.w(TAG, "Failed to update session Id: $e") + sessionDataStore.updateData { SessionData(sessionId) } + } catch (ex: IOException) { + Log.w(TAG, "Failed to update session Id", ex) } } } - override fun getCurrentSessionId() = currentSessionFromDatastore.get()?.sessionId - - private fun mapSessionsData(preferences: Preferences): FirebaseSessionsData = - FirebaseSessionsData(preferences[FirebaseSessionDataKeys.SESSION_ID]) + override fun getCurrentSessionId(): String? = currentSessionFromDatastore.get()?.sessionId private companion object { private const val TAG = "FirebaseSessionsRepo" diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt index 4c4775e8b24..409f9989348 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/SessionGenerator.kt @@ -60,7 +60,7 @@ constructor(private val timeProvider: TimeProvider, private val uuidGenerator: U sessionId = if (sessionIndex == 0) firstSessionId else generateSessionId(), firstSessionId, sessionIndex, - sessionStartTimestampUs = timeProvider.currentTimeUs(), + sessionStartTimestampUs = timeProvider.currentTime().us, ) return currentSession } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt index b66b09af19f..869b64b2ff2 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/TimeProvider.kt @@ -20,11 +20,17 @@ import android.os.SystemClock import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +/** Time with accessors for microseconds, milliseconds, and seconds. */ +internal data class Time(val ms: Long) { + val us = ms * 1_000 + val seconds = ms / 1_000 +} + /** Time provider interface, for testing purposes. */ internal interface TimeProvider { fun elapsedRealtime(): Duration - fun currentTimeUs(): Long + fun currentTime(): Time } /** "Wall clock" time provider implementation. */ @@ -38,14 +44,11 @@ internal object TimeProviderImpl : TimeProvider { override fun elapsedRealtime(): Duration = SystemClock.elapsedRealtime().milliseconds /** - * Gets the current "wall clock" time in microseconds. + * Gets the current "wall clock" time. * * This clock can be set by the user or the phone network, so the time may jump backwards or * forwards unpredictably. This clock should only be used when correspondence with real-world * dates and times is important, such as in a calendar or alarm clock application. */ - override fun currentTimeUs(): Long = System.currentTimeMillis() * US_PER_MILLIS - - /** Microseconds per millisecond. */ - private const val US_PER_MILLIS = 1000L + override fun currentTime(): Time = Time(ms = System.currentTimeMillis()) } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt index 67a48bc7924..b715cd9f79c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt @@ -19,18 +19,17 @@ package com.google.firebase.sessions.settings import android.os.Build import android.util.Log import androidx.annotation.VisibleForTesting -import com.google.firebase.annotations.concurrent.Background import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.InstallationId +import com.google.firebase.sessions.TimeProvider import dagger.Lazy import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.CoroutineContext import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.json.JSONException @@ -40,7 +39,7 @@ import org.json.JSONObject internal class RemoteSettings @Inject constructor( - @Background private val backgroundDispatcher: CoroutineContext, + private val timeProvider: TimeProvider, private val firebaseInstallationsApi: FirebaseInstallationsApi, private val appInfo: ApplicationInfo, private val configsFetcher: CrashlyticsSettingsFetcher, @@ -90,10 +89,9 @@ constructor( val options = mapOf( "X-Crashlytics-Installation-ID" to installationId, - "X-Crashlytics-Device-Model" to - removeForwardSlashesIn(String.format("%s/%s", Build.MANUFACTURER, Build.MODEL)), - "X-Crashlytics-OS-Build-Version" to removeForwardSlashesIn(Build.VERSION.INCREMENTAL), - "X-Crashlytics-OS-Display-Version" to removeForwardSlashesIn(Build.VERSION.RELEASE), + "X-Crashlytics-Device-Model" to sanitize("${Build.MANUFACTURER}${Build.MODEL}"), + "X-Crashlytics-OS-Build-Version" to sanitize(Build.VERSION.INCREMENTAL), + "X-Crashlytics-OS-Display-Version" to sanitize(Build.VERSION.RELEASE), "X-Crashlytics-API-Client-Version" to appInfo.sessionSdkVersion, ) @@ -129,22 +127,19 @@ constructor( } } - sessionsEnabled?.let { settingsCache.updateSettingsEnabled(sessionsEnabled) } - - sessionTimeoutSeconds?.let { - settingsCache.updateSessionRestartTimeout(sessionTimeoutSeconds) - } - - sessionSamplingRate?.let { settingsCache.updateSamplingRate(sessionSamplingRate) } - - cacheDuration?.let { settingsCache.updateSessionCacheDuration(cacheDuration) } - ?: let { settingsCache.updateSessionCacheDuration(86400) } - - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = sessionsEnabled, + sessionTimeoutSeconds = sessionTimeoutSeconds, + sessionSamplingRate = sessionSamplingRate, + cacheDurationSeconds = cacheDuration ?: defaultCacheDuration, + cacheUpdatedTimeMs = timeProvider.currentTime().ms, + ) + ) }, onFailure = { msg -> // Network request failed here. - Log.e(TAG, "Error failing to fetch the remote configs: $msg") + Log.e(TAG, "Error failed to fetch the remote configs: $msg") }, ) } @@ -153,18 +148,17 @@ constructor( override fun isSettingsStale(): Boolean = settingsCache.hasCacheExpired() @VisibleForTesting - internal fun clearCachedSettings() { - val scope = CoroutineScope(backgroundDispatcher) - scope.launch { settingsCache.removeConfigs() } + internal fun clearCachedSettings() = runBlocking { + settingsCache.updateConfigs(SessionConfigsSerializer.defaultValue) } - private fun removeForwardSlashesIn(s: String): String { - return s.replace(FORWARD_SLASH_STRING.toRegex(), "") - } + private fun sanitize(s: String) = s.replace(sanitizeRegex, "") private companion object { const val TAG = "SessionConfigFetcher" - const val FORWARD_SLASH_STRING: String = "/" + val defaultCacheDuration = 24.hours.inWholeSeconds.toInt() + + val sanitizeRegex = "/".toRegex() } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt index 92d530f2fa1..bd45ec8fb24 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettingsFetcher.kt @@ -17,7 +17,7 @@ package com.google.firebase.sessions.settings import android.net.Uri -import com.google.firebase.annotations.concurrent.Background +import com.google.firebase.annotations.concurrent.Blocking import com.google.firebase.sessions.ApplicationInfo import java.io.BufferedReader import java.io.InputStreamReader @@ -42,7 +42,7 @@ internal class RemoteSettingsFetcher @Inject constructor( private val appInfo: ApplicationInfo, - @Background private val blockingDispatcher: CoroutineContext, + @Blocking private val blockingDispatcher: CoroutineContext, ) : CrashlyticsSettingsFetcher { @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls. override suspend fun doConfigFetch( diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt new file mode 100644 index 00000000000..8d7e2484675 --- /dev/null +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt @@ -0,0 +1,58 @@ +/* + * 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.sessions.settings + +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.Serializer +import java.io.InputStream +import java.io.OutputStream +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json + +/** Session configs data for caching. */ +@Serializable +internal data class SessionConfigs( + val sessionsEnabled: Boolean?, + val sessionSamplingRate: Double?, + val sessionTimeoutSeconds: Int?, + val cacheDurationSeconds: Int?, + val cacheUpdatedTimeMs: Long?, +) + +/** DataStore json [Serializer] for [SessionConfigs]. */ +internal object SessionConfigsSerializer : Serializer { + override val defaultValue = + SessionConfigs( + sessionsEnabled = null, + sessionSamplingRate = null, + sessionTimeoutSeconds = null, + cacheDurationSeconds = null, + cacheUpdatedTimeMs = null, + ) + + override suspend fun readFrom(input: InputStream): SessionConfigs = + try { + Json.decodeFromString(input.readBytes().decodeToString()) + } catch (ex: Exception) { + throw CorruptionException("Cannot parse session configs", ex) + } + + override suspend fun writeTo(t: SessionConfigs, output: OutputStream) { + @Suppress("BlockingMethodInNonBlockingContext") // blockingDispatcher is safe for blocking calls + output.write(Json.encodeToString(SessionConfigs.serializer(), t).encodeToByteArray()) + } +} diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt index 2e60e51650a..468bbad6b7a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt @@ -17,128 +17,77 @@ package com.google.firebase.sessions.settings import android.util.Log -import androidx.annotation.VisibleForTesting import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.booleanPreferencesKey -import androidx.datastore.preferences.core.doublePreferencesKey -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.core.longPreferencesKey -import com.google.firebase.sessions.SessionConfigsDataStore +import com.google.firebase.sessions.TimeProvider import java.io.IOException import javax.inject.Inject import javax.inject.Singleton import kotlinx.coroutines.flow.first import kotlinx.coroutines.runBlocking -internal data class SessionConfigs( - val sessionEnabled: Boolean?, - val sessionSamplingRate: Double?, - val sessionRestartTimeout: Int?, - val cacheDuration: Int?, - val cacheUpdatedTime: Long?, -) +internal interface SettingsCache { + fun hasCacheExpired(): Boolean + + fun sessionsEnabled(): Boolean? + + fun sessionSamplingRate(): Double? + + fun sessionRestartTimeout(): Int? + + suspend fun updateConfigs(sessionConfigs: SessionConfigs) +} @Singleton -internal class SettingsCache +internal class SettingsCacheImpl @Inject -constructor(@SessionConfigsDataStore private val dataStore: DataStore) { - private lateinit var sessionConfigs: SessionConfigs +constructor( + private val timeProvider: TimeProvider, + private val sessionConfigsDataStore: DataStore, +) : SettingsCache { + private var sessionConfigs: SessionConfigs init { // Block until the cache is loaded from disk to ensure cache // values are valid and readable from the main thread on init. - runBlocking { updateSessionConfigs(dataStore.data.first().toPreferences()) } + runBlocking { sessionConfigs = sessionConfigsDataStore.data.first() } } - /** Update session configs from the given [preferences]. */ - private fun updateSessionConfigs(preferences: Preferences) { - sessionConfigs = - SessionConfigs( - sessionEnabled = preferences[SESSIONS_ENABLED], - sessionSamplingRate = preferences[SAMPLING_RATE], - sessionRestartTimeout = preferences[RESTART_TIMEOUT_SECONDS], - cacheDuration = preferences[CACHE_DURATION_SECONDS], - cacheUpdatedTime = preferences[CACHE_UPDATED_TIME], - ) - } + override fun hasCacheExpired(): Boolean { + val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs + val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds - internal fun hasCacheExpired(): Boolean { - val cacheUpdatedTime = sessionConfigs.cacheUpdatedTime - val cacheDuration = sessionConfigs.cacheDuration - - if (cacheUpdatedTime != null && cacheDuration != null) { - val timeDifferenceSeconds = (System.currentTimeMillis() - cacheUpdatedTime) / 1000 - if (timeDifferenceSeconds < cacheDuration) { + if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) { + val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000 + if (timeDifferenceSeconds < cacheDurationSeconds) { return false } } return true } - fun sessionsEnabled(): Boolean? = sessionConfigs.sessionEnabled - - fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate - - fun sessionRestartTimeout(): Int? = sessionConfigs.sessionRestartTimeout - - suspend fun updateSettingsEnabled(enabled: Boolean?) { - updateConfigValue(SESSIONS_ENABLED, enabled) - } - - suspend fun updateSamplingRate(rate: Double?) { - updateConfigValue(SAMPLING_RATE, rate) - } - - suspend fun updateSessionRestartTimeout(timeoutInSeconds: Int?) { - updateConfigValue(RESTART_TIMEOUT_SECONDS, timeoutInSeconds) - } + override fun sessionsEnabled(): Boolean? = sessionConfigs.sessionsEnabled - suspend fun updateSessionCacheDuration(cacheDurationInSeconds: Int?) { - updateConfigValue(CACHE_DURATION_SECONDS, cacheDurationInSeconds) - } + override fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate - suspend fun updateSessionCacheUpdatedTime(cacheUpdatedTime: Long?) { - updateConfigValue(CACHE_UPDATED_TIME, cacheUpdatedTime) - } + override fun sessionRestartTimeout(): Int? = sessionConfigs.sessionTimeoutSeconds - @VisibleForTesting - internal suspend fun removeConfigs() { + override suspend fun updateConfigs(sessionConfigs: SessionConfigs) { try { - dataStore.edit { preferences -> - preferences.clear() - updateSessionConfigs(preferences) - } - } catch (e: IOException) { - Log.w(TAG, "Failed to remove config values: $e") + sessionConfigsDataStore.updateData { sessionConfigs } + this.sessionConfigs = sessionConfigs + } catch (ex: IOException) { + Log.w(TAG, "Failed to update config values: $ex") } } - /** Updated the config value, or remove the key if the value is null. */ - private suspend fun updateConfigValue(key: Preferences.Key, value: T?) { - // TODO(mrober): Refactor these to update all the values in one transaction. + internal suspend fun removeConfigs() = try { - dataStore.edit { preferences -> - if (value != null) { - preferences[key] = value - } else { - preferences.remove(key) - } - updateSessionConfigs(preferences) - } + sessionConfigsDataStore.updateData { SessionConfigsSerializer.defaultValue } } catch (ex: IOException) { - Log.w(TAG, "Failed to update cache config value: $ex") + Log.w(TAG, "Failed to remove config values: $ex") } - } private companion object { const val TAG = "SettingsCache" - - val SESSIONS_ENABLED = booleanPreferencesKey("firebase_sessions_enabled") - val SAMPLING_RATE = doublePreferencesKey("firebase_sessions_sampling_rate") - val RESTART_TIMEOUT_SECONDS = intPreferencesKey("firebase_sessions_restart_timeout") - val CACHE_DURATION_SECONDS = intPreferencesKey("firebase_sessions_cache_duration") - val CACHE_UPDATED_TIME = longPreferencesKey("firebase_sessions_cache_updated_time") } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt new file mode 100644 index 00000000000..7e94eb3113e --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt @@ -0,0 +1,59 @@ +/* + * 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.sessions + +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@RunWith(AndroidJUnit4::class) +class SessionDatastoreTest { + private val appContext: Context = ApplicationProvider.getApplicationContext() + + @Test + fun getCurrentSessionId_returnsLatest() = runTest { + val sessionDatastore = + SessionDatastoreImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + sessionDataStore = + DataStoreFactory.create( + serializer = SessionDataSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") }, + ), + ) + + sessionDatastore.updateSessionId("sessionId1") + sessionDatastore.updateSessionId("sessionId2") + sessionDatastore.updateSessionId("sessionId3") + + runCurrent() + + assertThat(sessionDatastore.getCurrentSessionId()).isEqualTo("sessionId3") + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt index 7126bae4dbf..bf260e73a4f 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionGeneratorTest.kt @@ -16,12 +16,15 @@ package com.google.firebase.sessions +import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.sessions.testing.FakeTimeProvider import com.google.firebase.sessions.testing.FakeUuidGenerator -import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US +import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP import org.junit.Test +import org.junit.runner.RunWith +@RunWith(AndroidJUnit4::class) class SessionGeneratorTest { private fun isValidSessionId(sessionId: String): Boolean { if (sessionId.length != 32) { @@ -96,7 +99,7 @@ class SessionGeneratorTest { sessionId = SESSION_ID_1, firstSessionId = SESSION_ID_1, sessionIndex = 0, - sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US, + sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us, ) ) } @@ -119,7 +122,7 @@ class SessionGeneratorTest { sessionId = SESSION_ID_1, firstSessionId = SESSION_ID_1, sessionIndex = 0, - sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US, + sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us, ) ) @@ -135,7 +138,7 @@ class SessionGeneratorTest { sessionId = SESSION_ID_2, firstSessionId = SESSION_ID_1, sessionIndex = 1, - sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US, + sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us, ) ) @@ -151,7 +154,7 @@ class SessionGeneratorTest { sessionId = SESSION_ID_3, firstSessionId = SESSION_ID_1, sessionIndex = 2, - sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US, + sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us, ) ) } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt index e4fb0b00148..ccaf4f8954d 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt @@ -16,24 +16,22 @@ package com.google.firebase.sessions.settings -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.preferencesDataStoreFile import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp -import com.google.firebase.concurrent.TestOnlyExecutors import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.SessionEvents +import com.google.firebase.sessions.TimeProvider import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher -import kotlin.coroutines.CoroutineContext +import com.google.firebase.sessions.testing.FakeSettingsCache +import com.google.firebase.sessions.testing.FakeTimeProvider import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.test.UnconfinedTestDispatcher @@ -53,22 +51,16 @@ class RemoteSettingsTest { fun remoteSettings_successfulFetchCachesValues() = runTest(UnconfinedTestDispatcher()) { val firebaseApp = FakeFirebaseApp().firebaseApp - val context = firebaseApp.applicationContext val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + FakeTimeProvider(), firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), + FakeSettingsCache(), ) runCurrent() @@ -90,120 +82,100 @@ class RemoteSettingsTest { } @Test - fun remoteSettings_successfulFetchWithLessConfigsCachesOnlyReceivedValues() = - runTest(UnconfinedTestDispatcher()) { - val firebaseApp = FakeFirebaseApp().firebaseApp - val context = firebaseApp.applicationContext - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher() - - val remoteSettings = - buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), - ) - - runCurrent() - - assertThat(remoteSettings.sessionEnabled).isNull() - assertThat(remoteSettings.samplingRate).isNull() - assertThat(remoteSettings.sessionRestartTimeout).isNull() - - val fetchedResponse = JSONObject(VALID_RESPONSE) - fetchedResponse.getJSONObject("app_quality").remove("sessions_enabled") - fakeFetcher.responseJSONObject = fetchedResponse - remoteSettings.updateSettings() - - runCurrent() - - assertThat(remoteSettings.sessionEnabled).isNull() - assertThat(remoteSettings.samplingRate).isEqualTo(0.75) - assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) - - remoteSettings.clearCachedSettings() - } + fun remoteSettings_successfulFetchWithLessConfigsCachesOnlyReceivedValues() = runTest { + val firebaseApp = FakeFirebaseApp().firebaseApp + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher() + + val remoteSettings = + buildRemoteSettings( + FakeTimeProvider(), + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(), + ) + + runCurrent() + + assertThat(remoteSettings.sessionEnabled).isNull() + assertThat(remoteSettings.samplingRate).isNull() + assertThat(remoteSettings.sessionRestartTimeout).isNull() + + val fetchedResponse = JSONObject(VALID_RESPONSE) + fetchedResponse.getJSONObject("app_quality").remove("sessions_enabled") + fakeFetcher.responseJSONObject = fetchedResponse + remoteSettings.updateSettings() + + runCurrent() + + assertThat(remoteSettings.sessionEnabled).isNull() + assertThat(remoteSettings.samplingRate).isEqualTo(0.75) + assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) + + remoteSettings.clearCachedSettings() + } @Test - fun remoteSettings_successfulReFetchUpdatesCache() = - runTest(UnconfinedTestDispatcher()) { - val firebaseApp = FakeFirebaseApp().firebaseApp - val context = firebaseApp.applicationContext - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher() + fun remoteSettings_successfulReFetchUpdatesCache() = runTest { + val firebaseApp = FakeFirebaseApp().firebaseApp + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher() + val fakeTimeProvider = FakeTimeProvider() - val remoteSettings = - buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), - ) + val remoteSettings = + buildRemoteSettings( + fakeTimeProvider, + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(fakeTimeProvider), + ) - val fetchedResponse = JSONObject(VALID_RESPONSE) - fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1) - fakeFetcher.responseJSONObject = fetchedResponse - remoteSettings.updateSettings() + val fetchedResponse = JSONObject(VALID_RESPONSE) + fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1) + fakeFetcher.responseJSONObject = fetchedResponse + remoteSettings.updateSettings() - runCurrent() + runCurrent() - assertThat(remoteSettings.sessionEnabled).isFalse() - assertThat(remoteSettings.samplingRate).isEqualTo(0.75) - assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) + assertThat(remoteSettings.sessionEnabled).isFalse() + assertThat(remoteSettings.samplingRate).isEqualTo(0.75) + assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) - fetchedResponse.getJSONObject("app_quality").put("sessions_enabled", true) - fetchedResponse.getJSONObject("app_quality").put("sampling_rate", 0.25) - fetchedResponse.getJSONObject("app_quality").put("session_timeout_seconds", 1200) + fetchedResponse.getJSONObject("app_quality").put("sessions_enabled", true) + fetchedResponse.getJSONObject("app_quality").put("sampling_rate", 0.25) + fetchedResponse.getJSONObject("app_quality").put("session_timeout_seconds", 1200) - // TODO(mrober): Fix these so we don't need to sleep. Maybe use FakeTime? - // Sleep for a second before updating configs - Thread.sleep(2000) + fakeTimeProvider.addInterval(31.minutes) - fakeFetcher.responseJSONObject = fetchedResponse - remoteSettings.updateSettings() + fakeFetcher.responseJSONObject = fetchedResponse + remoteSettings.updateSettings() - runCurrent() + runCurrent() - assertThat(remoteSettings.sessionEnabled).isTrue() - assertThat(remoteSettings.samplingRate).isEqualTo(0.25) - assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(20.minutes) + assertThat(remoteSettings.sessionEnabled).isTrue() + assertThat(remoteSettings.samplingRate).isEqualTo(0.25) + assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(20.minutes) - remoteSettings.clearCachedSettings() - } + remoteSettings.clearCachedSettings() + } @Test fun remoteSettings_successfulFetchWithEmptyConfigRetainsOldConfigs() = runTest(UnconfinedTestDispatcher()) { val firebaseApp = FakeFirebaseApp().firebaseApp - val context = firebaseApp.applicationContext val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcher = FakeRemoteConfigFetcher() + val fakeTimeProvider = FakeTimeProvider() val remoteSettings = buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + fakeTimeProvider, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), + FakeSettingsCache(), ) val fetchedResponse = JSONObject(VALID_RESPONSE) @@ -212,6 +184,7 @@ class RemoteSettingsTest { remoteSettings.updateSettings() runCurrent() + fakeTimeProvider.addInterval(31.seconds) assertThat(remoteSettings.sessionEnabled).isFalse() assertThat(remoteSettings.samplingRate).isEqualTo(0.75) @@ -226,6 +199,7 @@ class RemoteSettingsTest { remoteSettings.updateSettings() runCurrent() + Thread.sleep(30) assertThat(remoteSettings.sessionEnabled).isFalse() assertThat(remoteSettings.samplingRate).isEqualTo(0.75) @@ -249,7 +223,6 @@ class RemoteSettingsTest { // - Third fetch should exit even earlier, never having gone into the mutex. val firebaseApp = FakeFirebaseApp().firebaseApp - val context = firebaseApp.applicationContext val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") val fakeFetcherWithDelay = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds) @@ -260,16 +233,11 @@ class RemoteSettingsTest { val remoteSettingsWithDelay = buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + FakeTimeProvider(), firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcherWithDelay, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), + configsFetcher = fakeFetcherWithDelay, + FakeSettingsCache(), ) // Do the first fetch. This one should fetched the configsFetcher. @@ -298,8 +266,6 @@ class RemoteSettingsTest { } internal companion object { - const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test" - const val VALID_RESPONSE = """ { @@ -329,14 +295,14 @@ class RemoteSettingsTest { * the test code. */ fun buildRemoteSettings( - backgroundDispatcher: CoroutineContext, + timeProvider: TimeProvider, firebaseInstallationsApi: FirebaseInstallationsApi, appInfo: ApplicationInfo, configsFetcher: CrashlyticsSettingsFetcher, settingsCache: SettingsCache, ): RemoteSettings = RemoteSettings_Factory.create( - { backgroundDispatcher }, + { timeProvider }, { firebaseInstallationsApi }, { appInfo }, { configsFetcher }, diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt index 12f40e7cca8..f87d773b970 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt @@ -17,20 +17,18 @@ package com.google.firebase.sessions.settings import android.os.Bundle -import androidx.datastore.preferences.core.PreferenceDataStoreFactory -import androidx.datastore.preferences.preferencesDataStoreFile import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp -import com.google.firebase.concurrent.TestOnlyExecutors -import com.google.firebase.sessions.SessionDataStoreConfigs import com.google.firebase.sessions.SessionEvents +import com.google.firebase.sessions.settings.RemoteSettingsTest.Companion.buildRemoteSettings import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher +import com.google.firebase.sessions.testing.FakeSettingsCache import com.google.firebase.sessions.testing.FakeSettingsProvider +import com.google.firebase.sessions.testing.FakeTimeProvider import kotlin.time.Duration.Companion.minutes import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -107,17 +105,12 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettingsTest.buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + buildRemoteSettings( + FakeTimeProvider(), firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), + FakeSettingsCache(), ) val sessionsSettings = @@ -150,17 +143,12 @@ class SessionsSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) val remoteSettings = - RemoteSettingsTest.buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + buildRemoteSettings( + FakeTimeProvider(), firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), + FakeSettingsCache(), ) val sessionsSettings = @@ -199,17 +187,12 @@ class SessionsSettingsTest { fakeFetcher.responseJSONObject = JSONObject(invalidResponse) val remoteSettings = - RemoteSettingsTest.buildRemoteSettings( - TestOnlyExecutors.background().asCoroutineDispatcher() + coroutineContext, + buildRemoteSettings( + FakeTimeProvider(), firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), fakeFetcher, - SettingsCache( - PreferenceDataStoreFactory.create( - scope = this, - produceFile = { context.preferencesDataStoreFile(SESSION_TEST_CONFIGS_NAME) }, - ) - ), + FakeSettingsCache(), ) val sessionsSettings = @@ -229,19 +212,12 @@ class SessionsSettingsTest { remoteSettings.clearCachedSettings() } - @Test - fun sessionSettings_dataStorePreferencesNameIsFilenameSafe() { - assertThat(SessionDataStoreConfigs.SESSIONS_CONFIG_NAME).matches("^[a-zA-Z0-9_=]+\$") - } - @After fun cleanUp() { FirebaseApp.clearInstancesForTest() } private companion object { - const val SESSION_TEST_CONFIGS_NAME = "firebase_session_settings_test" - const val VALID_RESPONSE = """ { diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt index c4d35c86456..729208c33ca 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt @@ -16,30 +16,23 @@ package com.google.firebase.sessions.settings -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp -import com.google.firebase.sessions.testing.FakeFirebaseApp -import kotlinx.coroutines.ExperimentalCoroutinesApi +import com.google.firebase.sessions.testing.FakeTimeProvider +import com.google.firebase.sessions.testing.TestDataStores import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) class SettingsCacheTest { - private val Context.dataStore: DataStore by - preferencesDataStore(name = SESSION_TEST_CONFIGS_NAME) @Test fun sessionCache_returnsEmptyCache() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) assertThat(settingsCache.sessionSamplingRate()).isNull() assertThat(settingsCache.sessionsEnabled()).isNull() @@ -49,14 +42,18 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsCachedValue() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) - - settingsCache.updateSettingsEnabled(false) - settingsCache.updateSamplingRate(0.25) - settingsCache.updateSessionRestartTimeout(600) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(1000) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = false, + sessionSamplingRate = 0.25, + sessionTimeoutSeconds = 600, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 1000, + ) + ) assertThat(settingsCache.sessionsEnabled()).isFalse() assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25) @@ -69,17 +66,22 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsPreviouslyStoredValue() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) - - settingsCache.updateSettingsEnabled(false) - settingsCache.updateSamplingRate(0.25) - settingsCache.updateSessionRestartTimeout(600) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(1000) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = false, + sessionSamplingRate = 0.25, + sessionTimeoutSeconds = 600, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 1000, + ) + ) // Create a new instance to imitate a second app launch. - val newSettingsCache = SettingsCache(context.dataStore) + val newSettingsCache = + SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) assertThat(newSettingsCache.sessionsEnabled()).isFalse() assertThat(newSettingsCache.sessionSamplingRate()).isEqualTo(0.25) @@ -93,14 +95,18 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsCacheExpiredWithShortCacheDuration() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) - - settingsCache.updateSettingsEnabled(false) - settingsCache.updateSamplingRate(0.25) - settingsCache.updateSessionRestartTimeout(600) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(0) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = false, + sessionSamplingRate = 0.25, + sessionTimeoutSeconds = 600, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 0, + ) + ) assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25) assertThat(settingsCache.sessionsEnabled()).isFalse() @@ -112,13 +118,18 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsCachedValueWithPartialConfigs() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) - - settingsCache.updateSettingsEnabled(false) - settingsCache.updateSamplingRate(0.25) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(1000) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = false, + sessionSamplingRate = 0.25, + sessionTimeoutSeconds = null, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 1000, + ) + ) assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25) assertThat(settingsCache.sessionsEnabled()).isFalse() @@ -130,25 +141,33 @@ class SettingsCacheTest { @Test fun settingConfigsAllowsUpdateConfigsAndCachesValues() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) - - settingsCache.updateSettingsEnabled(false) - settingsCache.updateSamplingRate(0.25) - settingsCache.updateSessionRestartTimeout(600) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(1000) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = false, + sessionSamplingRate = 0.25, + sessionTimeoutSeconds = 600, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 1000, + ) + ) assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25) assertThat(settingsCache.sessionsEnabled()).isFalse() assertThat(settingsCache.sessionRestartTimeout()).isEqualTo(600) assertThat(settingsCache.hasCacheExpired()).isFalse() - settingsCache.updateSettingsEnabled(true) - settingsCache.updateSamplingRate(0.33) - settingsCache.updateSessionRestartTimeout(100) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(0) + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = true, + sessionSamplingRate = 0.33, + sessionTimeoutSeconds = 100, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 0, + ) + ) assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.33) assertThat(settingsCache.sessionsEnabled()).isTrue() @@ -160,25 +179,33 @@ class SettingsCacheTest { @Test fun settingConfigsCleansCacheForNullValues() = runTest { - val context = FakeFirebaseApp().firebaseApp.applicationContext - val settingsCache = SettingsCache(context.dataStore) - - settingsCache.updateSettingsEnabled(false) - settingsCache.updateSamplingRate(0.25) - settingsCache.updateSessionRestartTimeout(600) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(1000) + val fakeTimeProvider = FakeTimeProvider() + val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = false, + sessionSamplingRate = 0.25, + sessionTimeoutSeconds = 600, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 1000, + ) + ) assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.25) assertThat(settingsCache.sessionsEnabled()).isFalse() assertThat(settingsCache.sessionRestartTimeout()).isEqualTo(600) assertThat(settingsCache.hasCacheExpired()).isFalse() - settingsCache.updateSettingsEnabled(null) - settingsCache.updateSamplingRate(0.33) - settingsCache.updateSessionRestartTimeout(null) - settingsCache.updateSessionCacheUpdatedTime(System.currentTimeMillis()) - settingsCache.updateSessionCacheDuration(1000) + settingsCache.updateConfigs( + SessionConfigs( + sessionsEnabled = null, + sessionSamplingRate = 0.33, + sessionTimeoutSeconds = null, + cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheDurationSeconds = 1000, + ) + ) assertThat(settingsCache.sessionSamplingRate()).isEqualTo(0.33) assertThat(settingsCache.sessionsEnabled()).isNull() @@ -192,8 +219,4 @@ class SettingsCacheTest { fun cleanUp() { FirebaseApp.clearInstancesForTest() } - - private companion object { - const val SESSION_TEST_CONFIGS_NAME = "firebase_test_session_settings" - } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt new file mode 100644 index 00000000000..2a3e28c00b9 --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt @@ -0,0 +1,52 @@ +/* + * 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.sessions.testing + +import com.google.firebase.sessions.TimeProvider +import com.google.firebase.sessions.settings.SessionConfigs +import com.google.firebase.sessions.settings.SessionConfigsSerializer +import com.google.firebase.sessions.settings.SettingsCache + +/** Fake implementation of [SettingsCache]. */ +internal class FakeSettingsCache( + private val timeProvider: TimeProvider = FakeTimeProvider(), + private var sessionConfigs: SessionConfigs = SessionConfigsSerializer.defaultValue, +) : SettingsCache { + override fun hasCacheExpired(): Boolean { + val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs + val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds + + if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) { + val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000 + if (timeDifferenceSeconds < cacheDurationSeconds) { + return false + } + } + + return true + } + + override fun sessionsEnabled(): Boolean? = sessionConfigs.sessionsEnabled + + override fun sessionSamplingRate(): Double? = sessionConfigs.sessionSamplingRate + + override fun sessionRestartTimeout(): Int? = sessionConfigs.sessionTimeoutSeconds + + override suspend fun updateConfigs(sessionConfigs: SessionConfigs) { + this.sessionConfigs = sessionConfigs + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt index 35010de415a..295600cf48e 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeTimeProvider.kt @@ -16,17 +16,19 @@ package com.google.firebase.sessions.testing +import com.google.firebase.sessions.Time import com.google.firebase.sessions.TimeProvider -import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP_US +import com.google.firebase.sessions.testing.TestSessionEventData.TEST_SESSION_TIMESTAMP import kotlin.time.Duration -import kotlin.time.DurationUnit +import kotlin.time.DurationUnit.MILLISECONDS /** * Fake [TimeProvider] that allows programmatically elapsing time forward. * * Default [elapsedRealtime] is [Duration.ZERO] until the time is moved using [addInterval]. */ -class FakeTimeProvider(private val initialTimeUs: Long = TEST_SESSION_TIMESTAMP_US) : TimeProvider { +internal class FakeTimeProvider(private val initialTime: Time = TEST_SESSION_TIMESTAMP) : + TimeProvider { private var elapsed = Duration.ZERO fun addInterval(interval: Duration) { @@ -38,5 +40,5 @@ class FakeTimeProvider(private val initialTimeUs: Long = TEST_SESSION_TIMESTAMP_ override fun elapsedRealtime(): Duration = elapsed - override fun currentTimeUs(): Long = initialTimeUs + elapsed.toLong(DurationUnit.MICROSECONDS) + override fun currentTime(): Time = Time(ms = initialTime.ms + elapsed.toLong(MILLISECONDS)) } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt new file mode 100644 index 00000000000..d7cc3a7f67d --- /dev/null +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt @@ -0,0 +1,50 @@ +/* + * 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.sessions.testing + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider +import com.google.firebase.sessions.SessionData +import com.google.firebase.sessions.SessionDataSerializer +import com.google.firebase.sessions.settings.SessionConfigs +import com.google.firebase.sessions.settings.SessionConfigsSerializer + +/** + * Container of instances of [DataStore] for testing. + * + * Note these do not pass the test scheduler to the instances, so won't work with `runCurrent`. + */ +internal object TestDataStores { + private val appContext: Context = ApplicationProvider.getApplicationContext() + + val sessionConfigsDataStore: DataStore by lazy { + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + produceFile = { appContext.dataStoreFile("sessionConfigsTestDataStore.data") }, + ) + } + + val sessionDataStore: DataStore by lazy { + DataStoreFactory.create( + serializer = SessionDataSerializer, + produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") }, + ) + } +} diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt index 7619bc12588..105950a37f4 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestSessionEventData.kt @@ -30,23 +30,24 @@ import com.google.firebase.sessions.ProcessDetails import com.google.firebase.sessions.SessionDetails import com.google.firebase.sessions.SessionEvent import com.google.firebase.sessions.SessionInfo +import com.google.firebase.sessions.Time internal object TestSessionEventData { - const val TEST_SESSION_TIMESTAMP_US: Long = 12340000 + val TEST_SESSION_TIMESTAMP: Time = Time(ms = 12340) val TEST_SESSION_DETAILS = SessionDetails( sessionId = "a1b2c3", firstSessionId = "a1a1a1", sessionIndex = 3, - sessionStartTimestampUs = TEST_SESSION_TIMESTAMP_US + sessionStartTimestampUs = TEST_SESSION_TIMESTAMP.us, ) val TEST_DATA_COLLECTION_STATUS = DataCollectionStatus( performance = DataCollectionState.COLLECTION_SDK_NOT_INSTALLED, crashlytics = DataCollectionState.COLLECTION_SDK_NOT_INSTALLED, - sessionSamplingRate = 1.0 + sessionSamplingRate = 1.0, ) val TEST_SESSION_DATA = @@ -54,19 +55,14 @@ internal object TestSessionEventData { sessionId = "a1b2c3", firstSessionId = "a1a1a1", sessionIndex = 3, - eventTimestampUs = TEST_SESSION_TIMESTAMP_US, + eventTimestampUs = TEST_SESSION_TIMESTAMP.us, dataCollectionStatus = TEST_DATA_COLLECTION_STATUS, firebaseInstallationId = "", firebaseAuthenticationToken = "", ) val TEST_PROCESS_DETAILS = - ProcessDetails( - processName = "com.google.firebase.sessions.test", - 0, - 100, - false, - ) + ProcessDetails(processName = "com.google.firebase.sessions.test", 0, 100, false) val TEST_APP_PROCESS_DETAILS = listOf(TEST_PROCESS_DETAILS) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 44079b349e8..5fa68f6cc03 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,6 +91,7 @@ androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "card androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" } androidx-core = { module = "androidx.core:core", version = "1.13.1" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } +androidx-datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } androidx-espresso-idling-resource = { module = "androidx.test.espresso:espresso-idling-resource", version.ref = "espressoCore" } From fcd270c1f5717590a3764efa81f34302cfeecf7f Mon Sep 17 00:00:00 2001 From: Matthew Robertson Date: Mon, 24 Mar 2025 07:11:04 -0600 Subject: [PATCH 039/146] Share settings cache between running processes (#6788) With the multi-process data store change, all processes will read the settings cache from the same file safely. This means if a second process started, it would read the cache the first process persisted. But if 2 processes were already running, and one fetched and cached new settings, it wouldn't automatically share it with the other running process. This change fixes that by having all processes watch the file. Also cleaned up settings a bit, and made everything in seconds to avoid converting units. Cleaned up unit tests. Also removed the need to lazy load the cache by doing a double check in the getter instead. There is more potential to clean up, but let's do it later. --- .../sessions/FirebaseSessionsTests.kt | 34 +-- .../sessions/settings/RemoteSettings.kt | 11 +- .../sessions/settings/SessionConfigs.kt | 4 +- .../sessions/settings/SettingsCache.kt | 37 ++- .../firebase/sessions/SessionDatastoreTest.kt | 2 +- .../sessions/settings/RemoteSettingsTest.kt | 268 +++++++----------- .../sessions/settings/SessionsSettingsTest.kt | 218 +++++++------- .../sessions/settings/SettingsCacheTest.kt | 123 ++++++-- .../sessions/testing/FakeSettingsCache.kt | 6 +- .../sessions/testing/TestDataStores.kt | 50 ---- 10 files changed, 365 insertions(+), 388 deletions(-) delete mode 100644 firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt diff --git a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt index 1cf67e0c5e1..c74b6e4e329 100644 --- a/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt +++ b/firebase-sessions/src/androidTest/kotlin/com/google/firebase/sessions/FirebaseSessionsTests.kt @@ -20,12 +20,10 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.Firebase -import com.google.firebase.FirebaseApp import com.google.firebase.FirebaseOptions import com.google.firebase.initialize import com.google.firebase.sessions.settings.SessionsSettings -import org.junit.After -import org.junit.Before +import org.junit.BeforeClass import org.junit.Test import org.junit.runner.RunWith @@ -36,23 +34,6 @@ import org.junit.runner.RunWith */ @RunWith(AndroidJUnit4::class) class FirebaseSessionsTests { - @Before - fun setUp() { - Firebase.initialize( - ApplicationProvider.getApplicationContext(), - FirebaseOptions.Builder() - .setApplicationId(APP_ID) - .setApiKey(API_KEY) - .setProjectId(PROJECT_ID) - .build() - ) - } - - @After - fun cleanUp() { - FirebaseApp.clearInstancesForTest() - } - @Test fun firebaseSessionsDoesInitialize() { assertThat(FirebaseSessions.instance).isNotNull() @@ -69,5 +50,18 @@ class FirebaseSessionsTests { private const val APP_ID = "1:1:android:1a" private const val API_KEY = "API-KEY-API-KEY-API-KEY-API-KEY-API-KEY" private const val PROJECT_ID = "PROJECT-ID" + + @BeforeClass + @JvmStatic + fun setUp() { + Firebase.initialize( + ApplicationProvider.getApplicationContext(), + FirebaseOptions.Builder() + .setApplicationId(APP_ID) + .setApiKey(API_KEY) + .setProjectId(PROJECT_ID) + .build(), + ) + } } } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt index b715cd9f79c..1079577e03c 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/RemoteSettings.kt @@ -23,13 +23,11 @@ import com.google.firebase.installations.FirebaseInstallationsApi import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.InstallationId import com.google.firebase.sessions.TimeProvider -import dagger.Lazy import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.json.JSONException @@ -43,11 +41,8 @@ constructor( private val firebaseInstallationsApi: FirebaseInstallationsApi, private val appInfo: ApplicationInfo, private val configsFetcher: CrashlyticsSettingsFetcher, - private val lazySettingsCache: Lazy, + private val settingsCache: SettingsCache, ) : SettingsProvider { - private val settingsCache: SettingsCache - get() = lazySettingsCache.get() - private val fetchInProgress = Mutex() override val sessionEnabled: Boolean? @@ -133,7 +128,7 @@ constructor( sessionTimeoutSeconds = sessionTimeoutSeconds, sessionSamplingRate = sessionSamplingRate, cacheDurationSeconds = cacheDuration ?: defaultCacheDuration, - cacheUpdatedTimeMs = timeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = timeProvider.currentTime().seconds, ) ) }, @@ -148,7 +143,7 @@ constructor( override fun isSettingsStale(): Boolean = settingsCache.hasCacheExpired() @VisibleForTesting - internal fun clearCachedSettings() = runBlocking { + internal suspend fun clearCachedSettings() { settingsCache.updateConfigs(SessionConfigsSerializer.defaultValue) } diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt index 8d7e2484675..ab310ebed8a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SessionConfigs.kt @@ -30,7 +30,7 @@ internal data class SessionConfigs( val sessionSamplingRate: Double?, val sessionTimeoutSeconds: Int?, val cacheDurationSeconds: Int?, - val cacheUpdatedTimeMs: Long?, + val cacheUpdatedTimeSeconds: Long?, ) /** DataStore json [Serializer] for [SessionConfigs]. */ @@ -41,7 +41,7 @@ internal object SessionConfigsSerializer : Serializer { sessionSamplingRate = null, sessionTimeoutSeconds = null, cacheDurationSeconds = null, - cacheUpdatedTimeMs = null, + cacheUpdatedTimeSeconds = null, ) override suspend fun readFrom(input: InputStream): SessionConfigs = diff --git a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt index 468bbad6b7a..1640a5c7b7a 100644 --- a/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt +++ b/firebase-sessions/src/main/kotlin/com/google/firebase/sessions/settings/SettingsCache.kt @@ -17,12 +17,18 @@ package com.google.firebase.sessions.settings import android.util.Log +import androidx.annotation.VisibleForTesting import androidx.datastore.core.DataStore +import com.google.firebase.annotations.concurrent.Background import com.google.firebase.sessions.TimeProvider import java.io.IOException +import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import javax.inject.Singleton +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking internal interface SettingsCache { @@ -41,23 +47,38 @@ internal interface SettingsCache { internal class SettingsCacheImpl @Inject constructor( + @Background private val backgroundDispatcher: CoroutineContext, private val timeProvider: TimeProvider, private val sessionConfigsDataStore: DataStore, ) : SettingsCache { - private var sessionConfigs: SessionConfigs + private val sessionConfigsAtomicReference = AtomicReference() + + private val sessionConfigs: SessionConfigs + get() { + // Ensure configs are loaded from disk before the first access + if (sessionConfigsAtomicReference.get() == null) { + // Double check to avoid the `runBlocking` unless necessary + sessionConfigsAtomicReference.compareAndSet( + null, + runBlocking { sessionConfigsDataStore.data.first() }, + ) + } + + return sessionConfigsAtomicReference.get() + } init { - // Block until the cache is loaded from disk to ensure cache - // values are valid and readable from the main thread on init. - runBlocking { sessionConfigs = sessionConfigsDataStore.data.first() } + CoroutineScope(backgroundDispatcher).launch { + sessionConfigsDataStore.data.collect(sessionConfigsAtomicReference::set) + } } override fun hasCacheExpired(): Boolean { - val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs + val cacheUpdatedTimeSeconds = sessionConfigs.cacheUpdatedTimeSeconds val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds - if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) { - val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000 + if (cacheUpdatedTimeSeconds != null && cacheDurationSeconds != null) { + val timeDifferenceSeconds = timeProvider.currentTime().seconds - cacheUpdatedTimeSeconds if (timeDifferenceSeconds < cacheDurationSeconds) { return false } @@ -74,12 +95,12 @@ constructor( override suspend fun updateConfigs(sessionConfigs: SessionConfigs) { try { sessionConfigsDataStore.updateData { sessionConfigs } - this.sessionConfigs = sessionConfigs } catch (ex: IOException) { Log.w(TAG, "Failed to update config values: $ex") } } + @VisibleForTesting internal suspend fun removeConfigs() = try { sessionConfigsDataStore.updateData { SessionConfigsSerializer.defaultValue } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt index 7e94eb3113e..efe7bb27a97 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/SessionDatastoreTest.kt @@ -44,7 +44,7 @@ class SessionDatastoreTest { DataStoreFactory.create( serializer = SessionDataSerializer, scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), - produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") }, + produceFile = { appContext.dataStoreFile("sessionDataStore.data") }, ), ) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt index ccaf4f8954d..74df328ae57 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/RemoteSettingsTest.kt @@ -19,10 +19,7 @@ package com.google.firebase.sessions.settings import androidx.test.ext.junit.runners.AndroidJUnit4 import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp -import com.google.firebase.installations.FirebaseInstallationsApi -import com.google.firebase.sessions.ApplicationInfo import com.google.firebase.sessions.SessionEvents -import com.google.firebase.sessions.TimeProvider import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher @@ -31,11 +28,8 @@ import com.google.firebase.sessions.testing.FakeTimeProvider import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeout import org.json.JSONObject @@ -43,43 +37,37 @@ import org.junit.After import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) class RemoteSettingsTest { @Test - fun remoteSettings_successfulFetchCachesValues() = - runTest(UnconfinedTestDispatcher()) { - val firebaseApp = FakeFirebaseApp().firebaseApp - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher() - - val remoteSettings = - buildRemoteSettings( - FakeTimeProvider(), - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - FakeSettingsCache(), - ) - - runCurrent() + fun remoteSettings_successfulFetchCachesValues() = runTest { + val firebaseApp = FakeFirebaseApp().firebaseApp + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher() - assertThat(remoteSettings.sessionEnabled).isNull() - assertThat(remoteSettings.samplingRate).isNull() - assertThat(remoteSettings.sessionRestartTimeout).isNull() + val remoteSettings = + RemoteSettings( + FakeTimeProvider(), + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(), + ) - fakeFetcher.responseJSONObject = JSONObject(VALID_RESPONSE) - remoteSettings.updateSettings() + assertThat(remoteSettings.sessionEnabled).isNull() + assertThat(remoteSettings.samplingRate).isNull() + assertThat(remoteSettings.sessionRestartTimeout).isNull() - runCurrent() + fakeFetcher.responseJSONObject = JSONObject(VALID_RESPONSE) + remoteSettings.updateSettings() - assertThat(remoteSettings.sessionEnabled).isFalse() - assertThat(remoteSettings.samplingRate).isEqualTo(0.75) - assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) + assertThat(remoteSettings.sessionEnabled).isFalse() + assertThat(remoteSettings.samplingRate).isEqualTo(0.75) + assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) - remoteSettings.clearCachedSettings() - } + remoteSettings.clearCachedSettings() + } @Test fun remoteSettings_successfulFetchWithLessConfigsCachesOnlyReceivedValues() = runTest { @@ -88,7 +76,7 @@ class RemoteSettingsTest { val fakeFetcher = FakeRemoteConfigFetcher() val remoteSettings = - buildRemoteSettings( + RemoteSettings( FakeTimeProvider(), firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), @@ -96,8 +84,6 @@ class RemoteSettingsTest { FakeSettingsCache(), ) - runCurrent() - assertThat(remoteSettings.sessionEnabled).isNull() assertThat(remoteSettings.samplingRate).isNull() assertThat(remoteSettings.sessionRestartTimeout).isNull() @@ -107,8 +93,6 @@ class RemoteSettingsTest { fakeFetcher.responseJSONObject = fetchedResponse remoteSettings.updateSettings() - runCurrent() - assertThat(remoteSettings.sessionEnabled).isNull() assertThat(remoteSettings.samplingRate).isEqualTo(0.75) assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) @@ -124,7 +108,7 @@ class RemoteSettingsTest { val fakeTimeProvider = FakeTimeProvider() val remoteSettings = - buildRemoteSettings( + RemoteSettings( fakeTimeProvider, firebaseInstallations, SessionEvents.getApplicationInfo(firebaseApp), @@ -137,8 +121,6 @@ class RemoteSettingsTest { fakeFetcher.responseJSONObject = fetchedResponse remoteSettings.updateSettings() - runCurrent() - assertThat(remoteSettings.sessionEnabled).isFalse() assertThat(remoteSettings.samplingRate).isEqualTo(0.75) assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) @@ -152,8 +134,6 @@ class RemoteSettingsTest { fakeFetcher.responseJSONObject = fetchedResponse remoteSettings.updateSettings() - runCurrent() - assertThat(remoteSettings.sessionEnabled).isTrue() assertThat(remoteSettings.samplingRate).isEqualTo(0.25) assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(20.minutes) @@ -162,110 +142,99 @@ class RemoteSettingsTest { } @Test - fun remoteSettings_successfulFetchWithEmptyConfigRetainsOldConfigs() = - runTest(UnconfinedTestDispatcher()) { - val firebaseApp = FakeFirebaseApp().firebaseApp - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher() - val fakeTimeProvider = FakeTimeProvider() - - val remoteSettings = - buildRemoteSettings( - fakeTimeProvider, - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - FakeSettingsCache(), - ) - - val fetchedResponse = JSONObject(VALID_RESPONSE) - fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1) - fakeFetcher.responseJSONObject = fetchedResponse - remoteSettings.updateSettings() - - runCurrent() - fakeTimeProvider.addInterval(31.seconds) - - assertThat(remoteSettings.sessionEnabled).isFalse() - assertThat(remoteSettings.samplingRate).isEqualTo(0.75) - assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) - - fetchedResponse.remove("app_quality") - - // Sleep for a second before updating configs - Thread.sleep(2000) - - fakeFetcher.responseJSONObject = fetchedResponse - remoteSettings.updateSettings() - - runCurrent() - Thread.sleep(30) - - assertThat(remoteSettings.sessionEnabled).isFalse() - assertThat(remoteSettings.samplingRate).isEqualTo(0.75) - assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) - - remoteSettings.clearCachedSettings() - } + fun remoteSettings_successfulFetchWithEmptyConfigRetainsOldConfigs() = runTest { + val firebaseApp = FakeFirebaseApp().firebaseApp + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher() + val fakeTimeProvider = FakeTimeProvider() + + val remoteSettings = + RemoteSettings( + fakeTimeProvider, + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(), + ) + + val fetchedResponse = JSONObject(VALID_RESPONSE) + fetchedResponse.getJSONObject("app_quality").put("cache_duration", 1) + fakeFetcher.responseJSONObject = fetchedResponse + remoteSettings.updateSettings() + + fakeTimeProvider.addInterval(31.seconds) + + assertThat(remoteSettings.sessionEnabled).isFalse() + assertThat(remoteSettings.samplingRate).isEqualTo(0.75) + assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) + + fetchedResponse.remove("app_quality") + + fakeFetcher.responseJSONObject = fetchedResponse + remoteSettings.updateSettings() + + assertThat(remoteSettings.sessionEnabled).isFalse() + assertThat(remoteSettings.samplingRate).isEqualTo(0.75) + assertThat(remoteSettings.sessionRestartTimeout).isEqualTo(40.minutes) + + remoteSettings.clearCachedSettings() + } @Test - fun remoteSettings_fetchWhileFetchInProgress() = - runTest(UnconfinedTestDispatcher()) { - // This test does: - // 1. Do a fetch with a fake fetcher that will block for 3 seconds. - // 2. While that is happening, do a second fetch. - // - First fetch is still fetching, so second fetch should fall through to the mutex. - // - Second fetch will be blocked until first completes. - // - First fetch returns, should unblock the second fetch. - // - Second fetch should go into mutex, sees cache is valid in "double check," exist early. - // 3. After a fetch completes, do a third fetch. - // - First fetch should have have updated the cache. - // - Third fetch should exit even earlier, never having gone into the mutex. - - val firebaseApp = FakeFirebaseApp().firebaseApp - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcherWithDelay = - FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds) - - fakeFetcherWithDelay.responseJSONObject - .getJSONObject("app_quality") - .put("sampling_rate", 0.125) - - val remoteSettingsWithDelay = - buildRemoteSettings( - FakeTimeProvider(), - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - configsFetcher = fakeFetcherWithDelay, - FakeSettingsCache(), - ) - - // Do the first fetch. This one should fetched the configsFetcher. - val firstFetch = launch(Dispatchers.Default) { remoteSettingsWithDelay.updateSettings() } - - // Wait a second, and then do the second fetch while first is still running. - // This one should block until the first fetch completes, but then exit early. - launch(Dispatchers.Default) { - delay(1.seconds) - remoteSettingsWithDelay.updateSettings() - } + fun remoteSettings_fetchWhileFetchInProgress() = runTest { + // This test does: + // 1. Do a fetch with a fake fetcher that will block for 3 seconds. + // 2. While that is happening, do a second fetch. + // - First fetch is still fetching, so second fetch should fall through to the mutex. + // - Second fetch will be blocked until first completes. + // - First fetch returns, should unblock the second fetch. + // - Second fetch should go into mutex, sees cache is valid in "double check," exist early. + // 3. After a fetch completes, do a third fetch. + // - First fetch should have have updated the cache. + // - Third fetch should exit even earlier, never having gone into the mutex. + + val firebaseApp = FakeFirebaseApp().firebaseApp + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcherWithDelay = + FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE), networkDelay = 3.seconds) + + fakeFetcherWithDelay.responseJSONObject.getJSONObject("app_quality").put("sampling_rate", 0.125) + + val remoteSettingsWithDelay = + RemoteSettings( + FakeTimeProvider(), + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + configsFetcher = fakeFetcherWithDelay, + FakeSettingsCache(), + ) - // Wait until the first fetch is done, then do a third fetch. - // This one should not even block, and exit early. - firstFetch.join() - withTimeout(1.seconds) { remoteSettingsWithDelay.updateSettings() } + // Do the first fetch. This one should fetched the configsFetcher. + val firstFetch = launch(Dispatchers.Default) { remoteSettingsWithDelay.updateSettings() } - // Assert that the configsFetcher was fetched exactly once. - assertThat(fakeFetcherWithDelay.timesCalled).isEqualTo(1) - assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125) + // Wait a second, and then do the second fetch while first is still running. + // This one should block until the first fetch completes, but then exit early. + launch(Dispatchers.Default) { + delay(1.seconds) + remoteSettingsWithDelay.updateSettings() } + // Wait until the first fetch is done, then do a third fetch. + // This one should not even block, and exit early. + firstFetch.join() + withTimeout(1.seconds) { remoteSettingsWithDelay.updateSettings() } + + // Assert that the configsFetcher was fetched exactly once. + assertThat(fakeFetcherWithDelay.timesCalled).isEqualTo(1) + assertThat(remoteSettingsWithDelay.samplingRate).isEqualTo(0.125) + } + @After fun cleanUp() { FirebaseApp.clearInstancesForTest() } - internal companion object { + private companion object { const val VALID_RESPONSE = """ { @@ -284,30 +253,5 @@ class RemoteSettingsTest { } } """ - - /** - * Build an instance of [RemoteSettings] using the Dagger factory. - * - * This is needed because the SDK vendors Dagger to a difference namespace, but it does not for - * these unit tests. The [RemoteSettings.lazySettingsCache] has type [dagger.Lazy] in these - * tests, but type `com.google.firebase.sessions.dagger.Lazy` in the SDK. This method to build - * the instance is the easiest I could find that does not need any reference to [dagger.Lazy] in - * the test code. - */ - fun buildRemoteSettings( - timeProvider: TimeProvider, - firebaseInstallationsApi: FirebaseInstallationsApi, - appInfo: ApplicationInfo, - configsFetcher: CrashlyticsSettingsFetcher, - settingsCache: SettingsCache, - ): RemoteSettings = - RemoteSettings_Factory.create( - { timeProvider }, - { firebaseInstallationsApi }, - { appInfo }, - { configsFetcher }, - { settingsCache }, - ) - .get() } } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt index f87d773b970..146857ae7f4 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SessionsSettingsTest.kt @@ -20,7 +20,6 @@ import android.os.Bundle import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.sessions.SessionEvents -import com.google.firebase.sessions.settings.RemoteSettingsTest.Companion.buildRemoteSettings import com.google.firebase.sessions.testing.FakeFirebaseApp import com.google.firebase.sessions.testing.FakeFirebaseInstallations import com.google.firebase.sessions.testing.FakeRemoteConfigFetcher @@ -28,9 +27,6 @@ import com.google.firebase.sessions.testing.FakeSettingsCache import com.google.firebase.sessions.testing.FakeSettingsProvider import com.google.firebase.sessions.testing.FakeTimeProvider import kotlin.time.Duration.Companion.minutes -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.json.JSONObject import org.junit.After @@ -38,7 +34,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner -@OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) class SessionsSettingsTest { @@ -89,128 +84,117 @@ class SessionsSettingsTest { remoteSettings = FakeSettingsProvider(), ) - runCurrent() - assertThat(sessionsSettings.sessionsEnabled).isFalse() assertThat(sessionsSettings.samplingRate).isEqualTo(0.5) assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(30.minutes) } @Test - fun sessionSettings_remoteSettingsOverrideDefaultsWhenPresent() = - runTest(UnconfinedTestDispatcher()) { - val firebaseApp = FakeFirebaseApp().firebaseApp - val context = firebaseApp.applicationContext - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) - - val remoteSettings = - buildRemoteSettings( - FakeTimeProvider(), - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - FakeSettingsCache(), - ) - - val sessionsSettings = - SessionsSettings( - localOverrideSettings = LocalOverrideSettings(context), - remoteSettings = remoteSettings, - ) - - sessionsSettings.updateSettings() - - runCurrent() - - assertThat(sessionsSettings.sessionsEnabled).isFalse() - assertThat(sessionsSettings.samplingRate).isEqualTo(0.75) - assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes) - - remoteSettings.clearCachedSettings() - } + fun sessionSettings_remoteSettingsOverrideDefaultsWhenPresent() = runTest { + val firebaseApp = FakeFirebaseApp().firebaseApp + val context = firebaseApp.applicationContext + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) + + val remoteSettings = + RemoteSettings( + FakeTimeProvider(), + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(), + ) + + val sessionsSettings = + SessionsSettings( + localOverrideSettings = LocalOverrideSettings(context), + remoteSettings = remoteSettings, + ) + + sessionsSettings.updateSettings() + + assertThat(sessionsSettings.sessionsEnabled).isFalse() + assertThat(sessionsSettings.samplingRate).isEqualTo(0.75) + assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes) + + remoteSettings.clearCachedSettings() + } @Test - fun sessionSettings_manifestOverridesRemoteSettingsAndDefaultsWhenPresent() = - runTest(UnconfinedTestDispatcher()) { - val metadata = Bundle() - metadata.putBoolean("firebase_sessions_enabled", true) - metadata.putDouble("firebase_sessions_sampling_rate", 0.5) - metadata.putInt("firebase_sessions_sessions_restart_timeout", 180) - val firebaseApp = FakeFirebaseApp(metadata).firebaseApp - val context = firebaseApp.applicationContext - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) - - val remoteSettings = - buildRemoteSettings( - FakeTimeProvider(), - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - FakeSettingsCache(), - ) - - val sessionsSettings = - SessionsSettings( - localOverrideSettings = LocalOverrideSettings(context), - remoteSettings = remoteSettings, - ) - - sessionsSettings.updateSettings() - - runCurrent() - - assertThat(sessionsSettings.sessionsEnabled).isTrue() - assertThat(sessionsSettings.samplingRate).isEqualTo(0.5) - assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(3.minutes) - - remoteSettings.clearCachedSettings() - } + fun sessionSettings_manifestOverridesRemoteSettingsAndDefaultsWhenPresent() = runTest { + val metadata = Bundle() + metadata.putBoolean("firebase_sessions_enabled", true) + metadata.putDouble("firebase_sessions_sampling_rate", 0.5) + metadata.putInt("firebase_sessions_sessions_restart_timeout", 180) + val firebaseApp = FakeFirebaseApp(metadata).firebaseApp + val context = firebaseApp.applicationContext + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher(JSONObject(VALID_RESPONSE)) + + val remoteSettings = + RemoteSettings( + FakeTimeProvider(), + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(), + ) + + val sessionsSettings = + SessionsSettings( + localOverrideSettings = LocalOverrideSettings(context), + remoteSettings = remoteSettings, + ) + + sessionsSettings.updateSettings() + + assertThat(sessionsSettings.sessionsEnabled).isTrue() + assertThat(sessionsSettings.samplingRate).isEqualTo(0.5) + assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(3.minutes) + + remoteSettings.clearCachedSettings() + } @Test - fun sessionSettings_invalidManifestConfigsDoNotOverride() = - runTest(UnconfinedTestDispatcher()) { - val metadata = Bundle() - metadata.putBoolean("firebase_sessions_enabled", false) - metadata.putDouble("firebase_sessions_sampling_rate", -0.2) // Invalid - metadata.putInt("firebase_sessions_sessions_restart_timeout", -2) // Invalid - val firebaseApp = FakeFirebaseApp(metadata).firebaseApp - val context = firebaseApp.applicationContext - val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") - val fakeFetcher = FakeRemoteConfigFetcher() - val invalidResponse = - VALID_RESPONSE.replace( - "\"sampling_rate\":0.75,", - "\"sampling_rate\":1.2,", // Invalid - ) - fakeFetcher.responseJSONObject = JSONObject(invalidResponse) - - val remoteSettings = - buildRemoteSettings( - FakeTimeProvider(), - firebaseInstallations, - SessionEvents.getApplicationInfo(firebaseApp), - fakeFetcher, - FakeSettingsCache(), - ) - - val sessionsSettings = - SessionsSettings( - localOverrideSettings = LocalOverrideSettings(context), - remoteSettings = remoteSettings, - ) - - sessionsSettings.updateSettings() - - runCurrent() - - assertThat(sessionsSettings.sessionsEnabled).isFalse() // Manifest - assertThat(sessionsSettings.samplingRate).isEqualTo(1.0) // SDK default - assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes) // Remote - - remoteSettings.clearCachedSettings() - } + fun sessionSettings_invalidManifestConfigsDoNotOverride() = runTest { + val metadata = Bundle() + metadata.putBoolean("firebase_sessions_enabled", false) + metadata.putDouble("firebase_sessions_sampling_rate", -0.2) // Invalid + metadata.putInt("firebase_sessions_sessions_restart_timeout", -2) // Invalid + val firebaseApp = FakeFirebaseApp(metadata).firebaseApp + val context = firebaseApp.applicationContext + val firebaseInstallations = FakeFirebaseInstallations("FaKeFiD") + val fakeFetcher = FakeRemoteConfigFetcher() + val invalidResponse = + VALID_RESPONSE.replace( + "\"sampling_rate\":0.75,", + "\"sampling_rate\":1.2,", // Invalid + ) + fakeFetcher.responseJSONObject = JSONObject(invalidResponse) + + val remoteSettings = + RemoteSettings( + FakeTimeProvider(), + firebaseInstallations, + SessionEvents.getApplicationInfo(firebaseApp), + fakeFetcher, + FakeSettingsCache(), + ) + + val sessionsSettings = + SessionsSettings( + localOverrideSettings = LocalOverrideSettings(context), + remoteSettings = remoteSettings, + ) + + sessionsSettings.updateSettings() + + assertThat(sessionsSettings.sessionsEnabled).isFalse() // Manifest + assertThat(sessionsSettings.samplingRate).isEqualTo(1.0) // SDK default + assertThat(sessionsSettings.sessionRestartTimeout).isEqualTo(40.minutes) // Remote + + remoteSettings.clearCachedSettings() + } @After fun cleanUp() { diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt index 729208c33ca..a8d8429b5a8 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/settings/SettingsCacheTest.kt @@ -16,23 +16,44 @@ package com.google.firebase.sessions.settings +import android.content.Context +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.dataStoreFile +import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import com.google.firebase.FirebaseApp import com.google.firebase.sessions.testing.FakeTimeProvider -import com.google.firebase.sessions.testing.TestDataStores +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +@OptIn(ExperimentalCoroutinesApi::class) @RunWith(RobolectricTestRunner::class) class SettingsCacheTest { + private val appContext: Context = ApplicationProvider.getApplicationContext() @Test fun sessionCache_returnsEmptyCache() = runTest { val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ), + ) + + runCurrent() assertThat(settingsCache.sessionSamplingRate()).isNull() assertThat(settingsCache.sessionsEnabled()).isNull() @@ -43,14 +64,24 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsCachedValue() = runTest { val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ), + ) settingsCache.updateConfigs( SessionConfigs( sessionsEnabled = false, sessionSamplingRate = 0.25, sessionTimeoutSeconds = 600, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 1000, ) ) @@ -66,22 +97,40 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsPreviouslyStoredValue() = runTest { + val sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ) + val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = sessionConfigsDataStore, + ) settingsCache.updateConfigs( SessionConfigs( sessionsEnabled = false, sessionSamplingRate = 0.25, sessionTimeoutSeconds = 600, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 1000, ) ) // Create a new instance to imitate a second app launch. val newSettingsCache = - SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = sessionConfigsDataStore, + ) + + runCurrent() assertThat(newSettingsCache.sessionsEnabled()).isFalse() assertThat(newSettingsCache.sessionSamplingRate()).isEqualTo(0.25) @@ -96,14 +145,24 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsCacheExpiredWithShortCacheDuration() = runTest { val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ), + ) settingsCache.updateConfigs( SessionConfigs( sessionsEnabled = false, sessionSamplingRate = 0.25, sessionTimeoutSeconds = 600, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 0, ) ) @@ -119,14 +178,24 @@ class SettingsCacheTest { @Test fun settingConfigsReturnsCachedValueWithPartialConfigs() = runTest { val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ), + ) settingsCache.updateConfigs( SessionConfigs( sessionsEnabled = false, sessionSamplingRate = 0.25, sessionTimeoutSeconds = null, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 1000, ) ) @@ -142,14 +211,24 @@ class SettingsCacheTest { @Test fun settingConfigsAllowsUpdateConfigsAndCachesValues() = runTest { val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ), + ) settingsCache.updateConfigs( SessionConfigs( sessionsEnabled = false, sessionSamplingRate = 0.25, sessionTimeoutSeconds = 600, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 1000, ) ) @@ -164,7 +243,7 @@ class SettingsCacheTest { sessionsEnabled = true, sessionSamplingRate = 0.33, sessionTimeoutSeconds = 100, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 0, ) ) @@ -180,14 +259,24 @@ class SettingsCacheTest { @Test fun settingConfigsCleansCacheForNullValues() = runTest { val fakeTimeProvider = FakeTimeProvider() - val settingsCache = SettingsCacheImpl(fakeTimeProvider, TestDataStores.sessionConfigsDataStore) + val settingsCache = + SettingsCacheImpl( + backgroundDispatcher = StandardTestDispatcher(testScheduler, "background"), + timeProvider = fakeTimeProvider, + sessionConfigsDataStore = + DataStoreFactory.create( + serializer = SessionConfigsSerializer, + scope = CoroutineScope(StandardTestDispatcher(testScheduler, "blocking")), + produceFile = { appContext.dataStoreFile("sessionConfigsDataStore.data") }, + ), + ) settingsCache.updateConfigs( SessionConfigs( sessionsEnabled = false, sessionSamplingRate = 0.25, sessionTimeoutSeconds = 600, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 1000, ) ) @@ -202,7 +291,7 @@ class SettingsCacheTest { sessionsEnabled = null, sessionSamplingRate = 0.33, sessionTimeoutSeconds = null, - cacheUpdatedTimeMs = fakeTimeProvider.currentTime().ms, + cacheUpdatedTimeSeconds = fakeTimeProvider.currentTime().seconds, cacheDurationSeconds = 1000, ) ) diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt index 2a3e28c00b9..2c58ef22d7d 100644 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt +++ b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/FakeSettingsCache.kt @@ -27,11 +27,11 @@ internal class FakeSettingsCache( private var sessionConfigs: SessionConfigs = SessionConfigsSerializer.defaultValue, ) : SettingsCache { override fun hasCacheExpired(): Boolean { - val cacheUpdatedTimeMs = sessionConfigs.cacheUpdatedTimeMs + val cacheUpdatedTimeSeconds = sessionConfigs.cacheUpdatedTimeSeconds val cacheDurationSeconds = sessionConfigs.cacheDurationSeconds - if (cacheUpdatedTimeMs != null && cacheDurationSeconds != null) { - val timeDifferenceSeconds = (timeProvider.currentTime().ms - cacheUpdatedTimeMs) / 1000 + if (cacheUpdatedTimeSeconds != null && cacheDurationSeconds != null) { + val timeDifferenceSeconds = timeProvider.currentTime().seconds - cacheUpdatedTimeSeconds if (timeDifferenceSeconds < cacheDurationSeconds) { return false } diff --git a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt b/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt deleted file mode 100644 index d7cc3a7f67d..00000000000 --- a/firebase-sessions/src/test/kotlin/com/google/firebase/sessions/testing/TestDataStores.kt +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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.sessions.testing - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.core.DataStoreFactory -import androidx.datastore.dataStoreFile -import androidx.test.core.app.ApplicationProvider -import com.google.firebase.sessions.SessionData -import com.google.firebase.sessions.SessionDataSerializer -import com.google.firebase.sessions.settings.SessionConfigs -import com.google.firebase.sessions.settings.SessionConfigsSerializer - -/** - * Container of instances of [DataStore] for testing. - * - * Note these do not pass the test scheduler to the instances, so won't work with `runCurrent`. - */ -internal object TestDataStores { - private val appContext: Context = ApplicationProvider.getApplicationContext() - - val sessionConfigsDataStore: DataStore by lazy { - DataStoreFactory.create( - serializer = SessionConfigsSerializer, - produceFile = { appContext.dataStoreFile("sessionConfigsTestDataStore.data") }, - ) - } - - val sessionDataStore: DataStore by lazy { - DataStoreFactory.create( - serializer = SessionDataSerializer, - produceFile = { appContext.dataStoreFile("sessionDataTestDataStore.data") }, - ) - } -} From c726a1a0c63f47f99be0d51f23157668d4817280 Mon Sep 17 00:00:00 2001 From: Daymon <17409137+daymxn@users.noreply.github.com> Date: Thu, 6 Feb 2025 17:06:40 -0600 Subject: [PATCH 040/146] Fix ModuleVersion bumping (#6679) Per [b/394908865](https://b.corp.google.com/issues/394908865), This fixes an issue where `ModuleVersion.bump()` was not properly resetting the smaller version types. Additionally, this fixes some other minor issues with bom generation. Namely, this PR also fixes: - [b/394908773](https://b.corp.google.com/issues/394908773) -> Fix bom release note ordering - [b/394909103](https://b.corp.google.com/issues/394909103) -> Separate published bom artifacts --- .github/workflows/make-bom.yml | 36 ++++++---- .../GenerateBomReleaseNotesTask.kt | 7 +- .../gradle/bomgenerator/GenerateBomTask.kt | 4 +- .../gradle/plugins/KotlinExtensions.kt | 37 ++++++++++ .../firebase/gradle/plugins/ModuleVersion.kt | 7 +- .../gradle/plugins/PublishingPlugin.kt | 16 ++++- .../gradle/plugins/datamodels/PomElement.kt | 24 +++++-- .../plugins/GenerateBomReleaseNotesTests.kt | 69 +++++++++++++++++-- .../gradle/plugins/GenerateBomTests.kt | 4 +- .../gradle/plugins/ModuleVersionTests.kt | 10 +++ 10 files changed, 176 insertions(+), 38 deletions(-) diff --git a/.github/workflows/make-bom.yml b/.github/workflows/make-bom.yml index 0ad2ecf4add..0e7d63f5c96 100644 --- a/.github/workflows/make-bom.yml +++ b/.github/workflows/make-bom.yml @@ -11,7 +11,9 @@ jobs: uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 with: python-version: '3.10' + - uses: actions/checkout@v4.1.1 + - name: Set up JDK 17 uses: actions/setup-java@v4.1.0 with: @@ -21,19 +23,25 @@ jobs: - name: Build run: | - ./ci/run.sh \ - --artifact-target-dir=./logs/artifacts \ - --artifact-patterns=bom.zip \ - --artifact-patterns=bomReleaseNotes.md \ - --artifact-patterns=recipeVersionUpdate.txt \ - gradle \ - -- \ - --build-cache \ - buildBomZip - - - name: Upload generated artifacts + ./gradlew buildBomBundleZip + + - name: Upload bom + uses: actions/upload-artifact@v4.3.3 + with: + name: bom + path: build/bom/ + retention-days: 15 + + - name: Upload release notes + uses: actions/upload-artifact@v4.3.3 + with: + name: bom_release_notes + path: build/bomReleaseNotes.md + retention-days: 15 + + - name: Upload recipe version update uses: actions/upload-artifact@v4.3.3 with: - name: artifacts - path: ./logs/artifacts/ - retention-days: 5 + name: recipe_version + path: build/recipeVersionUpdate.txt + retention-days: 15 diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt index 72fe51a8017..75bb991dd60 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomReleaseNotesTask.kt @@ -51,7 +51,7 @@ abstract class GenerateBomReleaseNotesTask : DefaultTask() { val previousDeps = previousBom.get().dependencyManagement?.dependencies.orEmpty() previousBomVersions.set(previousDeps.associate { it.fullArtifactName to it.version }) - val sortedDependencies = currentDeps.sortedBy { it.version } + val sortedDependencies = currentDeps.sortedBy { it.toString() } val headingId = "{: #bom_v${bom.version.replace(".", "-")}}" @@ -71,8 +71,9 @@ abstract class GenerateBomReleaseNotesTask : DefaultTask() { | Firebase Android SDKs mapped to this {{bom}} version |

|

- | Libraries that were versioned with this release are in highlighted rows.
- | Refer to a library's release notes (on this page) for details about its changes. + | Libraries that were versioned with this release are in highlighted rows. + |
Refer to a library's release notes (on this page) for details about its + | changes. |

| | diff --git a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt index 8e5599a530a..9fb7fe35130 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/bomgenerator/GenerateBomTask.kt @@ -25,8 +25,8 @@ import com.google.firebase.gradle.plugins.datamodels.LicenseElement import com.google.firebase.gradle.plugins.datamodels.PomElement import com.google.firebase.gradle.plugins.datamodels.fullArtifactName import com.google.firebase.gradle.plugins.datamodels.moduleVersion -import com.google.firebase.gradle.plugins.diff import com.google.firebase.gradle.plugins.orEmpty +import com.google.firebase.gradle.plugins.pairBy import com.google.firebase.gradle.plugins.partitionNotNull import com.google.firebase.gradle.plugins.services.GMavenService import org.gradle.api.DefaultTask @@ -144,7 +144,7 @@ abstract class GenerateBomTask : DefaultTask() { val oldBomVersion = ModuleVersion.fromString(oldBom.artifactId, oldBom.version) val oldBomDependencies = oldBom.dependencyManagement?.dependencies.orEmpty() - val changedDependencies = oldBomDependencies.diff(releasingDependencies) + val changedDependencies = oldBomDependencies.pairBy(releasingDependencies) { it.artifactId } val versionBumps = changedDependencies.mapNotNull { (old, new) -> diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt index 8b7634f4500..8f058603e3b 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/KotlinExtensions.kt @@ -296,6 +296,43 @@ infix fun List.diff(other: List): List> { return firstList.zip(secondList).filter { it.first != it.second } } +/** + * Creates a list of pairs between two lists, matching according to the provided [mapper]. + * + * ```kotlin + * data class Person(name: String, age: Int) + * + * val firstList = listOf( + * Person("Mike", 5), + * Person("Rachel", 6) + * ) + * + * val secondList = listOf( + * Person("Michael", 4), + * Person("Mike", 1) + * ) + * + * val diffList = firstList.pairBy(secondList) { + * it.name + * } + * + * diffList shouldBeEqualTo listOf( + * Person("Mike", 5) to Person("Mike", 1) + * Person("Rachel", 6) to null + * null to Person("Mike", 1) + * ) + * ``` + */ +inline fun List.pairBy(other: List, mapper: (T) -> R): List> { + val firstMap = associateBy { mapper(it) } + val secondMap = other.associateBy { mapper(it) } + + val changedOrRemoved = firstMap.map { it.value to secondMap[it.key] } + val added = secondMap.filterKeys { it !in firstMap }.map { null to it.value } + + return changedOrRemoved + added +} + /** * Creates a list that is forced to certain size. * diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt index c07b382aac6..3479c536699 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/ModuleVersion.kt @@ -269,9 +269,10 @@ data class ModuleVersion( .let { it ?: if (pre != null) VersionType.PRE else VersionType.PATCH } .let { when (it) { - VersionType.MAJOR -> copy(major = major + 1) - VersionType.MINOR -> copy(minor = minor + 1) - VersionType.PATCH -> copy(patch = patch + 1) + VersionType.MAJOR -> + copy(major = major + 1, minor = 0, patch = 0, pre = pre?.copy(build = 1)) + VersionType.MINOR -> copy(minor = minor + 1, patch = 0, pre = pre?.copy(build = 1)) + VersionType.PATCH -> copy(patch = patch + 1, pre = pre?.copy(build = 1)) VersionType.PRE -> copy(pre = pre?.bump()) } } diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt index b698dc08ad8..82a2aaae858 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/PublishingPlugin.kt @@ -61,6 +61,8 @@ import org.gradle.kotlin.dsl.register * outside of the standard [FIREBASE_PUBLISH_TASK] workflow (possibly at a later time in the release * cycle): * - [BUILD_BOM_ZIP_TASK] -> Creates a zip file of the contents of [GENERATE_BOM_TASK] + * [registerGenerateBomTask] + * - [BUILD_BOM_BUNDLE_ZIP_TASK] -> Creates a zip file of the contents of [BUILD_BOM_ZIP_TASK] * [registerGenerateBomTask], * [GENERATE_BOM_RELEASE_NOTES_TASK][registerGenerateBomReleaseNotesTask] and * [GENERATE_TUTORIAL_BUNDLE_TASK][registerGenerateTutorialBundleTask] @@ -140,9 +142,16 @@ abstract class PublishingPlugin : Plugin { destinationDirectory.set(project.layout.buildDirectory) } - project.tasks.register(BUILD_BOM_ZIP_TASK) { - from(generateBom, generateBomReleaseNotes, generateTutorialBundle) - archiveFileName.set("bom.zip") + val buildBomZip = + project.tasks.register(BUILD_BOM_ZIP_TASK) { + from(generateBom) + archiveFileName.set("bom.zip") + destinationDirectory.set(project.layout.buildDirectory) + } + + project.tasks.register(BUILD_BOM_BUNDLE_ZIP_TASK) { + from(buildBomZip, generateBomReleaseNotes, generateTutorialBundle) + archiveFileName.set("bomBundle.zip") destinationDirectory.set(project.layout.projectDirectory) } @@ -757,6 +766,7 @@ abstract class PublishingPlugin : Plugin { const val BUILD_KOTLINDOC_ZIP_TASK = "buildKotlindocZip" const val BUILD_RELEASE_NOTES_ZIP_TASK = "buildReleaseNotesZip" const val BUILD_BOM_ZIP_TASK = "buildBomZip" + const val BUILD_BOM_BUNDLE_ZIP_TASK = "buildBomBundleZip" const val FIREBASE_PUBLISH_TASK = "firebasePublish" const val PUBLISH_ALL_TO_BUILD_TASK = "publishAllToBuildDir" diff --git a/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt b/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt index e3fccfb66e9..dda8db2896d 100644 --- a/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt +++ b/plugins/src/main/java/com/google/firebase/gradle/plugins/datamodels/PomElement.kt @@ -158,7 +158,7 @@ data class PomElement( @XmlElement val artifactId: String, @XmlElement val version: String, @XmlElement val packaging: String? = null, - @XmlChildrenName("licenses") val licenses: List? = null, + @XmlChildrenName("license") val licenses: List? = null, @XmlElement val scm: SourceControlManagement? = null, @XmlElement val dependencyManagement: DependencyManagementElement? = null, @XmlChildrenName("dependency") val dependencies: List? = null, @@ -171,15 +171,25 @@ data class PomElement( * @see fromFile */ fun toFile(file: File): File { - val xmlWriter = XML { - indent = 2 - xmlDeclMode = XmlDeclMode.None - } - file.writeText(xmlWriter.encodeToString(this)) + file.writeText(toString()) return file } + /** + * Serializes this pom element into a valid XML element. + * + * @see toFile + */ + override fun toString(): String { + return xml.encodeToString(this) + } + companion object { + private val xml = XML { + indent = 2 + xmlDeclMode = XmlDeclMode.None + } + /** * Deserializes a [PomElement] from a `pom.xml` file. * @@ -201,6 +211,6 @@ data class PomElement( * @see fromFile */ fun fromElement(element: Element): PomElement = - XML.decodeFromReader(xmlStreaming.newReader(element)) + xml.decodeFromReader(xmlStreaming.newReader(element)) } } diff --git a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt index 3d914df29d8..182d7d0ffb2 100644 --- a/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt +++ b/plugins/src/test/kotlin/com/google/firebase/gradle/plugins/GenerateBomReleaseNotesTests.kt @@ -65,8 +65,68 @@ class GenerateBomReleaseNotesTests : FunSpec() { Firebase Android SDKs mapped to this {{bom}} version

- Libraries that were versioned with this release are in highlighted rows.
- Refer to a library's release notes (on this page) for details about its changes. + Libraries that were versioned with this release are in highlighted rows. +
Refer to a library's release notes (on this page) for details about its + changes. +

+
+ + + + + + + + + + + + + + + + + +
Artifact nameVersion mapped
to previous {{bom}} v1.0.0
Version mapped
to this {{bom}} v1.0.0
com.google.firebase:firebase-auth10.0.010.0.0
com.google.firebase:firebase-firestore10.0.010.0.0
+ + """ + .trimIndent() + } + + @Test + fun `sorts the entries alphabetically`() { + val dependencies = + listOf( + ArtifactDependency( + groupId = "com.google.firebase", + artifactId = "firebase-firestore", + version = "10.0.0", + ), + ArtifactDependency( + groupId = "com.google.firebase", + artifactId = "firebase-auth", + version = "10.0.0", + ), + ) + val bom = makeBom("1.0.0", dependencies) + val file = makeReleaseNotes(bom, bom) + + file.readText().trim() shouldBeText + """ + ### {{firebase_bom_long}} ({{bill_of_materials}}) version 1.0.0 {: #bom_v1-0-0} + {% comment %} + These library versions must be flat-typed, do not use variables. + The release note for this BoM version is a library-version snapshot. + {% endcomment %} + +