Skip to content

Migrate functions off of UI thread for continuations. #4364

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 5 commits into from
Nov 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

@SuppressLint("ThreadPoolCreation")
public class ExecutorsRegistrar implements ComponentRegistrar {
private static final Lazy<ScheduledExecutorService> BG_EXECUTOR =
static final Lazy<ScheduledExecutorService> BG_EXECUTOR =
new Lazy<>(
() ->
scheduled(
Expand All @@ -46,15 +46,15 @@ public class ExecutorsRegistrar implements ComponentRegistrar {
factory(
"Firebase Background", Process.THREAD_PRIORITY_BACKGROUND, bgPolicy()))));

private static final Lazy<ScheduledExecutorService> LITE_EXECUTOR =
static final Lazy<ScheduledExecutorService> LITE_EXECUTOR =
new Lazy<>(
() ->
scheduled(
Executors.newFixedThreadPool(
Math.max(2, Runtime.getRuntime().availableProcessors()),
factory("Firebase Lite", Process.THREAD_PRIORITY_DEFAULT, litePolicy()))));

private static final Lazy<ScheduledExecutorService> BLOCKING_EXECUTOR =
static final Lazy<ScheduledExecutorService> BLOCKING_EXECUTOR =
new Lazy<>(
() ->
scheduled(
Expand Down
1 change: 1 addition & 0 deletions firebase-functions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Unreleased
* [changed] Avoid executing code on the UI thread as much as possible.

# 20.2.1
* [changed] Updated dependency of `firebase-iid` to its latest
Expand Down
2 changes: 2 additions & 0 deletions firebase-functions/firebase-functions.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ android {
}

dependencies {
implementation project(':firebase-annotations')
implementation project(':firebase-common')
implementation project(':firebase-components')
implementation project(':appcheck:firebase-appcheck-interop')
Expand All @@ -73,6 +74,7 @@ dependencies {
javadocClasspath 'org.codehaus.mojo:animal-sniffer-annotations:1.19'
javadocClasspath 'com.google.auto.value:auto-value-annotations:1.6.6'

androidTestImplementation project(":integ-testing")
androidTestImplementation 'junit:junit:4.13.2'
androidTestImplementation "com.google.truth:truth:$googleTruthVersion"
androidTestImplementation 'androidx.test:runner:1.3.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@
import static org.junit.Assert.assertTrue;

import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.concurrent.TestOnlyExecutors;
import com.google.firebase.functions.FirebaseFunctionsException.Code;
import java.util.Arrays;
import java.util.HashMap;
Expand Down Expand Up @@ -91,7 +92,9 @@ public void testToken() throws InterruptedException, ExecutionException {
() -> {
HttpsCallableContext context = new HttpsCallableContext("token", null, null);
return Tasks.forResult(context);
});
},
TestOnlyExecutors.lite(),
TestOnlyExecutors.ui());

HttpsCallableReference function = functions.getHttpsCallable("tokenTest");
Task<HttpsCallableResult> result = function.call(new HashMap<>());
Expand All @@ -112,7 +115,9 @@ public void testInstanceId() throws InterruptedException, ExecutionException {
() -> {
HttpsCallableContext context = new HttpsCallableContext(null, "iid", null);
return Tasks.forResult(context);
});
},
TestOnlyExecutors.lite(),
TestOnlyExecutors.ui());

HttpsCallableReference function = functions.getHttpsCallable("instanceIdTest");
Task<HttpsCallableResult> result = function.call(new HashMap<>());
Expand All @@ -133,7 +138,9 @@ public void testAppCheck() throws InterruptedException, ExecutionException {
() -> {
HttpsCallableContext context = new HttpsCallableContext(null, null, "appCheck");
return Tasks.forResult(context);
});
},
TestOnlyExecutors.lite(),
TestOnlyExecutors.ui());

HttpsCallableReference function = functions.getHttpsCallable("appCheckTest");
Task<HttpsCallableResult> result = function.call(new HashMap<>());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider;
import com.google.firebase.auth.internal.InternalAuthProvider;
import com.google.firebase.concurrent.TestOnlyExecutors;
import com.google.firebase.iid.internal.FirebaseInstanceIdInternal;
import com.google.firebase.inject.Deferred;
import com.google.firebase.inject.Provider;
Expand Down Expand Up @@ -54,7 +55,10 @@ public void getContext_whenAuthAndAppCheckAreNotAvailable_shouldContainOnlyIid()
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
absentProvider(), providerOf(fixedIidProvider), absentDeferred());
absentProvider(),
providerOf(fixedIidProvider),
absentDeferred(),
TestOnlyExecutors.lite());

HttpsCallableContext context = Tasks.await(contextProvider.getContext());
assertThat(context.getAuthToken()).isNull();
Expand All @@ -67,7 +71,10 @@ public void getContext_whenOnlyAuthIsAvailable_shouldContainOnlyAuthTokenAndIid(
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
providerOf(fixedAuthProvider), providerOf(fixedIidProvider), absentDeferred());
providerOf(fixedAuthProvider),
providerOf(fixedIidProvider),
absentDeferred(),
TestOnlyExecutors.lite());

HttpsCallableContext context = Tasks.await(contextProvider.getContext());
assertThat(context.getAuthToken()).isEqualTo(AUTH_TOKEN);
Expand All @@ -80,7 +87,10 @@ public void getContext_whenOnlyAppCheckIsAvailable_shouldContainOnlyAppCheckToke
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
absentProvider(), providerOf(fixedIidProvider), deferredOf(fixedAppCheckProvider));
absentProvider(),
providerOf(fixedIidProvider),
deferredOf(fixedAppCheckProvider),
TestOnlyExecutors.lite());

HttpsCallableContext context = Tasks.await(contextProvider.getContext());
assertThat(context.getAuthToken()).isNull();
Expand All @@ -93,7 +103,10 @@ public void getContext_whenOnlyAuthIsAvailableAndNotSignedIn_shouldContainOnlyIi
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
providerOf(anonymousAuthProvider), providerOf(fixedIidProvider), absentDeferred());
providerOf(anonymousAuthProvider),
providerOf(fixedIidProvider),
absentDeferred(),
TestOnlyExecutors.lite());

HttpsCallableContext context = Tasks.await(contextProvider.getContext());
assertThat(context.getAuthToken()).isNull();
Expand All @@ -106,7 +119,10 @@ public void getContext_whenOnlyAppCheckIsAvailableAndHasError_shouldContainOnlyI
throws ExecutionException, InterruptedException {
FirebaseContextProvider contextProvider =
new FirebaseContextProvider(
absentProvider(), providerOf(fixedIidProvider), deferredOf(errorAppCheckProvider));
absentProvider(),
providerOf(fixedIidProvider),
deferredOf(errorAppCheckProvider),
TestOnlyExecutors.lite());

HttpsCallableContext context = Tasks.await(contextProvider.getContext());
assertThat(context.getAuthToken()).isNull();
Expand All @@ -121,7 +137,8 @@ public void getContext_whenAuthAndAppCheckAreAvailable_shouldContainAuthAppCheck
new FirebaseContextProvider(
providerOf(fixedAuthProvider),
providerOf(fixedIidProvider),
deferredOf(fixedAppCheckProvider));
deferredOf(fixedAppCheckProvider),
TestOnlyExecutors.lite());

HttpsCallableContext context = Tasks.await(contextProvider.getContext());
assertThat(context.getAuthToken()).isEqualTo(AUTH_TOKEN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import android.util.Log;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.annotations.concurrent.Lightweight;
import com.google.firebase.appcheck.interop.InternalAppCheckTokenProvider;
import com.google.firebase.auth.internal.InternalAuthProvider;
import com.google.firebase.iid.internal.FirebaseInstanceIdInternal;
import com.google.firebase.inject.Deferred;
import com.google.firebase.inject.Provider;
import com.google.firebase.internal.api.FirebaseNoSignedInUserException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference;

/** A ContextProvider that uses FirebaseAuth to get the token. */
Expand All @@ -33,13 +35,16 @@ class FirebaseContextProvider implements ContextProvider {
private final Provider<FirebaseInstanceIdInternal> instanceId;
private final AtomicReference<InternalAppCheckTokenProvider> appCheckRef =
new AtomicReference<>();
private final Executor executor;

FirebaseContextProvider(
Provider<InternalAuthProvider> tokenProvider,
Provider<FirebaseInstanceIdInternal> instanceId,
Deferred<InternalAppCheckTokenProvider> appCheckDeferred) {
Deferred<InternalAppCheckTokenProvider> appCheckDeferred,
@Lightweight Executor executor) {
this.tokenProvider = tokenProvider;
this.instanceId = instanceId;
this.executor = executor;
appCheckDeferred.whenAvailable(
p -> {
InternalAppCheckTokenProvider appCheck = p.get();
Expand All @@ -59,6 +64,7 @@ public Task<HttpsCallableContext> getContext() {
Task<String> appCheckToken = getAppCheckToken();
return Tasks.whenAll(authToken, appCheckToken)
.onSuccessTask(
executor,
v ->
Tasks.forResult(
new HttpsCallableContext(
Expand All @@ -74,6 +80,7 @@ private Task<String> getAuthToken() {
}
return auth.getAccessToken(false)
.continueWith(
executor,
task -> {
String authToken = null;
if (!task.isSuccessful()) {
Expand All @@ -98,6 +105,7 @@ private Task<String> getAppCheckToken() {
return appCheck
.getToken(false)
.onSuccessTask(
executor,
result -> {
if (result.getError() != null) {
// If there was an error getting the App Check token, do NOT send the placeholder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
package com.google.firebase.functions;

import android.content.Context;
import android.os.Handler;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -27,6 +26,8 @@
import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.google.firebase.FirebaseApp;
import com.google.firebase.annotations.concurrent.Lightweight;
import com.google.firebase.annotations.concurrent.UiThread;
import com.google.firebase.emulators.EmulatedServiceSettings;
import com.google.firebase.functions.FirebaseFunctionsException.Code;
import java.io.IOException;
Expand All @@ -35,6 +36,7 @@
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.MediaType;
Expand Down Expand Up @@ -69,6 +71,8 @@ public class FirebaseFunctions {
// A provider of client metadata to include with calls.
private final ContextProvider contextProvider;

private final Executor executor;

// The projectId to use for all functions references.
private final String projectId;

Expand All @@ -89,8 +93,11 @@ public class FirebaseFunctions {
Context context,
String projectId,
String regionOrCustomDomain,
ContextProvider contextProvider) {
ContextProvider contextProvider,
@Lightweight Executor executor,
@UiThread Executor uiExecutor) {
this.app = app;
this.executor = executor;
this.client = new OkHttpClient();
this.serializer = new Serializer();
this.contextProvider = Preconditions.checkNotNull(contextProvider);
Expand All @@ -112,15 +119,16 @@ public class FirebaseFunctions {
this.customDomain = regionOrCustomDomain;
}

maybeInstallProviders(context);
maybeInstallProviders(context, uiExecutor);
}

/**
* Runs ProviderInstaller.installIfNeededAsync once per application instance.
*
* @param context The application context.
* @param uiExecutor
*/
private static void maybeInstallProviders(Context context) {
private static void maybeInstallProviders(Context context, Executor uiExecutor) {
// Make sure this only runs once.
synchronized (providerInstalled) {
if (providerInstallStarted) {
Expand All @@ -131,7 +139,7 @@ private static void maybeInstallProviders(Context context) {

// Package installIfNeededAsync into a Runnable so it can be run on the main thread.
// installIfNeededAsync checks to make sure it is on the main thread, and throws otherwise.
Runnable runnable =
uiExecutor.execute(
() ->
ProviderInstaller.installIfNeededAsync(
context,
Expand All @@ -146,10 +154,7 @@ public void onProviderInstallFailed(int i, android.content.Intent intent) {
Log.d("FirebaseFunctions", "Failed to update ssl context");
providerInstalled.setResult(null);
}
});

Handler handler = new Handler(context.getMainLooper());
handler.post(runnable);
}));
}

/**
Expand Down Expand Up @@ -269,8 +274,9 @@ public void useEmulator(@NonNull String host, int port) {
Task<HttpsCallableResult> call(String name, @Nullable Object data, HttpsCallOptions options) {
return providerInstalled
.getTask()
.continueWithTask(task -> contextProvider.getContext())
.continueWithTask(executor, task -> contextProvider.getContext())
.continueWithTask(
executor,
task -> {
if (!task.isSuccessful()) {
return Tasks.forException(task.getException());
Expand All @@ -291,8 +297,9 @@ Task<HttpsCallableResult> call(String name, @Nullable Object data, HttpsCallOpti
Task<HttpsCallableResult> call(URL url, @Nullable Object data, HttpsCallOptions options) {
return providerInstalled
.getTask()
.continueWithTask(task -> contextProvider.getContext())
.continueWithTask(executor, task -> contextProvider.getContext())
.continueWithTask(
executor,
task -> {
if (!task.isSuccessful()) {
return Tasks.forException(task.getException());
Expand All @@ -305,7 +312,7 @@ Task<HttpsCallableResult> call(URL url, @Nullable Object data, HttpsCallOptions
/**
* Calls a Callable HTTPS trigger endpoint.
*
* @param name The name of the HTTPS trigger.
* @param url The name of the HTTPS trigger.
* @param data Parameters to pass to the function. Can be anything encodable as JSON.
* @param context Metadata to supply with the function call.
* @return A Task that will be completed when the request is complete.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,12 @@

import android.content.Context;
import androidx.annotation.GuardedBy;
import androidx.annotation.UiThread;
import com.google.firebase.FirebaseApp;
import com.google.firebase.annotations.concurrent.Lightweight;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Executor;

/** Multi-resource container for Functions. */
class FunctionsMultiResourceComponent {
Expand All @@ -33,12 +36,20 @@ class FunctionsMultiResourceComponent {
private final Context applicationContext;
private final ContextProvider contextProvider;
private final FirebaseApp app;
private final Executor liteExecutor;
private final Executor uiExecutor;

FunctionsMultiResourceComponent(
Context applicationContext, ContextProvider contextProvider, FirebaseApp app) {
Context applicationContext,
ContextProvider contextProvider,
FirebaseApp app,
@Lightweight Executor liteExecutor,
@UiThread Executor uiExecutor) {
this.applicationContext = applicationContext;
this.contextProvider = contextProvider;
this.app = app;
this.liteExecutor = liteExecutor;
this.uiExecutor = uiExecutor;
}

synchronized FirebaseFunctions get(String regionOrCustomDomain) {
Expand All @@ -48,7 +59,13 @@ synchronized FirebaseFunctions get(String regionOrCustomDomain) {
if (functions == null) {
functions =
new FirebaseFunctions(
app, applicationContext, projectId, regionOrCustomDomain, contextProvider);
app,
applicationContext,
projectId,
regionOrCustomDomain,
contextProvider,
liteExecutor,
uiExecutor);
instances.put(regionOrCustomDomain, functions);
}
return functions;
Expand Down
Loading