<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Performance on Murat Useinov</title>
    <link>https://useinov.com/tags/performance/</link>
    <description>Recent content in Performance on Murat Useinov</description>
    <generator>Hugo</generator>
    <language>en-us</language>
    <lastBuildDate>Wed, 20 Aug 2025 20:10:00 +0300</lastBuildDate>
    <atom:link href="https://useinov.com/tags/performance/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>vendor/autoload.php is not free</title>
      <link>https://useinov.com/posts/2025/autoload-not-free/</link>
      <pubDate>Wed, 20 Aug 2025 20:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/autoload-not-free/</guid>
      <description>&lt;p&gt;46 milliseconds. That is how long the bootstrap of one large application took before the framework even started, in a trace I pulled this week. A good share of it was autoloading.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;require vendor/autoload.php&lt;/code&gt; looks like a constant of nature. With plain PSR-4 rules it is a loop: every class load walks the prefixes and asks the filesystem whether a file exists. A few thousand classes on a cold request, and the loop becomes a number you can see in a flame graph.&lt;/p&gt;</description>
    </item>
    <item>
      <title>One TTL does not fit the whole page</title>
      <link>https://useinov.com/posts/2025/one-ttl-lie/</link>
      <pubDate>Mon, 16 Jun 2025 21:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2025/one-ttl-lie/</guid>
      <description>&lt;p&gt;&amp;ldquo;Hello, Ivan&amp;rdquo; in the header. That is what broke the full-page cache on one project: either the whole page became uncacheable, or Ivan&amp;rsquo;s greeting was served to strangers. We managed both, in the same month.&lt;/p&gt;
&lt;p&gt;Full-page cache behind a reverse proxy is a great first step. It dies on the first personalized element.&lt;/p&gt;
&lt;p&gt;The way out is to stop thinking of the page as one object. It is regions with different speeds. The header with the user name changes per user. Navigation changes per deploy. The product list changes every few minutes. Prices and stock change every few seconds. Give all of that one TTL and you must pick the minimum, which means almost no cache at all.&lt;/p&gt;</description>
    </item>
    <item>
      <title>The fastest request never reaches PHP</title>
      <link>https://useinov.com/posts/2024/http-caching/</link>
      <pubDate>Tue, 03 Dec 2024 12:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/http-caching/</guid>
      <description>&lt;p&gt;One header, and the FPM load graph fell off a cliff.&lt;/p&gt;
&lt;pre tabindex=&#34;0&#34;&gt;&lt;code&gt;Cache-Control: public, s-maxage=60
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;A public catalog page on one project renders the same HTML for every anonymous visitor. Same queries, same JSON from the search service, same template, thousands of times per hour. We profiled it, we tuned it, and only then asked the obvious question: why is PHP involved in the second request at all.&lt;/p&gt;
&lt;p&gt;HTTP had the answer before my career started. &lt;code&gt;public&lt;/code&gt; says a shared cache may store the response. &lt;code&gt;s-maxage&lt;/code&gt; gives the CDN or reverse proxy its own lifetime, separate from browser &lt;code&gt;max-age&lt;/code&gt;. With Varnish in front, the request path splits in two. MISS: full stack, FPM, database, sixty milliseconds. HIT: the proxy answers from memory, the PHP process never starts, the database never hears about it. Nothing in the application got faster. There was simply less application running.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Pick the library with a profiler, not a chart</title>
      <link>https://useinov.com/posts/2024/profiler-not-benchmark/</link>
      <pubDate>Wed, 07 Aug 2024 20:50:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/profiler-not-benchmark/</guid>
      <description>&lt;p&gt;Two hundred thousand ops per second against eighty. That was the bar chart, and the team was ready to pick a serializer from it. I asked for one day.&lt;/p&gt;
&lt;p&gt;The day went like this.&lt;/p&gt;
&lt;p&gt;Take a representative workload. Not a synthetic three-field object, but a hundred real payloads from production logs, with the nested structures, the nullable mess, the one field that is sometimes a list and sometimes a map. Anonymize them, save as fixtures. This step is the whole method. The bar chart was measured on someone else&amp;rsquo;s data with someone else&amp;rsquo;s configuration, and a serializer is exactly the kind of code whose cost depends on the shape of the input.&lt;/p&gt;</description>
    </item>
    <item>
      <title>A faster sprintf will not fix your p99</title>
      <link>https://useinov.com/posts/2024/microbenchmark-trap/</link>
      <pubDate>Tue, 18 Jun 2024 23:15:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2024/microbenchmark-trap/</guid>
      <description>&lt;p&gt;Three times faster. That is the number in the benchmark posts about PHP 8.4, where simple &lt;code&gt;sprintf()&lt;/code&gt; calls with basic placeholders get turned into plain string operations at compile time. The function call disappears.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1000000&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;++&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nv&#34;&gt;$s&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;sprintf&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;%s-%d&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$prefix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;);&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I like this work. I do not like how people read it.&lt;/p&gt;
&lt;p&gt;A microbenchmark measures one function in a loop, alone, with a warm cache and nothing else to do. Now profile a real request from the same application. In every backend I have profiled the picture has the same shape: most of the wall time is waiting. SQL queries, Redis round trips, an HTTP call to some partner API. The CPU spent inside PHP is a modest slice, and &lt;code&gt;sprintf&lt;/code&gt; inside that slice is a rounding error. Three times faster rounding error is still a rounding error.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Fast query, slow endpoint</title>
      <link>https://useinov.com/posts/2022/hydration-cost/</link>
      <pubDate>Tue, 23 Aug 2022 15:10:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2022/hydration-cost/</guid>
      <description>&lt;p&gt;The profiler said the query takes 20 ms. The export endpoint took 900 ms and a quarter of a gigabyte of memory. The missing 880 ms was Doctrine turning five thousand rows into five thousand entities.&lt;/p&gt;
&lt;p&gt;Hydration is not free. For every row the ORM builds an object, fills properties through reflection, registers it in the unit of work, creates proxies for relations. Per row it is nothing. Times five thousand, it is the endpoint. The profiler shows SQL because SQL is easy to show.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 14 and the price of a connection</title>
      <link>https://useinov.com/posts/2021/postgres-14-connections/</link>
      <pubDate>Thu, 30 Sep 2021 10:05:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2021/postgres-14-connections/</guid>
      <description>&lt;p&gt;A few hundred connections, mostly idle, and the whole server gets slower. That was Postgres for as long as I remember it: a process per connection, and past some count the snapshot bookkeeping starts to eat everything.&lt;/p&gt;
&lt;p&gt;PostgreSQL 14 is released today. I had the RC on a test box for a week, so a few notes from a backend seat. The headline for me is that this internal work got a serious rewrite. On our connection-heavy profile, many workers, short queries, lots of idle time, the difference is visible without squinting. Idle connections finally cost close to what people always assumed they cost.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 8.0: ignore the JIT, take the rest</title>
      <link>https://useinov.com/posts/2020/php8-new-baseline/</link>
      <pubDate>Fri, 27 Nov 2020 21:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/php8-new-baseline/</guid>
      <description>&lt;p&gt;JIT on, JIT off, the same Symfony endpoint. Difference within noise. Same for a Laravel endpoint. PHP 8.0 came out yesterday and this was the first thing I checked, because every headline is about the JIT.&lt;/p&gt;
&lt;p&gt;Not a scandal. A web request spends its time in I/O, in MySQL, in framework code full of method calls the JIT cannot do much with. JIT is for tight numeric loops, and your controller has none. If you compute fractals in PHP, congratulations. The rest of us can leave it off.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Stop building running totals in PHP</title>
      <link>https://useinov.com/posts/2020/window-functions-mysql/</link>
      <pubDate>Tue, 06 Oct 2020 15:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/window-functions-mysql/</guid>
      <description>&lt;p&gt;Two hundred thousand rows over the wire to compute forty numbers. That was our monthly report on MySQL 5.7: fetch all transactions for the period, loop in PHP, accumulate a running balance, compare each row with the previous one, rank customers by volume. Classic 5.7 shape, because the database could not say &amp;ldquo;previous row&amp;rdquo; or &amp;ldquo;rank within group&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;We are finally moving that project to MySQL 8. Two years after GA, which by database standards is reckless haste. The first win had nothing to do with performance. We deleted PHP.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Average response time explains nothing</title>
      <link>https://useinov.com/posts/2020/average-explains-nothing/</link>
      <pubDate>Tue, 04 Aug 2020 20:40:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2020/average-explains-nothing/</guid>
      <description>&lt;p&gt;Average response time: 180 ms. Ticket from support: &amp;ldquo;the API is slow&amp;rdquo;. Both true. The average is a diplomat. It offends nobody and tells you nothing.&lt;/p&gt;
&lt;p&gt;Latency is a distribution. Our 180 ms hides a p50 of 90 ms and a p99 above four seconds. One request in a hundred is terrible, and with thirty requests per page load, most users hit that unlucky one regularly. The people complaining are not imagining things. They live in the tail, and the average never visits there.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 7.4: typed properties first, preloading later</title>
      <link>https://useinov.com/posts/2019/php74-preloading/</link>
      <pubDate>Fri, 29 Nov 2019 15:25:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/php74-preloading/</guid>
      <description>&lt;p&gt;Two lines of a class, and half of my docblocks can be deleted:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-php&#34; data-lang=&#34;php&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Money&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;int&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$amount&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;private&lt;/span&gt; &lt;span class=&#34;nx&#34;&gt;string&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;$currency&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PHP 7.4 came out yesterday, and this is the part I start using on Monday. Those docblocks existed only to say &lt;code&gt;@var int&lt;/code&gt;. Now the language says it, and unlike the docblock, it checks. Assign a string, get a &lt;code&gt;TypeError&lt;/code&gt; at the assignment, not a strange bug three layers later.&lt;/p&gt;
&lt;p&gt;There is one new state to learn. A typed property without a default is uninitialized, and reading it before the first write throws. This is the feature. &amp;ldquo;Object exists but is not filled yet&amp;rdquo; finally fails loudly instead of pretending to be null.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PostgreSQL 12 inlines your CTE</title>
      <link>https://useinov.com/posts/2019/postgres12-cte/</link>
      <pubDate>Fri, 18 Oct 2019 21:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/postgres12-cte/</guid>
      <description>&lt;p&gt;&lt;code&gt;grep -rn &amp;quot;WITH &amp;quot; src/&lt;/code&gt; was the first thing I ran after upgrading to PostgreSQL 12 this month. I was looking for a trick that had stopped working.&lt;/p&gt;
&lt;p&gt;For years &lt;code&gt;WITH&lt;/code&gt; was an optimization fence. The planner materialized the CTE first and only then ran the outer query. Everyone used this both ways. As a bug: you wrap a subquery into a CTE for readability, the planner stops pushing conditions inside, a fast query becomes a scan of half a table. As a feature: you write a CTE on purpose, to pin evaluation order and stop the planner from being creative. Half of the CTE advice on the internet is really advice about the fence.&lt;/p&gt;</description>
    </item>
    <item>
      <title>N&#43;1 hides on your laptop</title>
      <link>https://useinov.com/posts/2019/n-plus-one-production/</link>
      <pubDate>Fri, 05 Jul 2019 22:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2019/n-plus-one-production/</guid>
      <description>&lt;p&gt;Ten rows in the dev database, on the same machine, 0.1 ms per query. Ten extra queries is one millisecond. The page feels instant, the code ships. Production has a thousand rows and the database one network hop away. Round trip is about a millisecond even in a good datacenter. A thousand queries is a second of pure network waiting. Not slow SQL. Each query is fast. The plural is slow.&lt;/p&gt;</description>
    </item>
    <item>
      <title>PHP 7 measured on real code</title>
      <link>https://useinov.com/posts/2015/php7-measured/</link>
      <pubDate>Sat, 26 Dec 2015 21:35:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/php7-measured/</guid>
      <description>&lt;p&gt;One endpoint. The heaviest catalog page of one project, same data, same opcache settings, PHP 5.6 against 7.0. That is the only benchmark I trust. 7.0 has been out for three weeks, everyone has seen the hello-world numbers, and I do not believe hello-world numbers on principle.&lt;/p&gt;
&lt;p&gt;Response time dropped about 40 percent. Memory per request, more than half. No code changes. I ran it again because I did not believe the first numbers either. The engine rewrite is real: smaller internal structures, cheaper function calls, and typical framework code is exactly that, thousands of small calls and arrays.&lt;/p&gt;</description>
    </item>
    <item>
      <title>Where Doctrine spends your milliseconds</title>
      <link>https://useinov.com/posts/2015/doctrine-latency/</link>
      <pubDate>Thu, 23 Apr 2015 10:45:00 +0300</pubDate>
      <guid>https://useinov.com/posts/2015/doctrine-latency/</guid>
      <description>&lt;p&gt;&lt;code&gt;$order-&amp;gt;getCustomer()-&amp;gt;getName()&lt;/code&gt; inside a &lt;code&gt;foreach&lt;/code&gt;. Every time I open a slow Doctrine endpoint, I find some version of this line within ten minutes. The PHP is rarely slow. The shape of the data access is.&lt;/p&gt;
&lt;p&gt;Same few places every time.&lt;/p&gt;
&lt;p&gt;Lazy loading. The classic N+1. Doctrine proxies make it invisible: you touch the relation in a loop and every touch is a query. The profiler query count tells the truth. The code does not.&lt;/p&gt;</description>
    </item>
  </channel>
</rss>
