Introducing

Laravel Kata: A Practical Way to Experiment with Performance Techniques

Using A/B testing to find the most optimal solution in Laravel, Eloquent, PHP, SQL, and Redis

Hendrik Prinsloo
7 min readMar 10, 2023
“blue elephant flying to the moon like ET on a bicycle, cartoon style” — By Mage with Stable Diffusion 2.1

We often neglect performance when rapidly developing software. It is common for sub-optimal solutions to creep through the cracks and get overlooked in code reviews, even by the most senior developers.

It is particularly true when a company transitions from startup to scale-up. The rate of development of new features grows as the team does, and so too do the mistakes. The whole team needs to become performance aware to prevent obvious bottlenecks from being introduced.

Laravel is one of the most popular frameworks in PHP and is driving many solutions today. As with any language or framework, it allows you to solve problems in many different ways but only a few good ones. To identify the bad from the good, you need to become proficient by frequently referring to the documentation and community to ensure you stay as close to the guidelines as possible.

The copy & paste plague

The introduction of bad code tends to become contagious due to the copy & paste plague. We often trust that the existing piece of code is good enough to replicate.

Always question existing logic in the codebase, especially legacy code. Expect that it wasn’t designed with scalability in mind.

Hockey stick effect

When a company transitions from startup to scale-up, everything around it is expected to grow. The rate of development, amount of end-users, and the growth of data. Waiting for the platform to reach a critical point of performance degradation is a very risky game and will cost you more to correct than taking a more proactive approach.

To predict the potential hockey stick effect is almost impossible without rigorous benchmarking, and even then, there’s no guarantee you will prevent or avoid it.

The Performance Hockey Stick — Source

It is difficult to catch these flaws early enough before they spread throughout the codebase. If you ever had to do a platform-wide benchmark to identify potential bottlenecks you will be familiar with this challenge. The exercise is tedious and often comes down to identifying a few basic bad patterns riddled throughout the codebase.

Mitigation techniques

There are many ways to prevent bottlenecks from infecting the performance of the application, and if you have the capacity it is a good idea to implement all of them. However, that is not always viable especially when resources are limited.

Active monitoring

Active monitoring is essential when building a scalable platform. However, you should not rely on catching bottlenecks at this stage. The alerts mean you are already annoying your end-users, which is not ideal and way too late. This is especially important in startups, as you would typically be onboarding a lot of new users and first impressions go a long way. Leaving is faster than waiting.

Scheduled benchmarking

This could be considered good practice if you can automate the process and fine-tune it to be extensive and realistic, which is a tricky and fine balance. You need to create a lab-like environment and simulate the behaviour of your end users as realistically as possible. This is black magic, as humans are unpredictable and you will always overlook something. It comes with an increased maintenance cost and a slower release cycle.

Continuous performance testing

Integrating benchmarking in your CI/CD pipelines is a solid strategy but suffers from the same symptoms as scheduled benchmarking. The development team would still have to simulate expected behaviour and identify the boundaries. This will always be flawed to some degree and very difficult to implement retrospectively.

Up-skilling the development team

This is probably the most effective and cost-efficient way of improving the performance of any platform. You can expect the performance to improve at the same rate as the knowledge of the developers. Making the developers performance-aware will radically improve the loading time and resource consumption of your application.

The inspiration

In my last position, we were lucky enough to identify quite early that we were reaching closer to the dreaded hockey stick effect. The performance started to decline in some critical areas, and we immediately shifted our focus to identifying and resolving the underlying issues.

We started with a manual benchmarking exercise to identify the most critical areas. Once we had the prioritised list, all the developers got involved. It cost us more time for the optimisations, but up-skilling the entire team was the priority and was well worth the delays. In retrospect, this decision ensured that we achieved massive gains, even better than we initially anticipated.

We focused on identifying the problematic areas, prioritised them, and got the team to optimise the list in sequence; taking special care to prevent overlap and potential merge conflicts. The team had documentation on some basic guidelines of (a) how to diagnose, (b) resolve, and (c) prove results. It was a great exercise and helped all the developers to become performance aware while collaborating on different approaches and techniques.

Optimising performance

When it comes to performance it is often difficult to identify the most optimal solution. It requires a lot of research, experimentation, and a continuous wrestle against bias and preference. You need to match different approaches against each other, thoroughly test them, and get some measurable stats to prove the most optimal solution.

It often happens that one approach is more performant than another on a lesser load and that the alternative approach performs better under higher strain. This inspired me to come up with a generic and sandboxed approach to experiment with different strategies and benchmark them against each other.

Developers can argue about a lot of things, especially when it comes down to best practices, editors, or the number of white spaces for tabs. But you can’t argue against stats.

This inspired the concept of a practical exercise ground.

Laravel Kata

Laravel Kata is a playground that enables you to compare different approaches against each other to help you find the optimal solution. By measuring the performance of these approaches in a few different ways, you should be able to identify which would be more efficient based on your circumstances.

Example challenge

The following example showcases the measurable performance between two snippets of code that achieve the same result: to return the value of PI.

Bad solution

class KataChallengeSample
{
public function calculatePi(int $limit): float
{
$denominator = 1;
$sum = 0;
for ($i = 0; $i < 100000; $i++) {
$sum = ($i % 2 === 0)
? $sum + (4 / $denominator)
: $sum - (4 / $denominator);
$denominator += 2;
}
return round($sum, 2);
}
}

Better solution

class KataChallengeSampleRecord
{
public function calculatePi(int $limit): float
{
return round(M_PI, 2);
}
}

The report

You always need to apply some context when approaching performance issues. There’s a lot more value in slightly optimising a section that is commonly executed than significantly optimising a section that is rarely executed. Furthermore, you need to find a balance between frequency, load, and complexity.

Sample report

These concepts inspired the following metrics:

  • Outputs (md5): Safety metric. Given the same inputs, function A should have the same output as function B, this is non-negotiable.
  • Line count: Depending on the potential of change in the logic, I would almost always prefer a solution that is more readable and maintainable than something with a lower space and time complexity. I would sacrifice a higher Big-O for fewer bugs any day.
  • Violations: Identifying redundant or sub-optimal code in a piece of logic is always worth refactoring. Code sniffing is a simple and effective way to identify these flaws.
  • Iterations (burst): Given a period (for example, 60 seconds) determine how many times a piece of logic can be executed in sequence.
  • Duration (burst): Given a target number of executions (for example, calling a function 1000 times in sequence) determine how long will it take to complete.
  • Load (concurrency): Very often overlooked and probably the most important, especially when it comes down to connecting to other services like the schema, caching, or 3rd party APIs. How does the piece of logic perform under load? Simulating load with a tool like Grafana k6 or Apache JMeter is essential to determine performance.

Calling the community

This project is nowhere near reaching maturity yet, but I believe there’s enough value to continue the pursuit. I’m sure I will continuously revisit it to theory craft concepts, as I have found the value and proved it already. I’m calling on the community and some energetic developers to get inspired by the concept to drive it further.

Depending on the feedback from the community, I would like to close some of the open loops. I’m currently focused on finding my next challenge and would like to invite anyone willing to create some pull requests.

Roadmap (Wishlist)

  • Upgrade to Laravel v10
  • Automated testing with k6 — preferably cloud-based with local support and integrate into the CI/CD pipeline
  • Simple web interface that supports real-time reporting
  • Gamification — introduce a way to grow the preset challenges in a way that gives contributors credit

Shoutout to Jason Viljoen for helping me experiment with this concept and reviewing this story.

--

--

Hendrik Prinsloo
Hendrik Prinsloo

Written by Hendrik Prinsloo

Full Stack Developer ● Toaster mechanic ● Technical sales advisor ● Forgotten password specialist ● Let-me-google-that-for-you expert

No responses yet