Getting rid of Laravel models to improve performance of the command (Blackfire.io profiling)

Marko Lekić
Fleka Developers
Published in
3 min readDec 3, 2015

--

We are currently working on a very interesting system which is a digital Sommelier service. With complex formulas built by professional sommeliers we match dishes and wines, and we’ve built mobile apps to allow people in restaurants, shops or at home to see which wines match dishes they would like to combine.

To match dishes and wines we run a Laravel command as a cron job every night which runs formulas to calculate pairing score between a dish and a wine. The command needs to go through hundreds of items and run formulas on them.

I’ll avoid going into too much detail about the project and get straight to the steps we took to significantly improve one functionality of the system.

The two Laravel models we have are as follows:

  • Dish with table “dishes” and a lot of properties
  • Vintage (single year of a wine) with table “vintages” and also a lot of properties

There’s a “many to many” relationship between these two with table “pairing_vintages_dishes” which also contains score for each dish -> wine pairing.

The first iteration of the command we built (in a hurry I might add) did a pretty straightforward thing:

  1. Fetch all Dishes
  2. Fetch all Vintages
  3. For every Dish, go through every Vintage and run formulas. There are several formulas here comparing multiple attributes of a Dish object to multiple attributes of a Vintage object. After running these formulas we run Laravel’s sync() function to save the score for all the related Vintages.

(The command didn’t actually go through all the objects, it compared only those which were changed during the day. But this still happened to be a lot of objects.)

The command worked, but very slowly. We left it working like that for some time until we finished critical stuff in the system and had time to go back and refactor some slow running code.

We ran the command with Blackfire.io and saw some interesting information when we ordered functions by percentage of exclusive time:

Screenshot from Blackfire.io profile, functions ordered by exclusive time percentage

Wall Time 57.6s
CPU Time 49.4s
I/O Time 8.23s
Memory 100MB

Instead of our complex formulas being the most called and expensive functions, it turns out our model’s functions are the ones being called the most.

This happens because Laravel’s Model is using PHP’s magic methods for getting attributes, meaning that every time you call $myObject->something it runs $myObject->getSomethingAttribute() function.

We’re definitely making a good use of models throughout the system, but it’s obvious that we can avoid them in this command, because it does a very specific thing. We can use regular objects instead of the objects that extend Laravel’s Model class.

So, instead of getting dish items like this (code simplified for brevity):

$dishes = Dish::all();

we used:

$dishes = DB::table(‘dishes’)->get();

Instead of getting a Collection object with Models as items, we get an array of objects of stdClass.

After doing this we had to remove our call to sync() function because it’s a function of Model class. Instead we used DB::table(‘pairing_vintages_dishes’)->insert(…) to insert the pairings.

Now, running the command with Blackfire.io we got this:

Screenshot from Blackfire.io profile, functions ordered by exclusive time percentage

Wall Time 3.42s
CPU Time 3.02s
I/O Time 407ms
Memory 23.1MB

In the comparison view in Blackfire.io we see the difference in functions called:

Comparison of two Blackfire.io profiles, functions ordered by exclusive time percentage

It now makes much more sense, the important functions are the ones being called the most.

In the end two quite simple changes significantly improved the performance of the script ran daily:

  1. Removing Larevel Model objects by using DB::table(‘…’)->get() instead of Model::all() to avoid many calls to magic methods
  2. Removing Laravel sync() call to save relationships and using DB::insert([…]) instead to avoid many SQL statements being run

--

--