Skip to content
This repository was archived by the owner on Jun 25, 2025. It is now read-only.

Commit de03f30

Browse files
committed
Updated Readme with some docs
1 parent 79100b9 commit de03f30

File tree

1 file changed

+196
-1
lines changed

1 file changed

+196
-1
lines changed

README.md

Lines changed: 196 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,199 @@ uses [ReactPHP Promise](https://github.com/reactphp/promise) implementation for
55
this purposes.
66

77
> This package is still on progress. Subscribe to the repository to be aware of
8-
> updates and follow future stable versions
8+
> updates and follow future stable versions
9+
10+
## POV
11+
12+
This package is part of a Prove of Concept with the main goal of working with
13+
Promises in Symfony. Before that, let's check where are we. Let's start from the
14+
beginning and talk about what Symfony is made for, the regular usage is usually
15+
used for and how we can really improve **SO MUCH** the performance of our
16+
applications.
17+
18+
> As a disclaimer, this POV is focused on APIs, so we are not adding here any
19+
> Twig support. Once the package becomes tested and a little bit stable, then we
20+
> will add this feature.
21+
22+
## Symfony on top of Apache / Nginx
23+
24+
When we talk about Symfony, we usually talk about a small stack of technologies
25+
that, and not only with Symfony but with almost every single PHP 7 framework and
26+
project.
27+
28+
We can think about Apache or Nginx as a server, Doctrine as a Database layer and
29+
maybe Redis as cache or key-value persistent storage. That's great. If we could
30+
ask every single Symfony project in this world, we would find mainly this
31+
configuration.
32+
33+
Let's check this configuration in a performance point of view. And when we mean
34+
performance, we mean about CPU consumption, Memory usage and Response times.
35+
Everything matters here, right? At least, everything should matter in terms of
36+
software economy.
37+
38+
**Step 1** - Apache server receives a new request and sends it directly to the
39+
app.php file.
40+
**Step 2** - Symfony kernel boots. That means that, in the better of the cases, the
41+
container is cached properly. The Kernel is created *(once again)*, the
42+
container configuration is loaded from this cache, and a new Request object is
43+
created with the data received from Apache.
44+
**Step 3** - The Kernel handles this Request, waiting for a Response.
45+
**Step 4** - Some framework magic (resolve controller, resolve dispatch some
46+
events...)
47+
**Step 5** - We call the controller entry point. Remember that we MUST return a
48+
Response instance (remember that we don't use Views here, so we discard
49+
returning an array here. Anyway, would be the same).
50+
**Step 6** - We do our logic. For example, we call a repository to get an array of
51+
values from Redis.
52+
**Step 7** - Redis returns an array of values, where the controller return a new
53+
Response with these values, where the Kernel, after some extra event dispatches,
54+
return this Response to Apache, which return the response to the final client.
55+
56+
This is one natural Request / Response workflow in one of our applications.
57+
Fast, isn't it? Let's check in terms of performance.
58+
59+
**Step 1** - We must have Apache server installed. By adding Apache as a man in
60+
the middle, we spend some time. Even if it's **1ms**, we will see later that
61+
each single **1ms** can be so much important here.
62+
**Step 2** - Symfony kernel is booted every time. Once and again. Every single
63+
request. Let's say... **15ms**? **20ms?** Something like that. Let's say
64+
**15ms** being SO optimists.
65+
**Step 7** - Imagine a Redis call as a representation of any external call. This
66+
could be a redis one (fast one), or an HTTP one, slow one. This action will
67+
last the time this operation lasts. Let's say **50ms**.
68+
69+
If we consider that the PHP application can be around **3ms**, let's calculate
70+
the total time of our requests from the final user point of view. This time is
71+
**70ms** per thread. Because both Apache or Nginx can manage several threads at
72+
the same time, we can say that the final response will be around 70ms per
73+
request. I would say not bad, but for people that respect performance, 70ms are
74+
very bad numbers.
75+
76+
## Symfony on top of ReactPHP
77+
78+
The main goal of this layer is to delete Apache. Why? Well, if you review the
79+
numbers below, you will find that booting the kernel each time is so expensive.
80+
We said around **15ms** as a symbolic number, but these numbers can easily
81+
increase so much depending on the server.
82+
83+
The main goal is to be sure that we keep the kernel built and running forever
84+
and ever, listening new Requests, and returning new Responses.
85+
86+
For this, we must know a project called [ReactPHP](https://github.com/reactphp).
87+
It is important to know that project because they have a nice HTTP Server to
88+
handle requests and return responses. Is based on Promises, but, as always, you
89+
don't have to work with Promises to work with Promises.
90+
91+
By using this server, we would remove the **Step 1** and the **Step 2**. The
92+
kernel is booted only once (can really be booted before the first request), and
93+
after that, each requests would start at **Step 3 **.
94+
95+
Time elapsed? Well, **54ms** per each server. In that case, the server would be
96+
the same PHP. The problem? Well, we only have one single thread here, so this
97+
server would be completely blocking here, allowing only to return around 20
98+
requests per second (while the HTTP blocking call is not resolved, the thread is
99+
waiting for it).
100+
101+
We can easily solve this problem by emulating what Apache does internally,
102+
having multiple threads or workers, and balancing between them as long as they
103+
are not available.
104+
105+
You can check a project called [PHP-PM](https://github.com/php-pm/php-pm). This
106+
server creates as many ReactPHP servers you need and use them all in a smart
107+
way.
108+
109+
Performance review. Well. If you have so many requests per second, and you have
110+
very hard I/O operations, you might want to add many workers there. Otherwise,
111+
you will experiment timeouts. Remember that you will still having several
112+
threads with blocking calls. A good approach, but not as good as a person that
113+
cares about performance while doing PHP wants to see.
114+
115+
## Symfony & Promises
116+
117+
So what about Promises? Can I work with Symfony and Promises at the same time?
118+
Yes you can. And is very easy. There is only one condition here. You can async
119+
everything you want, even I/O operations by using some Client like
120+
[BuzzClient](https://github.com/clue/reactphp-buzz), but as long as you get
121+
returned to the controller, you will have to turn this promises asynchronous and
122+
and get returned their value. You can do that by using
123+
[ReactPHP Block](https://github.com/clue/reactphp-block). Remember that the
124+
Symfony Kernel **MUST** return a Response object, and the same for the
125+
Controller.
126+
127+
So will it be really asynchronous if the event loop is only shared by one single
128+
thread? At all.
129+
130+
## Symfony Async Kernel
131+
132+
So on one side we have a Server called ReactPHP Http Server that work with a
133+
running loop, and on the other side, we have a domain built on top of Promises,
134+
with some non-blocking clients like the HTTP one or a Redis one.
135+
136+
This is the workflow.
137+
138+
**Step 1** - ReactPHP receives it's own Request, and creates a Symfony request.
139+
**Step 2** - The Kernel handles this Request, waiting for a Response.
140+
**Step 3** - Some framework magic (resolve controller, resolve dispatch some
141+
events...)
142+
**Step 4** - We call the controller entry point. Remember that we MUST return a
143+
Response instance.
144+
**Step 5** - We do our logic. For example, we call a repository to get an array
145+
of values from Redis.
146+
**Step 6** - Redis returns a **Promise** of values. This promise is returned to
147+
the controller, and has to be resolved. Once is resolved, returns a Response to
148+
the Kernel.
149+
**Step 7** - The Kernel returns the Response to the ReactPHP server, which
150+
creates a new promise with that Response.
151+
152+
When checking performance, we see that the I/O, even if it's asynchronous, is
153+
blocked in the controller by the application, so lasts the same 50ms than
154+
before. Our server is still blocking.
155+
156+
So, can we all see that this Symfony Kernel is the blocking part of the whole
157+
application?
158+
159+
What if would have a way of, instead of returning a Response, our Kernel could
160+
be able to handle Promises? In that case, we should'nt have to wait for any
161+
Promise response, passing the promise created by the I/O async client directly
162+
to the ReactPHP server.
163+
164+
Let's check the workflow.
165+
166+
**Step 1** - ReactPHP receives it's own Request, and creates a Symfony request.
167+
**Step 2** - The Kernel handles **asynchronously** this Request, waiting for a
168+
Promise containing a Response.
169+
**Step 3** - Some framework magic (resolve controller, resolve dispatch some
170+
events...)
171+
**Step 4** - We call the controller entry point. Now we can return a Promise
172+
instead of a Response instance.
173+
**Step 5** - We do our logic. For example, we call a repository to get an array
174+
of values from Redis.
175+
**Step 6** - Redis returns a Promise of values. This promise is returned to
176+
the controller. No need to resolve anything. Returning the Promise to the
177+
Kernel.
178+
**Step 7** - The Kernel returns the Promise to the ReactPHP server. Directly.
179+
180+
And checking the performance? Easy. The response will still return in 54ms. We
181+
can improve this time by improving your networking interface or by adding some
182+
cache. By the time spent in the server for that request?
183+
184+
**4ms**.
185+
Only **4ms**.
186+
187+
And the most important part.
188+
189+
With the old implementation:
190+
- (time 0) Request 1 *(slow)*
191+
- (time 1) Request 2 *(ultrafast)*
192+
- (time 2) Request 3 *(fast)*
193+
- (time 80) Response 1
194+
- (time 81) Response 2
195+
- (time 91) Response 3
196+
197+
With the new implementation
198+
- (time 0) Request 1 *(slow)*
199+
- (time 1) Request 2 *(ultrafast)*
200+
- (time 1) Response 2
201+
- (time 2) Request 3 *(fast)*
202+
- (time 12) Response 3
203+
- (time 80) Response 1

0 commit comments

Comments
 (0)