Description
I have recently been trying to figure out where my application spends most of its time when preparing requests and uncovered a couple of performance issues in the way that this api library is designed.
To illustrate the issue clearly I have profiled the application with a span-based profiler and annotated the result to better discuss the result.
The profile should be read from left to right and illustrates at which time a function which was being profiled was entered and exited. The long blue bar at the top of the image is the span of the application, whereas the short blue bar at the top left is the Application::boot() running. The short red bars represent db accesses.
For this discussion there are four distinct and important segments (the large, tall coloured boxes, which I added), coloured blue, red, yellow, and green.
The blue segment is the time that the application spent in the controller, after which, my business logic is over. The red, yellow, and green blocks represent time which is spend in preparing the response to send out.
To be precise, the red block is time spent in Laravel's response handling, while the yellow and green blocks are time spent in Dingo's response handling. The fact that the db access patterns look the same should be alarming. In fact, the access patterns are the same because the same queries are being run.
So what is going in the red, yellow, and green blocks? Basically $model->toJson()
is being called on the same model three times. My models have some attributes which are generated and appended using Laravel's appends, some of them require running db queries.
The real question is, why is toJson()
being called on the same model three times? The answer to which exposes the design issue in this api library. The red block is just laravel doing what laravel does when the controller returns a collection or model (actually, anything it thinks can be made into json) turn it into json. Because Dingo wraps Laravel's response processing, this step basically can't be skipped.
After the red block is run, Dingo receives Laravel's response, and prepares it, which involves discarding the result of the toJson()
above, extracting the original model which was used, and constructing a Dingo Response
object, which is a subclass of Laravel's Response
object. On constructing the response, laravel's Response::setContent()
method is called, which also runs toJson() on the model, because that's what Laravel's Response
does!
Finally, the third toJson()
(in the green block) is the transformer I'm using to get the json representation of the model. In effect, the red and yellow blocks are unnecessary, and discarded, but still take between 1/3 to 1/2 of total request processing time.
So what are the solutions to this problem? As a user of this library I see the following solutions:
- Make
toJson()
cheap - Cache the result of attribute methods on my objects
- Fix this library
- Use a different library
Unfortunately options 1. and 2. aren't real solutions. 1. limits me unnecessarily, 2. is just going to give me a headache.
Option 3. could be possible, but I think that the fix might be too drastic for a library which hasn't even hit 1.0 yet. It appears to me as though the core issue is the fact that dingo wraps itself around laravel's response. As far as I can tell there is no clean way to prevent the red block from happening in the current design, short of conditionally overriding the behaviour of Illuminate\Http\Response::setContent
. I think that solution would be to pass Laravel the actual formatted response in string format directly from the controller. I assume that this would break some or all of dingo's current design, but I don't have a good overview of how all of the pieces of dingo fit together, so I don't know what the repercussions of this would be.
If we don't reach some sort of consensus here then I guess I will have to resort to 4.
I hope that I have clearly presented the issue I am currently having and that we can begin a fruitful discussion based on my findings.