{"next_page": 2, "previous_page": null, "max_next_page": 10, "posts": [{"oid": "bun-webview-is-eating-up-my-tmp-storage", "title": "Bun WebView is eating up my tmp storage", "pub_date": "2026-04-29T12:22:01.203Z", "comments": 0, "categories": ["TypeScript", "Bun", "Linux"], "html": "<p><a href=\"/plog/html-getter\">Last week, I wrote about</a> a cool new API in <a href=\"https://bun.com/blog/bun-v1.3.12\">Bun</a> called <code>WebView</code> that makes it possible to open a URL in a headless browser without needing something like <code>Puppeteer</code> or <code>Playwright</code>. The code looks like this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">import</span> { <span class=\"hljs-title class_\">WebView</span> } <span class=\"hljs-keyword\">from</span> <span class=\"hljs-string\">&quot;bun&quot;</span>\n\n<span class=\"hljs-keyword\">await</span> <span class=\"hljs-keyword\">using</span> view = <span class=\"hljs-keyword\">new</span> <span class=\"hljs-title class_\">WebView</span>({ <span class=\"hljs-attr\">width</span>: <span class=\"hljs-number\">2000</span>, <span class=\"hljs-attr\">height</span>: <span class=\"hljs-number\">3000</span> })\n<span class=\"hljs-keyword\">await</span> view.<span class=\"hljs-title function_\">navigate</span>(url)\n\n<span class=\"hljs-comment\">// example, evaluate JavaScript </span>\n<span class=\"hljs-variable language_\">console</span>.<span class=\"hljs-title function_\">log</span>(<span class=\"hljs-keyword\">await</span> view.<span class=\"hljs-title function_\">evaluate</span>(<span class=\"hljs-string\">&quot;document.body.innerHTML&quot;</span>))\n\n<span class=\"hljs-comment\">// example, take a screenshot</span>\n<span class=\"hljs-keyword\">const</span> png = <span class=\"hljs-keyword\">await</span> view.<span class=\"hljs-title function_\">screenshot</span>()\n<span class=\"hljs-keyword\">await</span> <span class=\"hljs-title class_\">Bun</span>.<span class=\"hljs-title function_\">write</span>(values.<span class=\"hljs-property\">screenshot</span>, png)\n</code></pre>\n\n<p>However, there's a drawback: each time I run a process like this on my Ubuntu 20.04 server, it creates directories such as <code>/tmp/snap-private-tmp/snap.chromium/tmp/bun-chrome-80856</code> and <code>/tmp/snap-private-tmp/snap.chromium/tmp/org.chromium.Chromium.YL1S7L</code>.</p>\n<p>After I've run the above Bun code five times, I see this:</p>\n<pre>root@ubuntu-s-2vcpu-8gb-amd-nyc3-01:~# ls -l /tmp/snap-private-tmp/snap.chromium/tmp/\ntotal 0\ndrwx------ 31 django django 740 Apr 29 12:30 bun-chrome-80651\ndrwx------ 31 django django 740 Apr 29 12:30 bun-chrome-80856\ndrwx------ 31 django django 740 Apr 29 12:30 bun-chrome-81058\ndrwx------ 31 django django 740 Apr 29 12:30 bun-chrome-81260\ndrwx------ 31 django django 740 Apr 29 12:30 bun-chrome-81459\ndrwx------  2 django django  80 Apr 29 12:30 org.chromium.Chromium.3zCHrv\ndrwx------  2 django django  80 Apr 29 12:30 org.chromium.Chromium.ImIP4v\ndrwx------  2 django django  80 Apr 29 12:30 org.chromium.Chromium.YL1S7L\ndrwx------  2 django django  80 Apr 29 12:30 org.chromium.Chromium.erkDSy\ndrwx------  2 django django  80 Apr 29 12:30 org.chromium.Chromium.orhmkv</pre>\n\n<p>They're pretty big too.</p>\n<pre># du -sh /tmp/snap-private-tmp/snap.chromium/tmp/bun-chrome-80651/\n4.0M    /tmp/snap-private-tmp/snap.chromium/tmp/bun-chrome-80651/\n# du -sh /tmp/snap-private-tmp/snap.chromium/tmp/\n21M /tmp/snap-private-tmp/snap.chromium/tmp/</pre>\n\n<p>Here, that <code>bun-chrome-80651</code> was \"only\" 4MB. If you open a more busy web site, I've seen it become around 25MB.</p>\n<p>I learned this the hard way because I server nearly died from exhausted disk usage since that directory was taking up ~4GB.</p>\n<p>Now I have bash cron script that periodically deletes these files.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "html-getter", "title": "html-getter - A powerfully simple HTML scraper in Bun", "pub_date": "2026-04-17T13:34:17.084Z", "comments": 0, "categories": ["TypeScript", "macOS", "Bun"], "html": "<p>When <a href=\"https://bun.com/blog/bun-v1.3.12\">Bun v1.3.12</a> was released last week, they subtly included a powerful feature, almost under the radar: <code>WebView</code>. It's like Playwright/Puppeteer but more limited, especially in terms of documentation.</p>\n<p>I built a demo app based on it called <a href=\"https://github.com/peterbe/html-getter\">html-getter</a> which you can use like this:</p>\n<pre><code class=\"hljs\">\nbun run src/html-getter.ts https://www.peterbe.com\n</code></pre>\n\n<p>That will open <code>https://www.peterbe.com</code> with Chrome or Chromium or WebKit and evaluate the following JavaScript expression: <code>document.documentElement.outerHTML</code> (which is the whole HTML source of the whole DOM) and print this to stdout.</p>\n<p>or you can use it like this:</p>\n<pre><code class=\"hljs\">\nbun run src/html-getter.ts --body --screenshot /tmp/page.png https://www.peterbe.com\n</code></pre>\n\n<p>which means it prints everything inside the <code>&lt;body&gt;...&lt;/body&gt;</code> instead and at the same time, it dumps a screenshot of what it looks like, which can be useful for debugging.</p>\n<p>What's cool about this is that you no longer need to install <code>playwright</code> or <code>puppeteer</code>. Scraping HTML is just one of many applications you can use <code>WebView</code> for. I wouldn't use it for automated headless browser testing though because the documentation, tooling, and APIs for Playwright is still so much much better.</p>\n<p><strong>WARNING</strong></p>\n<p>At the time of writing this, I am unable, on macOS, to compile this Bun script into a binary. When I do, this happens:</p>\n<pre><code class=\"hljs\">\n$ bun run build\n$ bun build src/html-getter.ts --target=bun --outfile out/html-getter --compile --bytecode\n  [14ms]  bundle  1 modules\n  [77ms] compile  out/html-getter\n$ ./out/html-getter https://www.peterbe.com\n[1]    70304 killed     ./out/html-getter https://www.peterbe.com\n</code></pre>\n\n<p>The same worked on my x64 Linux server, but to get this to compile on macOS might need some more careful thought.</p>", "url": "https://github.com/peterbe/html-getter", "disallow_comments": false, "split": null}, {"oid": "bestest-security-tip-for-updating-packages-with-bun", "title": "Bestest security tip for updating packages with Bun", "pub_date": "2026-04-14T14:25:40.655Z", "comments": 0, "categories": ["Bun"], "html": "<p>I don't know when this was added but if you use Bun in your TypeScript project, you might be familiar with <code>bun upgrade</code> which is a CLI tool for upgrading the packages you pin and depend on. You can now pass it a \"cool down period\" which means a certain package update doesn't count unless it's been published for at least X hours.</p>\n<p><strong>This is critical for avoiding installing compromised NPM packages.</strong> Sometimes a package gets hacked. If you were to be unlucky and upgrade to it at that window of time, you can have pinned an insecure/compromised version into your lock file. \nWhen the <a href=\"https://www.trendmicro.com/en_us/research/26/c/axios-npm-package-compromised.html\">Axios supply chain attack</a> happened, there was a 39 minute window when installing the latest version of <code>axios</code> would (potentially) sneak in the bad package version.</p>\n<p>Now, this is how I use <code>bun update</code>:</p>\n<pre><code class=\"hljs\">\nbun update --interactive --minimum-release-age=86400\n</code></pre>\n\n<p>That means, it won't upgrade anything that hasn't sat for at least 1 day.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "pytest-import-file-mismatch", "title": "pytest \"import file mismatch\"", "pub_date": "2026-04-01T13:20:53.199Z", "comments": 0, "categories": ["Python"], "html": "<p>I stumbled into a strange error when running <code>pytest</code> in a side-project:</p>\n<pre>import file mismatch:\nimported module &#x27;test_comments&#x27; has this __file__ attribute:\n  /&lt;root&gt;/django-peterbecom/peterbecom/api/tests/test_comments.py\nwhich is not the same as the test file we want to collect:\n  /&lt;root&gt;/django-peterbecom/peterbecom/publicapi/tests/test_comments.py\nHINT: remove __pycache__ / .pyc files and/or use a unique basename for your test file modules</pre>\n\n<p>I desperately made sure I deleted every <code>__pycache__</code> directory and every <code>*.pyc</code> file. I also ran <code>rm -fr .pytest_cache</code>.</p>\n<p>It was strange because I knew I had, in the past, had two files called exactly the same. E.g. <code>test_utils.py</code>.</p>\n<p>Turns out, you <em>can</em> have two <code>test_something.py</code> called exactly the same but they <strong>must both sit in a directory that has a <code>__init__.py</code></strong>.</p>\n<p>So there was nothing wrong with <code>/&lt;root&gt;/django-peterbecom/peterbecom/api/tests/test_comments.py</code>. I just had to do:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-built_in\">touch</span> /&lt;root&gt;/django-peterbecom/peterbecom/api/tests/__init__.py\n</code></pre>\n\n<p>...and the problem got solved.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "copy-the-current-line-in-vs-code", "title": "Copy the current line in VS Code", "pub_date": "2026-03-12T15:07:52.002Z", "comments": 0, "categories": ["macOS"], "html": "<p><strong>tl;dr; In VS Code you can copy the current line without first selecting it. Cmd-C without selection defaults to whole line.</strong></p>\n<p>One of those \"How did I not know this until now?!?!\"<br />\nVS Code is not emacs even though its emulation gets many things right. But one thing that I only just learned is that to copy a whole current line, you don't have to select it first. If nothing is selected, whichever line the cursor is on gets the entire line copied.</p>\n<p>The regular way of doing it:</p>\n<video controls poster=\"/cache/0f/21/0f21ee81bf7fcf4fd4555b9628219961.jpg\" style=\"max-width:100%\">  \n  <source src=\"/cache/plog/copy-the-current-line-in-vs-code/453259ccea.1773321273.mov\" type=\"video/mov\"/>\n  <source src=\"/cache/plog/copy-the-current-line-in-vs-code/453259ccea.1773321273.mp4\" type=\"video/mp4\"/>\n  <source src=\"/cache/plog/copy-the-current-line-in-vs-code/453259ccea.1773321273.webm\" type=\"video/webm\"/>\n  Download the <a href=\"/cache/plog/copy-the-current-line-in-vs-code/453259ccea.1773321273.mov\">video/mov</a> or <a href=\"/cache/plog/copy-the-current-line-in-vs-code/453259ccea.1773321273.mp4\">video/mp4</a> or <a href=\"/cache/plog/copy-the-current-line-in-vs-code/453259ccea.1773321273.webm\">video/webm</a>\n</video>\n\n<p><em>the keyboard use here is down, Ctrl-Shift-left, Ctrl-Shift-left, Cmd-C. Then moving down to a new line and Cmd-V</em></p>\n<p>Here, note that the cursor is just somewhere on line 6:</p>\n<video controls poster=\"/cache/ab/c0/abc0d70308a40cdc6e3121f272de23fe.jpg\" style=\"max-width:100%\">  \n  <source src=\"/cache/plog/copy-the-current-line-in-vs-code/0bac73b362.1773321406.mov\" type=\"video/mov\"/>\n  <source src=\"/cache/plog/copy-the-current-line-in-vs-code/0bac73b362.1773321406.mp4\" type=\"video/mp4\"/>\n  <source src=\"/cache/plog/copy-the-current-line-in-vs-code/0bac73b362.1773321406.webm\" type=\"video/webm\"/>\n  Download the <a href=\"/cache/plog/copy-the-current-line-in-vs-code/0bac73b362.1773321406.mov\">video/mov</a> or <a href=\"/cache/plog/copy-the-current-line-in-vs-code/0bac73b362.1773321406.mp4\">video/mp4</a> or <a href=\"/cache/plog/copy-the-current-line-in-vs-code/0bac73b362.1773321406.webm\">video/webm</a>\n</video>\n\n<p><em>the keyboard use here is down, down, Cmd-C, down, Cmd-V</em></p>\n<h4 id=\"bonus\"><a class=\"toclink\" href=\"#bonus\">Bonus</a></h4>\n<p>In this particular demo video, I'm just replicating line 6 and putting a copy of that on (new) line 8. There's actually another keyboard shortcut sequence for that: Option+Shift+down then Option+down</p>\n<p>Option+Shift+down replicates the current line and puts a copy right below. Then Option+down moves the current line.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "logger.error-or-logger.exception", "title": "logger.error or logger.exception in Python", "pub_date": "2026-03-06T15:27:44.219Z", "comments": 0, "categories": ["Python"], "html": "<p>Consider this Python code:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">try</span>:\n    <span class=\"hljs-number\">1</span> / <span class=\"hljs-number\">0</span>\n<span class=\"hljs-keyword\">except</span> Exception <span class=\"hljs-keyword\">as</span> e:\n    logger.error(<span class=\"hljs-string\">&quot;An error occurred while dividing by zero.: %s&quot;</span>, e)\n</code></pre>\n\n<p>The output of this is:</p>\n<pre><code class=\"hljs\">\nAn error occurred while dividing by zero.: division by zero\n</code></pre>\n\n<p>No traceback. Perhaps you don't care because you don't need it.<br />\nI see code like this quite often and it's curious that you even use <code>logger.error</code> if it's not a problem. And it's curious that you include the stringified exception into the logger message.</p>\n<p>Another common pattern I see is use of <code>exc_info=True</code> like this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">try</span>:\n    <span class=\"hljs-number\">1</span> / <span class=\"hljs-number\">0</span>\n<span class=\"hljs-keyword\">except</span> Exception:\n    logger.error(<span class=\"hljs-string\">&quot;An error occurred while dividing by zero.&quot;</span>, exc_info=<span class=\"hljs-literal\">True</span>)\n</code></pre>\n\n<p>Its output is:</p>\n<pre><code class=\"hljs\">\nAn error occurred while dividing by zero.\nTraceback (most recent call last):\n  File &quot;/Users/peterbengtsson/dummy.py&quot;, line 23, in &lt;module&gt;\n    1 / 0\n    ~~^~~\nZeroDivisionError: division by zero\n</code></pre>\n\n<p>Ok, now you get the traceback and the error value (<code>division by zero</code> in this case).</p>\n<p>But a more convenient function is <code>logger.exception</code> which looks like this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">try</span>:\n    <span class=\"hljs-number\">1</span> / <span class=\"hljs-number\">0</span>\n<span class=\"hljs-keyword\">except</span> Exception:\n    logger.exception(<span class=\"hljs-string\">&quot;An error occurred while dividing by zero.&quot;</span>)\n</code></pre>\n\n<p>Its output is:</p>\n<pre><code class=\"hljs\">\nAn error occurred while dividing by zero.\nTraceback (most recent call last):\n  File &quot;/Users/peterbengtsson/dummy.py&quot;, line 9, in &lt;module&gt;\n    1 / 0\n    ~~^~~\nZeroDivisionError: division by zero\n</code></pre>\n\n<p>So it's sugar for <code>logger.error</code>.</p>\n<p>Also, a common logging config is something like this:</p>\n<pre>import logging\n\nlogger = logging.getLogger(__name__)\nlogging.basicConfig(\n    format=&quot;%(asctime)s - %(levelname)s - %(name)s - %(message)s&quot;, level=logging.ERROR\n)</pre>\n\n<p>So if you use <code>logger.exception</code> what will it print? In short, the same as if you used <code>logger.error</code>. For example, with the <code>logger.exception(\"An error occurred while dividing by zero.\")</code> line above:</p>\n<pre><code class=\"hljs\">\n2026-03-06 10:45:23,570 - ERROR - __main__ - An error occurred while dividing by zero.\nTraceback (most recent call last):\n  File &quot;/Users/peterbengtsson/dummy.py&quot;, line 12, in &lt;module&gt;\n    1 / 0\n    ~~^~~\nZeroDivisionError: division by zero\n</code></pre>\n\n<h3 id=\"bonus-add_note\"><a class=\"toclink\" href=\"#bonus-add_note\">Bonus - <code>add_note</code></a></h3>\n<p>You can, if it's applicable, inject some more information about the exception. Consider:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">try</span>:\n    n / <span class=\"hljs-number\">0</span>\n<span class=\"hljs-keyword\">except</span> Exception <span class=\"hljs-keyword\">as</span> exception:\n    exception.add_note(<span class=\"hljs-string\">f&quot;The numerator was <span class=\"hljs-subst\">{n}</span>.&quot;</span>)\n    logger.exception(<span class=\"hljs-string\">&quot;An error occurred while dividing by zero.&quot;</span>)\n</code></pre>\n\n<p>The net output of this is:</p>\n<pre><code class=\"hljs\">\n2026-03-06 10:48:34,279 - ERROR - __main__ - An error occurred while dividing by zero.\nTraceback (most recent call last):\n  File &quot;/Users/peterbengtsson/dummy.py&quot;, line 13, in &lt;module&gt;\n    1 / 0\n    ~~^~~\nZeroDivisionError: division by zero\nThe numerator was 123.\n</code></pre>", "url": null, "disallow_comments": false, "split": null}, {"oid": "how-to-find-which-git-sha-it-was-when-you-merged-in-the-default-branch", "title": "How to find which git SHA it was when you merged in the default branch", "pub_date": "2026-02-26T17:23:04.239Z", "comments": 1, "categories": ["Linux", "Git"], "html": "<p>Please tell me there's a better way.</p>\n<p>I'm in a git feature branch. It was branched off of <code>master</code> some time ago. Then I made some commits. Later, I merged in latest <code>master</code>, and after that some more commits. The question is what was the SHA when I merged in latest <code>master</code>? How to find that out?</p>\n<p>Here's what I did:</p>\n<p>I'm in my feature branch and I run:</p>\n<pre><code class=\"hljs\">\ngit <span class=\"hljs-built_in\">log</span> --merges --first-parent\n</code></pre>\n\n<p>It prints this out:</p>\n<pre><code class=\"hljs\">\ncommit 2c010ef6e94d792f25f7309ed83a2d35e41e5051\nMerge: 24b2a2513d6f e2817859ed02\nAuthor: Peter Bengtsson &lt;email&gt;\nDate:   Thu Feb 26 14:06:24 2026 +0000\n\n  Merge remote-tracking branch &#x27;origin&#x27; into name-of-my-feature-branch\n</code></pre>\n\n<p>The SHA that was, on <code>master</code>, at the time of merging, was <code>e2817859ed02</code> which is the second thing the <code>Merge: 24b2a2513d6f e2817859ed02</code> line.</p>\n<p>Now, equipped with this I can go back in time <code>git checkout e2817859ed02</code> and on that detached HEAD, I can do things like run (unit) tests to see if the <code>master</code> branch was busted at the time I merged it into my feature branch.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "gg2-branches-got-50percent-faster-by-a-promise.all", "title": "gg2 branches got 50% faster by a Promise.all", "pub_date": "2026-02-16T13:17:09.034Z", "comments": 3, "categories": ["TypeScript", "Bun", "JavaScript"], "html": "<p><a href=\"https://gg2.peterbe.com/\">gg2</a> is a command line tool for doing things with Git branches, faster. One of the convenient commands is <code>gg2 branches</code>. \nIt lists your recent branches, sorted by committer-date and for each branch it displays a human-friendly date distance and whether it's already been merged. Looks like this:</p>\n<p><a href=\"/cache/d9/44/d94443c94b1f1d57abc02f7d4dcb717c.png\"><img src=\"/cache/d9/44/d94443c94b1f1d57abc02f7d4dcb717c.png\" class=\"fullsize\"></a></p>\n<p>Turns out, for large repos figuring out which branches have already been merged is quite slow. For example, in this repo it takes half a second to list all merged branches:</p>\n<pre><code class=\"hljs\">\n$ git branch --all --merged | wc -l\n669\n\n$ time git branch --all --merged 1&gt;/dev/null\n\nreal    0m0.490s\nuser    0m0.460s\nsys     0m0.030s\n</code></pre>\n\n<p>The problem was that <code>gg2 branches</code> also does multiple other <code>git branch</code> listing commands to get information. In particular: get the current branch, figure out the default branch, a list of branch committer dates, and a list of merged branches.<br />\nAll of these async operations were done in serial. With a bit of rearranging, and refactoring, all of these can be turned into this:</p>\n<pre><code class=\"hljs\">\n<span class=\"hljs-keyword\">const</span> [defaultBranch, currentBranch, dates, isMerged] = <span class=\"hljs-keyword\">await</span> <span class=\"hljs-title class_\">Promise</span>.<span class=\"hljs-title function_\">all</span>([\n  <span class=\"hljs-title function_\">getDefaultBranch</span>(git),\n  <span class=\"hljs-title function_\">getCurrentBranch</span>(git),\n  <span class=\"hljs-title function_\">getAllBranchDates</span>(git),\n  <span class=\"hljs-title function_\">getAllMergedBranches</span>(git),\n])\n</code></pre>\n\n<p>The <code>getAllMergedBranches(git)</code> function still takes a very long time and I think it lists all remote branches that have been merged. Anyway, now it's only the slowest part of the <code>Promise.all</code> rather than one of multiple.</p>\n<p><strong>UPDATE</strong></p>\n<p>The act of blogging about something can sometimes bring out how wrong you are.<br />\nWhat I didn't realize is that I was using <code>git branch --all --merged</code> to know which local branches have already been merged into the default branch. But you can just use <code>git branch --merged</code> to get the same result but much much faster.</p>\n<p><a href=\"https://github.com/peterbe/gg2/pull/101\">Correcting for this</a>, on a large repo, the average time to execute <code>gg2 branches</code> went from median 541ms to median 188ms. So that's 3x speed improvement.</p>", "url": null, "disallow_comments": false, "split": null}, {"oid": "xbar-my-prs", "title": "xbar-my-prs Info about your GitHub PRs in the macOS menu bar", "pub_date": "2026-01-29T18:37:54.287Z", "comments": 0, "categories": ["TypeScript", "macOS", "Bun"], "html": "<p><a href=\"https://github.com/peterbe/xbar-my-prs\">xbar-my-prs</a> is a plugin I wrote for something called <a href=\"https://xbarapp.com/\">xbar</a> which is a macOS app that let's you write scripts that then show up in the macOS menu bar. Mine is about fetching information about my recent GitHub pull requests.</p>\n<p>A picture of it in action:</p>\n<p><a href=\"/cache/b8/f6/b8f6c9b52314c6a7674ac86d44fc8a46.png\"><img src=\"/cache/b8/f6/b8f6c9b52314c6a7674ac86d44fc8a46.png\" class=\"fullsize\" style=\"width:70%\"></a></p>\n<p>Every minute, it does this GitHub search: <a href=\"https://github.com/search?q=is%3Apr+is%3Aopen+author%3A%40me+sort%3Aupdated+&amp;type=pullrequests\"><code>is:pr author:@me sort:updated</code></a></p>\n<p>Then it parses the results and attempts to display a neat summary.</p>\n<p>And each time it runs, it stores a copy of the result in the <code>/tmp</code> directory so that it can compare what the result became compared to before. So if something has changed in the last minute, it displays a summary of the change(s). Example:</p>\n<p><a href=\"/cache/d5/d5/d5d5ee05f145495eb912840129a5b20b.png\"><img src=\"/cache/d5/d5/d5d5ee05f145495eb912840129a5b20b.png\" class=\"fullsize\" style=\"width:60%\"></a></p>\n<h3 id=\"how-you-can-get-it-too\"><a class=\"toclink\" href=\"#how-you-can-get-it-too\">How you can get it too</a></h3>\n<p>First you have to install <a href=\"https://xbarapp.com/\">xbar</a> onto your macOS.</p>\n<p>At the time of writing you can't install it with Homebrew. Working on that. But all you need is <code>just</code> and Bun and then you can install it like this:</p>\n<pre><code class=\"hljs\">\ngit <span class=\"hljs-built_in\">clone</span> https://github.com/peterbe/xbar-my-prs.git &amp;&amp; <span class=\"hljs-built_in\">cd</span> xbar-my-prs\njust install\njust ship\n</code></pre>\n\n<p>If all goes well, it should have crated an executable file at:</p>\n<pre><code class=\"hljs\">\n~/Library/Application\\ Support/xbar/plugins/my-prs.1m.bin\n</code></pre>\n\n<p>on your system. And that should be all xbar needs to know there's a new plugin to run.</p>", "url": "https://github.com/peterbe/xbar-my-prs", "disallow_comments": false, "split": null}, {"oid": "optimizing-bun-compiled-binary-for-gg2", "title": "Optimizing Bun compiled binary for gg2", "pub_date": "2026-01-13T16:06:09.784Z", "comments": 0, "categories": ["TypeScript", "Bun"], "html": "<p><strong>tl;dr; adding <code>--bytecode</code> and <code>--production</code> to <code>bun build --compile</code> gives a marginal boost.</strong></p>\n<p>I have a CLI tool called <a href=\"https://gg2.peterbe.com/\">gg2</a> which is written in TypeScript and using Bun <strong>compiled to a single executable binary</strong> that can be <a href=\"https://www.peterbe.com/plog/gg2-is-now-installable-with-homebrew\">installed with Homebrew</a>. It's fast. <a href=\"https://www.peterbe.com/plog/gg2-is-now-installable-with-homebrew\">Only 17% slower than GitHub's Go-compile <code>gh</code> CLI</a>. But can it get even faster?</p>\n<p>I read about <a href=\"https://bun.com/docs/bundler/bytecode\">Bytecode Caching</a> which is supposed to make the executable \"dramatically improved\". Hmm. Let's see. There's also the <code>--production</code> option, which according to <code>Set NODE_ENV=production and enable minification</code>. Not convinced it's doing much but let's try.</p>\n<p>I compiled the binary 4 different ways:</p>\n<ul>\n<li><code>bun build src/index.ts --target=bun --compile --outfile out/gg</code></li>\n<li><code>bun build src/index.ts --target=bun --compile --outfile out/gg-bytecode --bytecode</code></li>\n<li><code>bun build src/index.ts --target=bun --compile --outfile out/gg-production --production</code></li>\n<li><code>bun build src/index.ts --target=bun --compile --outfile out/gg-bytecode-production --production --bytecode</code></li>\n</ul>\n<p>Then I ran <code>hyperfine</code> like this:</p>\n<pre><code class=\"hljs\">\nhyperfine &quot;./out/gg --version&quot; &quot;./out/gg-bytecode --version&quot; &quot;./out/gg-production --version&quot; &quot;./out/gg-bytecode-production --version&quot; --warmup 6\n</code></pre>\n\n<p>The results are as follows:</p>\n<table>\n<thead>\n<tr>\n<th>BINARY</th>\n<th>TIME (ms, smaller is better)</th>\n<th>SIZE (MB)</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>gg</td>\n<td>87</td>\n<td>58</td>\n</tr>\n<tr>\n<td>gg-bytecode</td>\n<td>81</td>\n<td>60</td>\n</tr>\n<tr>\n<td>gg-production</td>\n<td>82.4</td>\n<td>55</td>\n</tr>\n<tr>\n<td>gg-bytecode-production</td>\n<td>79.6</td>\n<td>60</td>\n</tr>\n</tbody>\n</table>\n<p><a href=\"/cache/c7/1f/c71f48b8b4ab4acb5c455a140d0301da.png\"><img src=\"/cache/c7/1f/c71f48b8b4ab4acb5c455a140d0301da.png\" class=\"fullsize\"></a></p>\n<p>(<em>sorry for being terrible at using Excel to draw charts</em>)</p>\n<h3 id=\"conclusion\"><a class=\"toclink\" href=\"#conclusion\">Conclusion</a></h3>\n<p>I'm skeptical. The <code>hyperfine</code> often complains about statistical outliers so you have to run it with warmup and re-run it till it doesn't complain.</p>\n<p>One caveat with using <code>--bytecode</code> is that the compiled code is not re-usable across <strong>different versions of bun</strong> but I don't understand how that's applicable if it's a executable binary that is shipped outside of Bun.</p>\n<p>I'm going to add <code>--bytecode</code> and <code>--production</code> to the next release of <a href=\"https://github.com/peterbe/gg2\"><code>gg2</code></a>.</p>", "url": null, "disallow_comments": false, "split": null}]}