<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>BSD Ports blog</title>
  <link href="https://daemonports.org/blog/" rel="alternate"/>
  <link href="https://daemonports.org/blog/feed.xml" rel="self"/>
  <id>https://daemonports.org/blog/</id>
  <updated>2026-10-21T22:53:39Z</updated>
  <entry>
    <title>ssg 1.8.76: the same speed, less memory, three workarounds fewer</title>
    <link href="https://daemonports.org/blog/ssg-1-8-76/"/>
    <id>https://daemonports.org/blog/ssg-1-8-76/</id>
    <published>2026-10-21T23:00:00Z</published>
    <updated>2026-10-21T22:53:39Z</updated>
    <content type="html">&lt;p&gt;This site is built by &lt;a href=&#34;https://ssg.tradik.com/&#34;&gt;ssg&lt;/a&gt;, and since tonight&#39;s refresh
by version 1.8.76. Was it faster? Not measurably. It used less memory, and it
fixed three bugs this site had found and worked around, which mattered more.&lt;/p&gt;
&lt;h2 id=&#34;time-within-the-noise&#34;&gt;Time: within the noise&lt;/h2&gt;
&lt;p&gt;The server rebuilds all 110,689 pages on every refresh, and the
&lt;a href=&#34;https://daemonports.org/stats/&#34;&gt;stats page&lt;/a&gt; records each build. Four builds on 1.8.74 took 245 to
253 seconds; the first on 1.8.76 took 249.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/ssg-1876-time.svg&#34; alt=&#34;Build time per refresh: 245, 253, 253 and 251 seconds on 1.8.74, 249 on 1.8.76&#34;&gt;&lt;/p&gt;
&lt;p&gt;Builds on the same version already differ by eight seconds, so one build four
seconds under the average says nothing yet. On a workstation the same build
went from 2 min 21 s to 2 min 10 s, but that is one run each on a machine
doing other work. If 1.8.76 is faster, the next days of refreshes on the stats
page will show it; one run does not.&lt;/p&gt;
&lt;h2 id=&#34;memory-about-5-less&#34;&gt;Memory: about 5% less&lt;/h2&gt;
&lt;p&gt;The peak memory of the build moved, on the server and on the workstation
alike:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/ssg-1876-memory.svg&#34; alt=&#34;Peak memory per refresh: 6.11 to 6.20 GB on 1.8.74, 5.83 GB on 1.8.76&#34;&gt;&lt;/p&gt;
&lt;p&gt;From 6.11–6.20 GB to 5.83 GB on the server, from 5.64 to 5.35 GB on the
workstation: about 300 MB less, below the spread of the old runs and in the
same direction in both places. For a build that has to fit beside a search
engine holding 2.4 GB, that is the useful part.&lt;/p&gt;
&lt;h2 id=&#34;three-bugs-fewer&#34;&gt;Three bugs fewer&lt;/h2&gt;
&lt;p&gt;1.8.75 and 1.8.76 fixed three bugs reported from this site the same day they
were found. Each had a workaround here; each workaround is gone:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;What it did here&lt;/th&gt;
&lt;th&gt;Workaround, now removed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://github.com/spagu/ssg/issues/342&#34;&gt;#342&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;the build report &lt;code&gt;build.json&lt;/code&gt; was written readable by its owner only, and the web server answered 403&lt;/td&gt;
&lt;td&gt;a &lt;code&gt;chmod&lt;/code&gt; over every published copy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://github.com/spagu/ssg/issues/344&#34;&gt;#344&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a taxonomy mapped to &lt;code&gt;maintainerTerm&lt;/code&gt; also read the raw &lt;code&gt;maintainer&lt;/code&gt;, so &lt;code&gt;ports@MidnightBSD..org&lt;/code&gt; stopped the build&lt;/td&gt;
&lt;td&gt;&lt;code&gt;field: maintainerTerm&lt;/code&gt; in the configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href=&#34;https://github.com/spagu/ssg/issues/346&#34;&gt;#346&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;two pages at one address overwrote each other without a word: the stats page lost its address to FreeBSD&#39;s &lt;code&gt;benchmarks&lt;/code&gt; category&lt;/td&gt;
&lt;td&gt;none possible in ssg; the stats page moved to &lt;code&gt;/stats/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The last one is worth a sentence more. A page that silently vanishes is found
only by someone who goes looking for it, and here someone did: the menu said
Benchmarks and the page showed FreeBSD&#39;s benchmarking tools. ssg now stops
the build when two documents claim one address and names both. The data tool
behind this site keeps its own check as well, because it can fail in seconds,
before the search engine is loaded and before the four-minute build starts.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>What the disks cost the search index</title>
    <link href="https://daemonports.org/blog/loading-the-search-index/"/>
    <id>https://daemonports.org/blog/loading-the-search-index/</id>
    <published>2026-10-21T21:45:00Z</published>
    <updated>2026-10-21T22:53:39Z</updated>
    <content type="html">&lt;p&gt;Adding four ports trees meant loading about 70,000 new documents into
&lt;a href=&#34;https://mddb.tradik.com/&#34;&gt;mddb&lt;/a&gt;, the search engine behind the search box,
plus one update of every FreeBSD document. It took 30 minutes, about &lt;strong&gt;125
documents a second&lt;/strong&gt;. mddb&#39;s own benchmark speaks of thousands. This is what
the difference was.&lt;/p&gt;
&lt;h2 id=&#34;half-the-time-was-waiting-for-the-disk&#34;&gt;Half the time was waiting for the disk&lt;/h2&gt;
&lt;p&gt;The server has two 7200 rpm disks from 2011 in a ZFS mirror, with no separate
log device, so every &lt;code&gt;fsync&lt;/code&gt; waits for a platter. dtrace counted the fsync
calls mddbd made in ten seconds of the load:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/timesplit.svg&#34; alt=&#34;Of 10 seconds, 5.1 were spent in 758 fsync calls&#34;&gt;&lt;/p&gt;
&lt;p&gt;758 calls, 6.75 ms each. A request of 500 documents should need about two
commits, one for the documents and one for the full-text index, since mddb
learned to index a whole batch in one transaction. It made about 300 fsyncs
per request instead.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/writepath.svg&#34; alt=&#34;The write path of one add-batch request&#34;&gt;&lt;/p&gt;
&lt;p&gt;Where the commits beyond those two come from is not settled. The metadata
index queue commits once per document, which is the first suspect, but
dtrace could not attach to the Go binary to prove it. The measurement is
reported to mddb as &lt;a href=&#34;https://github.com/tradik/mddb/issues/306&#34;&gt;tradik/mddb#306&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;measured-again-the-same-way-everywhere&#34;&gt;Measured again, the same way everywhere&lt;/h2&gt;
&lt;p&gt;A first guess from a laptop said &amp;quot;sixteen times slower&amp;quot;, and it was wrong:
the laptop and the server were not doing the same thing. So the same 5,000
documents went into an empty database, 500 per request, from the same
client, in four places:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/load-rate.svg&#34; alt=&#34;Documents loaded per second: server with sync 682, server without sync 1,340, SSD 2,034, tmpfs 2,091&#34;&gt;&lt;/p&gt;
&lt;p&gt;On an empty database, the disks cost a factor of two, not sixteen. The rest
of the gap belongs to size: the full load wrote a 2.5 GB file for 105,124
documents, about 24 KB each, and each commit on a large tree touches more
pages than on a small one. The rate fell as the file grew.&lt;/p&gt;
&lt;p&gt;The size of a request, which looked like the culprit for a while, is not:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/batch-sweep.svg&#34; alt=&#34;Documents per second by request size, from 250 to 5,000: flat around 2,000&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;what-changed&#34;&gt;What changed&lt;/h2&gt;
&lt;p&gt;mddb&#39;s database is an index of the ports trees. It can be rebuilt from them
at any time, so it does not need the durability &lt;code&gt;fsync&lt;/code&gt; buys. It now lives on
a ZFS dataset of its own with &lt;code&gt;sync=disabled&lt;/code&gt; (and 16 KB records, closer to
BoltDB&#39;s pages), and only that dataset: the rest of the server is unchanged.
Should the server lose power, the worst case is a full reload, which the
refresh does by itself.&lt;/p&gt;
&lt;p&gt;A refresh sends only what changed: the first one after the move updated four
documents in two seconds. The &lt;a href=&#34;https://daemonports.org/stats/&#34;&gt;benchmarks page&lt;/a&gt; now times ten
searches after every refresh, shows how many documents and revisions mddb
holds and what the last load sent, so a change like this one shows up in
numbers rather than in a feeling that the site is slow.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Five ports trees on one site</title>
    <link href="https://daemonports.org/blog/five-ports-trees/"/>
    <id>https://daemonports.org/blog/five-ports-trees/</id>
    <published>2026-10-21T21:00:00Z</published>
    <updated>2026-10-21T22:53:39Z</updated>
    <content type="html">&lt;p&gt;Until today this site knew one tree, FreeBSD&#39;s. It now reads five: FreeBSD&#39;s
ports, OpenBSD&#39;s ports, NetBSD&#39;s pkgsrc, DragonFly BSD&#39;s DPorts and
MidnightBSD&#39;s mports. That is about 105,000 ports and 110,000 pages, rebuilt
every 30 minutes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/ports-per-system.svg&#34; alt=&#34;Ports per system: FreeBSD 35,268, DragonFly 29,359, NetBSD 19,357, OpenBSD 10,576, MidnightBSD 10,528&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;each-tree-read-the-way-it-is-published&#34;&gt;Each tree, read the way it is published&lt;/h2&gt;
&lt;p&gt;No two of them publish their tree the same way, so each is read from what its
project already ships:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Read from&lt;/th&gt;
&lt;th&gt;Gives&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FreeBSD&lt;/td&gt;
&lt;td&gt;the &lt;code&gt;INDEX&lt;/code&gt;, the ports tree, &lt;code&gt;pkg.freebsd.org&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;versions, options, files, history, package versions per release&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenBSD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sqlports&lt;/code&gt;, the SQLite database of the whole tree, shipped as a package&lt;/td&gt;
&lt;td&gt;every Makefile variable, dependencies by kind&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NetBSD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pkg_summary&lt;/code&gt; of the binary packages, the pkgsrc tree&lt;/td&gt;
&lt;td&gt;versions, licences, run dependencies; maintainers and history from the tree&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DragonFly&lt;/td&gt;
&lt;td&gt;&lt;code&gt;packagesite.pkg&lt;/code&gt;, as &lt;code&gt;pkg&lt;/code&gt; reads it&lt;/td&gt;
&lt;td&gt;versions, sizes, run dependencies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MidnightBSD&lt;/td&gt;
&lt;td&gt;the mports tree, Makefile by Makefile&lt;/td&gt;
&lt;td&gt;everything a Makefile states&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://daemonports.org/images/blog/pipeline.svg&#34; alt=&#34;From five ports trees to pages and a search index&#34;&gt;&lt;/p&gt;
&lt;p&gt;Two of these needed more than a parser. OpenBSD signs its packages with
signify, which keeps the signature, a hash per 64 KB block, in the gzip
header&#39;s comment field. For the 46 MB sqlports package that comment runs to
tens of kilobytes, and Go&#39;s &lt;code&gt;compress/gzip&lt;/code&gt; refuses any header field longer
than 512 bytes. The header is now skipped by hand and the body inflated as
it is. MidnightBSD&#39;s package index names packages but not their origins, so
mports is read as a tree, the same way an overlay repository is.&lt;/p&gt;
&lt;h2 id=&#34;the-same-software-side-by-side&#34;&gt;The same software, side by side&lt;/h2&gt;
&lt;p&gt;A port is now identified by its system and its origin, so &lt;code&gt;www/nginx&lt;/code&gt; is five
ports with five pages: FreeBSD&#39;s at &lt;code&gt;/www/nginx/&lt;/code&gt;, as before, the others
under the system&#39;s own path, such as &lt;code&gt;/openbsd/www/nginx/&lt;/code&gt;. Each page lists
the same software in the other systems:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;System&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FreeBSD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.30.6_1,3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenBSD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.30.6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NetBSD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.30.5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DragonFly&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.26.3_3,3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MidnightBSD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;www/nginx&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;1.30.1,3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The match is the same origin first, else the one port of the same package
name in that system. Where a system has two ports of one name, &lt;code&gt;lua51&lt;/code&gt; and
&lt;code&gt;lua54&lt;/code&gt; say, nothing is matched, since either could be the wrong one.&lt;/p&gt;
&lt;h2 id=&#34;what-the-data-turned-up&#34;&gt;What the data turned up&lt;/h2&gt;
&lt;p&gt;Reading five trees at once finds what reading one never does:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;MidnightBSD&#39;s maintainer lines&lt;/strong&gt; spell its ports address about a dozen
ways, among them &lt;code&gt;ports@MidnightBSD..org&lt;/code&gt; and &lt;code&gt;ports@MidnightBSD&amp;gt;org&lt;/code&gt;. Spellings
that make the same page now share one maintainer page.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;DPorts copies FreeBSD&#39;s maintainer lines&lt;/strong&gt;, so a FreeBSD maintainer would
appear to maintain every port twice. DragonFly&#39;s ports show the maintainer
but stay off the maintainer pages.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;More than a thousand DragonFly packages&lt;/strong&gt; give their homepage as
&lt;code&gt;UNKNOWN&lt;/code&gt;. A homepage
is now linked only when it is a URL.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Every system has its own tab in the header, its own page of categories and
its own install commands on each port page: &lt;code&gt;pkg install&lt;/code&gt;, &lt;code&gt;pkg_add&lt;/code&gt;,
&lt;code&gt;pkgin install&lt;/code&gt;, &lt;code&gt;mport install&lt;/code&gt;. The search box searches all five, and the
&lt;a href=&#34;https://daemonports.org/stats/&#34;&gt;benchmarks page&lt;/a&gt; shows what a refresh of them costs.&lt;/p&gt;
</content>
  </entry>
</feed>
