<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ibrahim Hajjaj</title>
    <description>The latest articles on DEV Community by Ibrahim Hajjaj (@ibrahimwithi).</description>
    <link>https://dev.to/ibrahimwithi</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4110222%2F4f26e8a9-a63a-4a5c-ac9d-3c08a7622d9b.jpg</url>
      <title>DEV Community: Ibrahim Hajjaj</title>
      <link>https://dev.to/ibrahimwithi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ibrahimwithi"/>
    <language>en</language>
    <item>
      <title>Your benchmark's single score is a weighting nobody agreed to</title>
      <dc:creator>Ibrahim Hajjaj</dc:creator>
      <pubDate>Mon, 07 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/ibrahimwithi/your-benchmarks-single-score-is-a-weighting-nobody-agreed-to-5ge7</link>
      <guid>https://dev.to/ibrahimwithi/your-benchmarks-single-score-is-a-weighting-nobody-agreed-to-5ge7</guid>
      <description>&lt;p&gt;Disclosure before anything else, because it is the thing that should make you read the rest suspiciously: I sell one of the nine plugins in the benchmark this post is about. It is currently second on the board.&lt;/p&gt;

&lt;p&gt;I built the benchmark to answer a simple question. Which WordPress backup plugin is fastest? Two plugins get byte-identical fixed snapshots on identical sites, backup and restore run in containers with no outbound network, each pairing does a warm-up and three measured runs, and the results feed an Elo rating.&lt;/p&gt;

&lt;p&gt;The measuring was the easy part. The hard part was working out whether the number at the top of the page was allowed to exist at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two axes disagree, and not by accident
&lt;/h2&gt;

&lt;p&gt;Every run produces two things worth comparing: how long the backup took, and how big the archive came out. Rank the field by each and you get two orders.&lt;/p&gt;

&lt;p&gt;Kendall's tau between them is &lt;strong&gt;-0.50&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is not "unrelated". That is actively opposed. Across the whole field, the plugins that finish fastest write the biggest archives. Which, stated that way, is obvious: compression costs CPU time, and skipping it buys you seconds and costs you bytes. What is less obvious is what it does to a leaderboard.&lt;/p&gt;

&lt;p&gt;If I rank by speed, I am partly ranking by who compresses least. Not as a quirk of one plugin. As a rule, across nine of them.&lt;/p&gt;

&lt;p&gt;Six of the nine are Pareto-optimal: no other plugin beats them on both speed and size at once. Only three are beaten on both axes and can be honestly called worse. For the other six, "better" is a question about your constraint, not about the software.&lt;/p&gt;

&lt;p&gt;So any single ranking number I publish is a weighting between two axes that disagree. I would be picking that weighting on your behalf, applying it to your decision, and not telling you I had done it.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is settled ground, and someone already wrote the rules
&lt;/h2&gt;

&lt;p&gt;I went looking for whether serious benchmark bodies had a position on composite scores. They do, and it is stricter than I expected.&lt;/p&gt;

&lt;p&gt;SPEC's &lt;a href="https://www.spec.org/fairuse/" rel="noopener noreferrer"&gt;Fair Use rules&lt;/a&gt; have a whole section on what they call a &lt;em&gt;derived value&lt;/em&gt;: any number that is a function of a benchmark metric plus something else. Composites are explicitly allowed. What is not allowed is presenting one as the benchmark's own metric.&lt;/p&gt;

&lt;p&gt;Their worked example is close enough to my problem to sting. A gaming society builds a composite from a weighted subset of one SPEC benchmark plus a weighted subset of another, and calls it "GamePerfMark". SPEC's ruling: the composite may be useful and interesting, the weighting and the subsetting were done by the society rather than by SPEC, and calling it "SPECgame" would be a violation.&lt;/p&gt;

&lt;p&gt;Two other rules in the same document do real work:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The basis for comparison must be stated.&lt;/strong&gt; Not "the fastest". The fastest &lt;em&gt;among what&lt;/em&gt;, measured &lt;em&gt;how&lt;/em&gt;, retrieved &lt;em&gt;when&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Several benchmarks define &lt;strong&gt;required metrics&lt;/strong&gt; that must be quoted alongside any headline figure, so the flattering number cannot travel on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is about honesty in the abstract. It is a description of how a number gets misused once it leaves the page it was measured on, written by people who watched it happen for thirty years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second place a number gets overconfident
&lt;/h2&gt;

&lt;p&gt;Ranking is one problem. The error bar next to the ranking is another, and it took a real bug to show me.&lt;/p&gt;

&lt;p&gt;The confidence interval started as the textbook approximation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CI = 1.96 * (400 / sqrt(n))      n = battles played

    5 battles  -&amp;gt;  +/- 351
   50 battles  -&amp;gt;  +/- 111
  200 battles  -&amp;gt;  +/-  55
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More battles, tighter interval. That formula assumes each battle is an independent observation.&lt;/p&gt;

&lt;p&gt;Then I looked at a record where 24 of 40 battles were failures, and every one of the 24 was the same fault reproduced from one bug.&lt;/p&gt;

&lt;p&gt;That is one fact about the plugin, observed 24 times. It is not 24 facts. Feed it in as 24 and &lt;code&gt;sqrt(n)&lt;/code&gt; hands you an interval that claims a precision the evidence does not have.&lt;/p&gt;

&lt;p&gt;The fix is the cluster-sampling design effect, taken at its most conservative: outcomes sharing a root cause are treated as perfectly correlated, so a cluster of any size counts once.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;effectiveSampleSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;battleCount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;clusterSizes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;battleCount&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;effective&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;battleCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;clusterSizes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;effective&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// Never claim fewer observations than the number of distinct things seen.&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;effective&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;battleCount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clusterSizes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Failures are grouped by harness version, hosting tier and phase. Forty battles with one cluster of 24 become 17 effective observations, and the interval goes from ±124 to ±190. Wider, which is the entire point, and wider by an amount you can check: &lt;code&gt;1.96 * (400 / sqrt(17))&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The part I want to be clear about, because it is the part that makes this honest rather than convenient: &lt;strong&gt;the rating itself does not move.&lt;/strong&gt; The losses are real, the plugin earned them, and nothing about the cluster makes them count for less. Only the stated uncertainty widens, and it widens against the plugin's own record looking decisive.&lt;/p&gt;

&lt;p&gt;The test that guards it is a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;widens the interval rather than narrowing it&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;honest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateConfidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;effectiveSampleSize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;naive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculateConfidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;honest&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeGreaterThan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;naive&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What that costs the person who built it
&lt;/h2&gt;

&lt;p&gt;The board carries two Elo columns, because of everything above. One for speed, one overall. My plugin, SafeGuard, is &lt;strong&gt;second on the speed board at 1268&lt;/strong&gt;. BackWPup is third at 1263. Both carry ±113.&lt;/p&gt;

&lt;p&gt;Five points inside a ±113 interval is a tie. Not a narrow win, not "edges out". A tie, and the page says so in those words rather than letting me quote the higher number and walk away.&lt;/p&gt;

&lt;p&gt;Now the part I could have quietly left out. On the &lt;em&gt;overall&lt;/em&gt; column SafeGuard is first, at 1442 against All-in-One WP Migration's 1238. That is a 204-point gap, and it is the number I would use if I were selling. Their intervals still overlap by 22 points, so I am not going to call that settled either.&lt;/p&gt;

&lt;p&gt;Both figures are true. They are on the same page, in the same row, and which one you meet first is a design decision I made. That is the entire argument of this post, aimed at myself: a leaderboard is an editorial act, and the only defence is showing the reader the axis you sorted on and the one you did not.&lt;/p&gt;

&lt;p&gt;I also had to check whether a single rating was even valid here, because Elo assumes transitivity: if A beats B and B beats C, A should beat C. When that fails, ratings start reflecting who happened to be scheduled against whom. Measured across the pairwise tier medians, the cyclic component sits at 0.48 of the transitive one for speed and 0.52 for size. Both comfortably below 1, so ranking each axis on its own holds up. The rating system was not the problem. The single number on top of it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things to check in any benchmark you read
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the headline one number, and does the page say what it weighted?&lt;/strong&gt; If it is a composite and the weighting is not stated, you are reading somebody's opinion with a decimal point on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do the axes agree?&lt;/strong&gt; If the benchmark reports more than one thing, ask whether they move together. If they move against each other, ranking by one is silently ranking by the inverse of the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Does the error bar assume independence it does not have?&lt;/strong&gt; A record where one bug decided half the results is one observation wearing a large number. And check the arithmetic while you are there: I found a stale figure in my own methodology page writing this post, an interval quoted as ±453 where the formula gives ±190, because somebody had passed the number of clusters into &lt;code&gt;sqrt(n)&lt;/code&gt; instead of the effective sample size. The correction was right and the number printed beside it was not.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The benchmark is at &lt;a href="https://backuparena.com" rel="noopener noreferrer"&gt;backuparena.com&lt;/a&gt;. 581 completed runs, 180 scored, 401 excluded, and the methodology page carries all of the above in more detail than this post. I should be precise about that exclusion number rather than let it sound braver than it is: 360 of the 401 are simply superseded by a later run of the same pairing, which is bookkeeping. Forty-one were excluded for cause. Every one of the 401 is browsable at &lt;a href="https://backuparena.com/battles?status=excluded" rel="noopener noreferrer"&gt;/battles?status=excluded&lt;/a&gt;, and opening any row shows the reason it was thrown out.&lt;/p&gt;

&lt;p&gt;The largest of those is worth stating because it is the kind of fault that flatters nobody. On the media-heavy profile our archive lister read one part of a multi-part archive at a time, so two plugins recorded 28,801 upload files against 30,582 for every other plugin. A silent undercount, penalising exactly the two plugins that happened to split their archives, and nothing about the numbers looked wrong.&lt;/p&gt;

&lt;p&gt;The code is not public, and that is a real limitation rather than an oversight. Every claim above is a measurement I can show you the log for. It is not one you can rerun yourself, and I would rather write that sentence than have somebody else write it for me.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>testing</category>
      <category>programming</category>
      <category>wordpress</category>
    </item>
  </channel>
</rss>
