Skip to content

Implement a SessionSubscriber for Firebase Performance #6683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Feb 11, 2025
2 changes: 1 addition & 1 deletion firebase-perf/firebase-perf.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Component<?>> getComponents() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<RemoteConfigComponent> firebaseRemoteConfigProvider;
private final FirebaseInstallationsApi firebaseInstallationsApi;
private final Provider<TransportFactory> transportFactoryProvider;

/**
* Constructs the FirebasePerformance class and allows injecting dependencies.
*
Expand All @@ -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;
Expand All @@ -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(
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -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() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Nullable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

return aqsSessionId;
}

/** Sets the AQS sessionId for the given session. */
public void setAQSId(SessionSubscriber.SessionDetails aqs) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want the Perf session to have the id of the current AQS session when the Perf session starts, but then allow AQS to have different sessions due to background foreground etc? Because Crashlytics will have the current AQS session id at crash time added to the report.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AQS knows the "first session id" and the current session id. If that is your concern, I think it's better to have the current aqs session id as the perf session id, and then if we need to do lookups we can join it to aqs data and know the first session id.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the behaviour a little compared to #6665.

This retains the PerfSession ID as is, which is used to identify sessions for gauge collection.

However, my plan is to use the AQS session ID to log the gauges. Whenever there is an AQS change, it will also result in a new PerfSession.

if (aqsSessionId == null) {
aqsSessionId = aqs.getSessionId();
}
}

/**
* Returns a timer object that has been seeded with the system time at which the session began.
*/
Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,20 @@
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;
import com.google.firebase.perf.v1.GaugeMetric;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/** Session manager to generate sessionIDs and broadcast to the application. */
@Keep // Needed because of b/117526359.
public class SessionManager extends AppStateUpdateHandler {
public class SessionManager {

@SuppressLint("StaticFieldLeak")
private static final SessionManager instance = new SessionManager();
Expand All @@ -45,7 +42,6 @@ public class SessionManager extends AppStateUpdateHandler {
private final Set<WeakReference<SessionAwareObject>> clients = new HashSet<>();

private PerfSession perfSession;
private Future syncInitFuture;

/** Returns the singleton instance of SessionManager. */
public static SessionManager getInstance() {
Expand All @@ -71,50 +67,14 @@ public SessionManager(
this.gaugeManager = gaugeManager;
this.perfSession = perfSession;
this.appStateMonitor = appStateMonitor;
registerForAppState();
}

/**
* Finalizes gauge initialization during cold start. This must be called before app start finishes
* (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);
}

/**
Expand All @@ -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;
}

Expand Down Expand Up @@ -207,9 +167,4 @@ private void startOrStopCollectingGauges(ApplicationProcessState appState) {
public void setPerfSession(PerfSession perfSession) {
this.perfSession = perfSession;
}

@VisibleForTesting
public Future getSyncInitFuture() {
return this.syncInitFuture;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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);
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
Loading
Loading