-
-
Notifications
You must be signed in to change notification settings - Fork 450
Improve throughput when sending FCM Messages by using HTTP/2 #874
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
Conversation
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## 7.x #874 +/- ##
============================================
+ Coverage 91.05% 91.08% +0.02%
+ Complexity 1596 1589 -7
============================================
Files 156 156
Lines 4418 4407 -11
============================================
- Hits 4023 4014 -9
+ Misses 395 393 -2 |
|
Hi jeromegamez, I wonder if we need to check if curl lib supports http2 like here Thank you. |
|
HTTP/2 support has been available since cURL 7.47, released in 2016... although PHP 8.x requires just cURL 7.29, I think it's safe to assume that current OSes ship with supported cURL releases. I'd rather not add a check related to a downstream library 🤔 |
|
Hm. I just tried sending 100 messages with the following script, and ran into errors myself once I tried it with more than ~75 messages: <?php
use Kreait\Firebase\Factory;
use Kreait\Firebase\Messaging\CloudMessage;
$factory = (new Factory())
->withServiceAccount('/path/to/my/service_account.json')
;
$messaging = $factory->createMessaging();
$message = CloudMessage::fromArray([
'notification' => [
'title' => 'Hello World!',
'body' => 'Hello World!',
]
]);
$targets = array_fill(0, 70, "<device token>");
$results = $messaging->sendMulticast($message, $targets);
dump($results->successes()->count());
dump($results->failures()->count());When it works (with fewer messages), and I enable debug for the requests, I see something like It seems that the connection isn't kept alive/reused after all, so I'll need to continue working on this and explore if the situation can be ameliorated with a Guzzle Pools and a (configurable) concurrency. |
|
I tested this locally sending invalid tokens (token1, token2, token3...) to a valid project and i cannot replicate this issue. |
d0377af to
77f78de
Compare
|
Okay, I could make it work and send 1500 messages in ~5 seconds, without running into failures. However, I'm not sure if the send reports for successfully sent messages are correct (and I'm not sure they were ever correct).
The mapping between message and response is done by iterating the messages and using the next message to map it to the next successful response. I'm not sure the responses are processed in the same order as the requests were sent, but if this mapping is correct (or ever was), that I can't say for sure. |
|
Instead of using iterators, I could use an array of requests and then map messages to responses, but then, sending of thousands of messages at a time would increase the memory usage by a lot, I don't know if people will then come complain 🤔. Another way would be to only report failures, but if people rely on the successful reports, that would be a BC. Another possibility would be to introduce a new method with a new result object. I don't know how I feel about that, there are already so many methods to send messages. The second and third approach would mean that successful responses could not be mapped to the respective messages anymore. Again, I can't say for sure this is the case in the current implementation either. Hmmm 🤔 |
|
Thanks for working on this @jeromegamez!
Really? According to the docs the response should contain a
I don't think we can assume that the responses are processed in the same order. How about using the index and retrieving the message on demand?: 'fulfilled' => function (ResponseInterface $response, $index) use ($messages, &$sendReports) {
$message = $this->ensureMessage($messages[$index]);
// ...
$sendReports[$index] = SendReport::success($message->target(), $json);
},
'rejected' => function (RequestException $reason, $index) use (&$sendReports) {
// ...
$sendReports[$index] = SendReport::failure($message->target(), $reason, $message);
} |
|
The last time I checked, the docs lied 😅, but I'll have another look. |
|
Alright, I've split the two approaches into two commits ("Use generators...", and "Use arrays..."), and did some naive benchmarks: ### Generators, HTTP/2, using GuzzleHTTP's pool
10 messages, Memory usage: 0.719215 MiB, Duration: 276.203834 ms
100 messages, Memory usage: 0.476227 MiB, Duration: 404.786958 ms
1000 messages, Memory usage: 1.351799 MiB, Duration: 2573.935125 ms
10000 messages, Memory usage: 9.478622 MiB, Duration: 23759.530041 ms
### Arrays, HTTP/2, using GuzzleHTTP's pool
10 messages, Memory usage: 0.767624 MiB, Duration: 295.545958 ms
100 messages, Memory usage: 0.893921 MiB, Duration: 536.475875 ms
1000 messages, Memory usage: 6.248581 MiB, Duration: 7520.771083 ms
10000 messages, Memory usage: 58.461571 MiB, Duration: 40931.196458 ms
### Latest Release 7.9.1, HTTP 1.1, async requests
10 messages, Memory usage: 0.695015 MiB, Duration: 287.356417 ms
100 messages, Memory usage: 1.948715 MiB, Duration: 424.529083 ms
PHP Warning: include(vendor/guzzlehttp/guzzle/src/Exception/ConnectException.php):
Failed to open stream: Too many open files in vendor/composer/ClassLoader.php on line 576No need to say that we need this PR one way or the other 😅. However, the jump in execution time and memory usage is high when we use arrays instead of generators. Given that the current implementation doesn't work reliably at all (results may vary depending on the underlying system), I think that using arrays would be a reasonable choice for the first iteration. Using Generators is the way to go in the long run, though. I'll figure that one out after this PR. If nobody objects, I'll proceed with the arrays in the next days. Here's the script I used: <?php
declare(strict_types=1);
require 'vendor/autoload.php';
use Kreait\Firebase\Factory;
use Kreait\Firebase\Messaging\CloudMessage;
use Kreait\Firebase\Messaging\MessageTarget;
use Kreait\Firebase\Messaging\Notification;
$token = "I'm sending all messages to a single token";
$sizes = [10, 100, 1000, 10000];
$factory = (new Factory())->withServiceAccount('/path/to/service-account.json');
$messaging = $factory->createMessaging();
foreach ($sizes as $size) {
$targets = array_fill(0, $size, $token);
$messages = [];
foreach ($targets as $target) {
$messages[] = CloudMessage::withTarget(MessageTarget::TOKEN, $target)
->withNotification(Notification::create('Title', $target));
}
$usageAtStart = memory_get_peak_usage(false) / 1024 / 1024;
$timeAtStart = hrtime(true);
$report = $messaging->sendAll($messages);
$usageAtEnd = memory_get_peak_usage(false) / 1024 / 1024;
$timeAtEnd = hrtime(true);
$peakUsage = $usageAtEnd - $usageAtStart;
$duration = ($timeAtEnd - $timeAtStart) / 1e+6; // nanoseconds to milliseconds
printf("%d messages, Memory usage: %f MiB, Duration: %f ms\n", $size, $peakUsage, $duration);
} |
|
@rickclephas I checked, and the response body after a successful send looks like this: {
"name": "projects/my-project/messages/867384e8-9692-40f4-b26c-e6f89990293b"
}The message ID, which I never knew what to use it for ¯\(ツ)/¯ |
Sounds great! Some thoughts:
|
|
Alright, I think we found a nice middleground, great minds think alike, so as a final change, the messages are now stored in an array, the requests given as a Generator, and the mapping between messages and responses is done via the index variable available through the response handler configuration. 10 messages, Memory usage: 0.750603 MiB, Duration: 320.770875 ms
100 messages, Memory usage: 0.617378 MiB, Duration: 366.145416 ms
1000 messages, Memory usage: 2.851311 MiB, Duration: 2722.624125 ms
10000 messages, Memory usage: 24.174843 MiB, Duration: 28863.839500 msThe numbers are looking good, closer to the "pure" generators approach than the "pure" array approach. I will let this sit for a few days, or until someone confirms that this works for them without errors or problems. |
103758b to
79d6218
Compare
I would be happy to try this in our project and report back. Just have one question: $messages = [CloudMessage::fromArray(...), ...];
$reports = $messaging->sendAll($messages)->getItems();
// is it guaranteed that report 0 is referring to message 0?
assert($reports[0]->target() === $messages[0]->target());At the moment we rely on this order to associate the reports to our device records. |
|
I believe that the order will be preserved, because I understood the If this is the case, the generated requests are in the same order as the messages in the array, so the index in the response handlers should be the same as the index in the message array. If it isn't, it means we have to go with the "everything in arrays" approach, right? I can't test this because I only have one manually created registration token to work with. I have mentioned it at some occasions, but as a reminder: I haven't used Firebase myself in ~10 years 😅 |
Yeah that's how I understand it as well. The thing I am unsure about is the order in which the Meaning the message inside the report is correct but the message inside report 0 wouldn't be message 0.
I could do some basic testing with multiple tokens on our development environment.
No problem, your work and support is much appreciated! Evenmore so if you aren't actively using Firebase yourself. I do actively use Firebase, but I don't normally use PHP so forgive me if this doesn't make sense, but couldn't we use the index provided by the callbacks to also determine the order of the reports?: $sendReports[$index] = SendReport::...;I am not really sure if this has any obvious issues in how PHP handles index based arrays vs custom key based arrays. |
|
The reports should now be ordered by the order of messages. But: I noticed that there is one test randomly failing, because the order is not as expected: I'll have to dive into that - hopefully it's the test that has a thinking error 🤞🏻. |
d778e23 to
8e84974
Compare
|
Alright I think I get why the test is randomly failing. $array = [];
$array[1] = "One";
$array[0] = "Zero";
var_dump($array); // array(2) { [1]=> string(3) "One" [0]=> string(4) "Zero" }While the indexes are correct the order in the array isn't.
Using - $sendReports = [];
+ $sendReports = array_fill(0, count($messages), null); |
|
Didn't you say you rarely do PHP? This is spot-on! I will implement this as soon as a find the time later! |
😅 yeah recently been helping some coworkers, but otherwise not too much PHP experience. I already did some testing with the "stresstest" script and so far everything else seems to be working fine. I'll do some normal testing as well, but I think we are good once the test is green 👍🏻. |
|
I added the Thank you for the fruitful collaboration 🫶🏻 ! And as always, if the project is useful to you, please consider becoming a sponsor! |
|
Released with https://github.com/kreait/firebase-php/releases/tag/7.10.0 🚀 |
|
We are currently running the 5.0.0 version and we currently have a depdendency conflict with lcobucci/jwt atm. Thanks in advance |
|
I'd recommend first updating the SDK to 6.x and inspecting the changelog and the upgrade.md before updating to 7.x. |
See #873