kreait / laravel-firebase

A Laravel package for the Firebase PHP Admin SDK
https://github.com/kreait/firebase-php
MIT License
994 stars 163 forks source link

Calls to the Firebase library are slow #114

Closed LandryNorris closed 2 years ago

LandryNorris commented 2 years ago

When using the Database and Auth libraries, endpoints that use Firebase take several seconds to complete. Calls to database tend to add ~10s and calls to auth tend to add ~5s. Other endpoints that do not use firebase complete in milliseconds. This occurs for every call, not just the first one. I am getting the Database and Auth objects via DI in my service class. I am running the laravel server on localhost port 8000.

jeromegamez commented 2 years ago

Without knowing what you are actually doing in your code, I can unfortunately just guess why it might be taking so long.

Performing actions with Firebase components will add processing time because one action will usually result in at least one HTTP Request to the Firebase APIs.

Depending on the number of actions and the outgoing internet connection (latency and/or a proxy affect this), this could add up.

For example, if during one request to your application, you perform one action on 10 Firebase resources, and each roundtrip takes 0.5 seconds, you'll end up with a total processing time of 5 seconds.

If you'd like me to support with more than just guesses, please provide a reproducible example πŸ™

LandryNorris commented 2 years ago

I ran into this issue at work, but I'll work on writing a minimal reproducible example this weekend. In terms of adding up, I'm only performing individual operations right now (one endpoint only calls set, update, or get on one reference, with some also getting an id from auth using getByEmail). I have mainly worked with the firebase admin SDK on the JVM before, where I see a similar delay for the first firebase operation, but all future operations are much quicker. I'm getting the Database object via DI currently, so I would assume it's not re-loading the SDK each endpoint call.

jeromegamez commented 2 years ago

Due to the nature of PHP, it will indeed reload the whole application (including the SDK) each time an endpoint is called (unless you use something Laravel Octane, but I don't have any experience with that so far).

This means, on each request, the SDK will first fetch an authentication token from Firebase and then perform the action…

This can be alleviated by using a Laravel cache store (configured for the SDK in https://github.com/kreait/laravel-firebase/blob/1ef03e181b3936f9fad9d1dbb382de0c95ab7e25/config/firebase.php#L144)

I just noticed that I might not be caching everything I could cache, I'll have a look at it in the next few days.

LandryNorris commented 2 years ago

I've been working on finding ways to reproduce this issue, but it seems to only show up on my apartment's WiFi. I can't seem to get the issue to show up on any other network. Wireshark seems to indicate that it's an issue with some kex algorithm not being supported, and it looks like the app waits 10 seconds and tries another one.

jeromegamez commented 2 years ago

Thank you for your investigation and for coming back to share your findings! If possible, could you also try with a mobile hotspot on your phone? It's just for my curiosity if this changes something.

Home WiFi can indeed be an issue - seemingly depending on the weather (!) I sometimes don't get Firebase Notifications on my local browser test setup πŸ˜…. This could be something in the router, a shared proxy in between, or the ISP (I'm just guessing, though).

Unfortunately, the SDK can only be as fast as the network connection it's using, I think the proximity of the application to the Firebase servers might also come into play, but I'm not sure here either πŸ˜…

Have you tried playing around with the HTTP Client Options? They're documented at https://firebase-php.readthedocs.io/en/5.x/setup.html#http-client-options and a subset of them can be configured in the Laravel package as well https://github.com/kreait/laravel-firebase/blob/0d698c4806c7588638bc9fa46f2878e114629902/src/FirebaseProjectManager.php#L113-L121

LandryNorris commented 2 years ago

I can confirm that it works faster on my phone hotspot. The odd part is that the Web Console does not experience any delays and the JVM Admin SDK I'm using for other projects is not affected either, even for the same Firebase Project. It is only the PHP Admin SDK that experiences these delays, but only on my apartment WiFi. I'll have to take a look at the client options you mentioned, since my Unit Tests go from about half a second on other networks to 70 seconds at my apartment.

jeromegamez commented 2 years ago

I just created a new release that now (finally) caches the authentication credentials used to make calls to the Firebase APIs. Honestly, I don't know how I could have overlooked this for all the time this package exists :D.

Starting with the second call, Firebase actions should now be faster (I hope). Please let me know if that helped!

LandryNorris commented 2 years ago

I have updated to 3.4.0, and the issue still appears. I have set up a Unit Test that just calls code like below for several different Realtime root references. The expectation is that the first takes ~10 seconds and the rest are quicker, but I'm seeing a linear increase of ~10 seconds for each test. This is reflected when running the project on localhost.

$service = App::make(Database::class); $ref = $service->getReference($baseRef); $key = $ref->getChildKeys()[0]; $value = $ref->getChild($key)->getValue(); $this->assertNotNull($value);

LandryNorris commented 2 years ago

It appears that there is a 10 second timeout somewhere. I ran time curl several times on an endpoint I set up with code similar to the above, and saw timings of 10.339, 10.328, 10.321, and 10.342. Just as before, this only happens on my apartment's WiFi network. I am starting to think the 10 second timeout is a Firebase bug that applies when getting the token, but only shows up on some networks, but if this library is saving the token, this should only happen once. Is there some way I could debug when this library requests a new token to verify if it only does so once?

jeromegamez commented 2 years ago

Can you share how you're setting up the tests, for example in an example repo, so that I can test/reproduce it myself?

LandryNorris commented 2 years ago

firebase-base-reproduced-bug.tar.gz Here is a tarball with my example project. When run on my WiFi network, each test using Firebase takes ~10 seconds. I currently have 4 and ./artisan test says it took 41.45 seconds.

I did not include the admin json for my firebase project in the tarball, but you should be able to put the service account json for any firebase project in the storage folder and change the .env vars to match the database URL and name of the file. The only requirement is from testFirebase, which requires a baseRef with a child inside of it. I included this test to isolate reads, since reading should be the simplest operation.

LandryNorris commented 2 years ago

I have tested this using Arch Linux and Ubuntu on both my laptop (Ice Lake i7, 16GB RAM, Intel AX210) and my desktop (Ryzen 5600x, 32GB RAM, Intel AX210)

jeromegamez commented 2 years ago

Unfortunately, I couldn't reproduce the problem, even with your code.

The reason why you don't see the performance benefits is most likely that you have set FIREBASE_CACHE_STORE=array in the .env file, and between each unit test, the application is torn down and recreated (including the cache).

In addition, I'd recommend using $this->app->singleton(Database::class) instead of App::make(Database::class), and I would recommend instantiating a database instance in the setup method of your test case instead of recreating it in every single test.

So, the reason why you don't get the speed benefit is the array cache which is destroyed after every single test - using $this->app instead of App::make() could help in the context of a complete test case.

At the moment you're doing three requests per test: one to fetch the admin auth token, one to set the database ref value, and another one to reset the database ref value.

You could also try creating a new service account file for your project, in case you're using the same account in your local and remote environment - it was sometimes reported that it helped using different service accounts per environment.


Unfortunately, I can not accompany you in the search of a solution further than here, as this problem seems only to be occurring in one specific network (your home wifi), I hope you can understand πŸ™ .

Please share your findings if you get closer to a solution, and I would gladly (no sarcasm ^^) re-open this issue once you or someone else can point me to something I can reproduce and fix in the code of the package or underlying SDK 🀞.

Good luck!

jeromegamez commented 2 years ago

Oh, one more thing, I noticed in your code:

$result = $ref->set($value);
$this->assertNotNull($result);

$ref->set() will always return the same, unchanged reference, if you want to check if the value was correctly written to the remote database, you should test this by re-fetching the value of the reference:

$this->assertNotNull($ref->getValue());

The integration tests of the SDK show how I did the reference tests: https://github.com/kreait/firebase-php/blob/5.x/tests/Integration/Database/ReferenceTest.php